İyi Beslenmiş Filozoflar veya Rekabetçi .NET Programlaması

İyi Beslenmiş Filozoflar veya Rekabetçi .NET Programlaması

Filozofların Yemek Problemini örnek olarak kullanarak .Net'te eşzamanlı ve paralel programlamanın nasıl çalıştığını görelim. Konuların/işlemlerin senkronizasyonundan aktör modeline kadar (ileriki bölümlerde) plan şu şekildedir. Makale, ilk tanışma için veya bilginizi tazelemek için yararlı olabilir.

Neden hiç? Transistörler minimum boyutlarına ulaşır, Moore yasası ışık hızının sınırlandırılmasına dayanır ve bu nedenle sayısında bir artış gözlenir, daha fazla transistör yapılabilir. Aynı zamanda, veri miktarı artıyor ve kullanıcılar sistemlerden anında yanıt bekliyor. Böyle bir durumda, çalışan bir iş parçacığımız olduğunda "normal" programlama artık etkili değildir. Eşzamanlı veya eşzamanlı yürütme sorununu bir şekilde çözmeniz gerekir. Ayrıca, bu sorun farklı düzeylerde mevcuttur: iş parçacığı düzeyinde, işlemler düzeyinde, ağdaki makineler düzeyinde (dağıtılmış sistemler). .NET, bu tür sorunları hızlı ve verimli bir şekilde çözmek için yüksek kaliteli, zaman içinde test edilmiş teknolojilere sahiptir.

Görev

Edsger Dijkstra, bu problemi öğrencilerine 1965 gibi erken bir tarihte ortaya koydu. Yerleşik formülasyon aşağıdaki gibidir. Belli sayıda (genellikle beş) filozof ve aynı sayıda çatal vardır. Yuvarlak bir masaya otururlar, aralarında çatallar vardır. Filozoflar tabaklarındaki sonsuz yemeği yiyebilir, düşünebilir veya bekleyebilirler. Bir filozof yemek için iki çatal almanız gerekir (sonuncusu çatalı ilkiyle paylaşır). Çatalı almak ve bırakmak iki ayrı eylemdir. Tüm filozoflar sessiz. Görev öyle bir algoritma bulmaktır ki 54 yıl sonra bile hepsi düşünüp dolsun.

İlk olarak, ortak bir alan kullanarak bu sorunu çözmeye çalışalım. Çatallar ortak masanın üzerinde duruyor ve filozoflar olduklarında onları alıp geri koyuyorlar. Burada senkronizasyonla ilgili sorunlar var, tam olarak ne zaman surebets almalı? ya çatal yoksa? vs. Ama önce filozoflardan başlayalım.

İleti dizilerini başlatmak için bir iş parçacığı havuzu kullanırız. Task.Run yöntem:

var cancelTokenSource = new CancellationTokenSource();
Action<int> create = (i) => RunPhilosopher(i, cancelTokenSource.Token);
for (int i = 0; i < philosophersAmount; i++) 
{
    int icopy = i;
    // Поместить задачу в очередь пула потоков. Метод RunDeadlock не запускаеться 
    // сразу, а ждет своего потока. Асинхронный запуск.
    philosophers[i] = Task.Run(() => create(icopy), cancelTokenSource.Token);
}

İş parçacığı havuzu, iş parçacığı oluşturma ve silmeyi optimize etmek için tasarlanmıştır. Bu havuzun görevleri olan bir kuyruğu vardır ve CLR, bu görevlerin sayısına bağlı olarak iş parçacıkları oluşturur veya kaldırır. Tüm Uygulama Etki Alanları için bir havuz. Bu havuz hemen hemen her zaman kullanılmalıdır, çünkü. ileti dizileri, kuyrukları vb. oluşturmak, silmekle uğraşmanıza gerek yok. Havuz olmadan da mümkündür, ancak o zaman doğrudan kullanmanız gerekir. Thread, bu, bir iş parçacığının önceliğini değiştirmeniz gerektiğinde, uzun bir işlemimiz olduğunda, bir Ön plan iş parçacığı için vs. kullanışlıdır.

Diğer bir deyişle, System.Threading.Tasks.Task sınıf aynı Thread, ancak her türlü kolaylıklara sahip: bir görevi bir dizi başka görevden sonra çalıştırma, bunları işlevlerden döndürme, uygun şekilde kesintiye uğratma ve daha fazlası. vb. Async / wait yapılarını desteklemek için gereklidirler (Görev tabanlı Asenkron Model, IO işlemlerini beklemek için sözdizimsel şeker). Bunun hakkında daha sonra konuşacağız.

CancelationTokenSource burada, iş parçacığının kendisini çağıran iş parçacığının sinyalinde sonlandırabilmesi için gereklidir.

Senkronizasyon sorunları

Engellenen Filozoflar

Tamam, nasıl konu açılacağını biliyoruz, hadi öğle yemeği yemeye çalışalım:

// Кто какие вилки взял. К примеру: 1 1 3 3 - 1й и 3й взяли первые две пары.
private int[] forks = Enumerable.Repeat(0, philosophersAmount).ToArray();

// То же, что RunPhilosopher()
private void RunDeadlock(int i, CancellationToken token) 
{
    // Ждать вилку, взять её. Эквивалентно: 
    // while(true) 
    //     if forks[fork] == 0 
    //          forks[fork] = i+1
    //          break
    //     Thread.Sleep() или Yield() или SpinWait()
    void TakeFork(int fork) =>
        SpinWait.SpinUntil(() => 
            Interlocked.CompareExchange(ref forks[fork], i+1, 0) == 0);

    // Для простоты, но можно с Interlocked.Exchange:
    void PutFork(int fork) => forks[fork] = 0;

    while (true)
    {
        TakeFork(Left(i));
        TakeFork(Right(i));
        eatenFood[i] = (eatenFood[i] + 1) % (int.MaxValue - 1);
        PutFork(Left(i));
        PutFork(Right(i));
        Think(i);

        // Завершить работу по-хорошему.
        token.ThrowIfCancellationRequested();
    }
}

Burada önce sol çatalı, sonra sağ çatalı almaya çalışıyoruz ve işe yararsa o zaman yiyip geri koyuyoruz. Bir çatal almak atomiktir, yani. iki iş parçacığı aynı anda birini alamaz (yanlış: ilki çatalın serbest olduğunu okur, ikincisi de birinci alır, ikincisi alır). Bunun için Interlocked.CompareExchangebir işlemci talimatı ile uygulanması gereken (TSL, XCHG), atomik sıralı okuma ve yazma için bir bellek parçasını kilitler. Ve SpinWait yapıya eşdeğerdir while(true) sadece biraz "sihir" ile - iş parçacığı işlemciyi alır (Thread.SpinWait), ancak bazen kontrolü başka bir konuya aktarır (Thread.Yeild) veya uykuya dalar (Thread.Sleep).

Ancak bu çözüm işe yaramıyor, çünkü akışlar çok geçmeden (benim için bir saniye içinde) tıkanır: tüm filozoflar sol çatallarını alırlar ama sağ çatallarını almazlar. Forks dizisi şu değerlere sahiptir: 1 2 3 4 5.

İyi Beslenmiş Filozoflar veya Rekabetçi .NET Programlaması

Şekilde, engelleme konuları (kilitlenme). Yeşil - yürütme, kırmızı - senkronizasyon, gri - iş parçacığı uyuyor. Eşkenar dörtgenler, Görevlerin başlangıç ​​zamanını gösterir.

Filozofların Açlığı

Özellikle yemek için çok fazla düşünmek gerekli olmasa da, açlık insanı felsefeden vazgeçirir. Problemimizde ipliklerin aç kalma durumunu simüle etmeye çalışalım. Açlık, bir iş parçacığının çalıştığı, ancak önemli bir iş yapılmadığı zamandır, başka bir deyişle, bu aynı kilitlenmedir, ancak şimdi iş parçacığı uyumuyor, ancak aktif olarak yiyecek bir şeyler arıyor, ancak yiyecek yok. Sık sık tıkanmayı önlemek için, bir tane daha alamamışsak çatalı geri koyacağız.

// То же что и в RunDeadlock, но теперь кладем вилку назад и добавляем плохих философов.
private void RunStarvation(int i, CancellationToken token)
{
    while (true)
    {
        bool hasTwoForks = false;
        var waitTime = TimeSpan.FromMilliseconds(50);
        // Плохой философов может уже иметь вилку:
        bool hasLeft = forks[Left(i)] == i + 1;
        if (hasLeft || TakeFork(Left(i), i + 1, waitTime))
        {
            if (TakeFork(Right(i), i + 1, TimeSpan.Zero))
                hasTwoForks = true;
            else
                PutFork(Left(i)); // Иногда плохой философ отдает вилку назад.
        } 
        if (!hasTwoForks)
        {
            if (token.IsCancellationRequested) break;
            continue;
        }
        eatenFood[i] = (eatenFood[i] + 1) % (int.MaxValue - 1);
        bool goodPhilosopher = i % 2 == 0;
        // А плохой философ забывает положить свою вилку обратно:
        if (goodPhilosopher)
            PutFork(Left(i));
        // А если и правую не положит, то хорошие будут вообще без еды.
        PutFork(Right(i));

        Think(i);

        if (token.IsCancellationRequested)
            break;
    }
}

// Теперь можно ждать определенное время.
bool TakeFork(int fork, int philosopher, TimeSpan? waitTime = null)
{
    return SpinWait.SpinUntil(
        () => Interlocked.CompareExchange(ref forks[fork], philosopher, 0) == 0,
              waitTime ?? TimeSpan.FromMilliseconds(-1)
    );
}

Bu kodla ilgili önemli olan şey, dört filozoftan ikisinin sol çatalını bırakmayı unutmasıdır. Ve iplikler aynı önceliğe sahip olmasına rağmen, diğerleri aç kalmaya başlarken daha fazla yemek yedikleri ortaya çıktı. Burada tamamen açlıktan ölmüyorlar çünkü. kötü filozoflar bazen çatallarını geri koyarlar. İyi insanların kötülerden yaklaşık 5 kat daha az yedikleri ortaya çıktı. Yani koddaki küçük bir hata performansın düşmesine neden olur. Burada ayrıca, tüm filozofların sol çatalı aldığı, sağ çatalın olmadığı, solu koyduğu, beklediği, tekrar solu aldığı vb. Nadir bir durumun mümkün olduğunu belirtmekte fayda var. Bu durum aynı zamanda bir açlık, daha çok bir çıkmazdır. tekrar edemedim. Aşağıda, iki kötü filozofun iki çatalı da aldığı ve iki iyi filozofun açlıktan öldüğü bir durum için bir resim var.

İyi Beslenmiş Filozoflar veya Rekabetçi .NET Programlaması

Burada konuların bazen uyandığını ve kaynağı almaya çalıştığını görebilirsiniz. Dört çekirdekten ikisi hiçbir şey yapmıyor (yukarıdaki yeşil grafik).

Bir Filozofun Ölümü

Pekala, filozofların şanlı bir yemeğini kesintiye uğratabilecek bir başka sorun da, içlerinden birinin elinde çatallarla aniden ölmesidir (ve onu bu şekilde gömeceklerdir). Sonra komşular yemeksiz kalacak. Bu durum için kendiniz bir örnek kod bulabilirsiniz, örneğin, dışarı atılır. NullReferenceException filozof çatalları aldıktan sonra. Ve bu arada, istisna işlenmeyecek ve çağıran kod sadece onu yakalamayacak (bunun için AppDomain.CurrentDomain.UnhandledException ve benzeri.). Bu nedenle, iş parçacıklarının kendilerinde ve zarif bir şekilde sonlandırılan hata işleyicilere ihtiyaç vardır.

garson

Peki bu kilitlenme, açlık ve ölüm sorununu nasıl çözeceğiz? Sadece bir filozofun çatallara ulaşmasına izin vereceğiz, bu yer için karşılıklı bir iplik dışlaması ekleyeceğiz. Nasıl yapılır? Filozofların yanında duran ve herhangi bir filozofa çatalları almasına izin veren bir garson olduğunu varsayalım. Bu garsonu nasıl yapacağız ve filozoflar ona nasıl soracaklar, merak edilen sorular.

En basit yol, filozofların sürekli olarak garsondan çatallara erişim istemesidir. Onlar. şimdi filozoflar yakınlarda bir çatal beklemeyecek, bekleyecek veya garsona soracaklar. İlk başta, bunun için yalnızca Kullanıcı Alanını kullanıyoruz, içinde çekirdekten herhangi bir prosedürü çağırmak için kesintiler kullanmıyoruz (aşağıda onlar hakkında).

Kullanıcı alanındaki çözümler

Burada bir çatal ve iki filozof ile eskiden yaptığımızın aynısını yapacağız, bir döngü içinde dönüp bekleyeceğiz. Ama şimdi hepsi filozof olacak ve olduğu gibi sadece bir çatal olacak, yani. sadece garsondan bu “altın çatalı” alan filozofun yemek yiyeceği söylenebilir. Bunun için SpinLock kullanıyoruz.

private static SpinLock spinLock = new SpinLock();  // Наш "официант"
private void RunSpinLock(int i, CancellationToken token)
{
    while (true)
    {
        // Взаимная блокировка через busy waiting. Вызываем до try, чтобы
        // выбрасить исключение в случае ошибки в самом SpinLock.
        bool hasLock = false;
        spinLock.Enter(ref hasLock);
        try
        {
            // Здесь может быть только один поток (mutual exclusion).
            forks[Left(i)] = i + 1;  // Берем вилку сразу, без ожидания.
            forks[Right(i)] = i + 1;
            eatenFood[i] = (eatenFood[i] + 1) % (int.MaxValue - 1);
            forks[Left(i)] = 0;
            forks[Right(i)] = 0;
        }
        finally
        {
            if(hasLock) spinLock.Exit();  // Избегаем проблемы со смертью философа.
        }

        Think(i);

        if (token.IsCancellationRequested)
            break;
    }
}

SpinLock bu bir engelleyici, kabaca söylemek gerekirse, aynı while(true) { if (!lock) break; }, ama içinde olduğundan daha fazla "sihir" ile SpinWait (orada kullanılan). Artık bekleyenleri saymayı, biraz uyutmayı ve daha fazlasını biliyor. vb. Genel olarak optimize etmek için mümkün olan her şeyi yapar. Ancak bunun, işlemci kaynaklarını tüketen ve akışı koruyan aynı aktif döngü olduğunu hatırlamalıyız; bu, filozoflardan biri diğerlerinden daha öncelikli hale gelirse, ancak altın bir çatala sahip değilse (Öncelik Tersine Çevirme sorunu) açlığa yol açabilir. . Bu nedenle, üçüncü taraf çağrıları, iç içe kilitler ve diğer sürprizler olmadan yalnızca paylaşılan bellekteki çok çok kısa değişiklikler için kullanıyoruz.

İyi Beslenmiş Filozoflar veya Rekabetçi .NET Programlaması

için çizim SpinLock. Akışlar sürekli olarak altın çatal için "mücadele ediyor". Hatalar var - şekilde, seçilen alanda. Çekirdekler tam olarak kullanılmaz: bu dört iş parçacığı tarafından yalnızca yaklaşık 2/3.

Buradaki başka bir çözüm, yalnızca kullanmak olacaktır. Interlocked.CompareExchange yukarıdaki kodda (açlıktan ölmekte olan filozoflarda) gösterilenle aynı aktif bekleme ile, ancak bu, daha önce de belirtildiği gibi, teorik olarak tıkanmaya yol açabilir.

Hakkında Interlocked Unutulmamalıdır ki, sadece CompareExchange, aynı zamanda atomik okuma VE yazma için diğer yöntemler. Ve değişiklik tekrarı yoluyla, başka bir iş parçacığının değişikliklerini yapmak için zamanı olması durumunda (okuma 1, okuma 2, yazma 2, yazma 1 kötü), tek bir değerde karmaşık değişiklikler için kullanılabilir (Kilitli Herhangi bir model).

Çekirdek Modu Çözümleri

Bir döngüde kaynak israfını önlemek için bir ileti dizisini nasıl engelleyebileceğimize bakalım. Diğer bir deyişle, örneğimize devam ederek, garsonun filozofu nasıl uyuttuğunu ve sadece gerektiğinde uyandırdığını görelim. İlk olarak, işletim sisteminin çekirdek modu aracılığıyla bunun nasıl yapılacağına bakalım. Oradaki tüm yapılar genellikle kullanıcı alanındakilerden daha yavaştır. Örneğin birkaç kat daha yavaş AutoResetEvent belki 53 kat daha yavaş SpinLock [Richter]. Ancak onların yardımıyla, yönetilen veya yönetilmeyen tüm sistemdeki süreçleri senkronize edebilirsiniz.

Buradaki temel yapı, Dijkstra tarafından yarım yüzyıldan fazla bir süre önce önerilen semafordur. Bir semafor, basit bir ifadeyle, sistem tarafından yönetilen pozitif bir tamsayı ve üzerinde iki işlem, artırma ve eksiltmedir. Sıfıra düşemezse, çağıran iş parçacığı bloke edilir. Sayı, başka bir etkin iş parçacığı/işlem tarafından artırıldığında, iş parçacıkları atlanır ve semafor geçen sayı kadar tekrar azaltılır. Semaforlu bir darboğazda trenler hayal edilebilir. .NET, benzer işlevselliğe sahip birkaç yapı sunar: AutoResetEvent, ManualResetEvent, Mutex ve kendim Semaphore. Kullanacağız AutoResetEvent, bu, bu yapıların en basitidir: yalnızca iki değer 0 ve 1 (yanlış, doğru). Onun Metodu WaitOne() değer 0 ise çağıran iş parçacığını engeller ve 1 ise 0'a düşürür ve atlar. Bir metod Set() 1'e yükseltir ve bir garsonun geçmesine izin verir, o da tekrar 0'a indirir. Metro turnikesi gibi davranır.

Çözümü karmaşıklaştıralım ve kilidi aynı anda değil, her filozof için kullanalım. Onlar. şimdi aynı anda birkaç filozof olabilir, bir değil. Ancak, yarışlardan (yarış koşullarından) doğru bir şekilde kaçınmak, surebetleri almak için masaya erişimi yine engelliyoruz.

// Для блокирования отдельного философа.
// Инициализируется: new AutoResetEvent(true) для каждого.
private AutoResetEvent[] philosopherEvents;

// Для доступа к вилкам / доступ к столу.
private AutoResetEvent tableEvent = new AutoResetEvent(true);

// Рождение философа.
public void Run(int i, CancellationToken token)
{
    while (true)
    {
        TakeForks(i); // Ждет вилки.
        // Обед. Может быть и дольше.
        eatenFood[i] = (eatenFood[i] + 1) % (int.MaxValue - 1);
        PutForks(i); // Отдать вилки и разблокировать соседей.
        Think(i);
        if (token.IsCancellationRequested) break;
    }
}

// Ожидать вилки в блокировке.
void TakeForks(int i)
{
    bool hasForks = false;
    while (!hasForks) // Попробовать еще раз (блокировка не здесь).
    {
        // Исключающий доступ к столу, без гонок за вилками.
        tableEvent.WaitOne();
        if (forks[Left(i)] == 0 && forks[Right(i)] == 0)
            forks[Left(i)] = forks[Right(i)] = i + 1;
        hasForks = forks[Left(i)] == i + 1 && forks[Right(i)] == i + 1;
        if (hasForks)
            // Теперь философ поест, выйдет из цикла. Если Set 
            // вызван дважды, то значение true.
            philosopherEvents[i].Set();
        // Разблокировать одного ожидающего. После него значение tableEvent в false.
        tableEvent.Set(); 
        // Если имеет true, не блокируется, а если false, то будет ждать Set от соседа.
        philosopherEvents[i].WaitOne();
    }
}

// Отдать вилки и разблокировать соседей.
void PutForks(int i)
{
    tableEvent.WaitOne(); // Без гонок за вилками.
    forks[Left(i)] = 0;
    // Пробудить левого, а потом и правого соседа, либо AutoResetEvent в true.
    philosopherEvents[LeftPhilosopher(i)].Set();
    forks[Right(i)] = 0;
    philosopherEvents[RightPhilosopher(i)].Set();
    tableEvent.Set();
}

Burada neler olduğunu anlamak için, filozofun çatalları alamadığını düşünün, o zaman eylemleri aşağıdaki gibi olacaktır. Masaya erişim için bekliyor. Onu aldıktan sonra çatalları almaya çalışır. işe yaramadı Tabloya erişim sağlar (karşılıklı dışlama). Ve "turnikesini" geçer (AutoResetEvent) (başlangıçta açıktırlar). Tekrar döngüye giriyor, çünkü çatalı yok. Onları almaya çalışır ve "turnikesinde" durur. Sağdaki veya soldaki daha şanslı bir komşu, yemeğini bitirdikten sonra, "turnikesini açarak" filozofumuzun kilidini açar. Filozofumuz onu ikinci kez geçer (ve arkasından kapanır). Üçüncü kez çatalları almaya çalışır. İyi şanlar. Ve yemek yemek için turnikesinin yanından geçer.

Bu tür bir kodda rastgele hatalar olduğunda (bunlar her zaman vardır), örneğin, bir komşu yanlış belirtilir veya aynı nesne oluşturulur. AutoResetEvent hepsi için (Enumerable.Repeat), o zaman filozoflar geliştiricileri bekliyor olacak, çünkü Bu tür kodlarda hata bulmak oldukça zor bir iştir. Bu çözümle ilgili başka bir sorun da bazı filozofların açlıktan ölmeyeceğini garanti etmemesidir.

Hibrit Çözümler

Kullanıcı kipinde ve döngüde kaldığımız zaman ve çekirdek boyunca iş parçacığını engellediğimiz zaman olmak üzere iki zamanlama yaklaşımına baktık. İlk yöntem kısa kilitler için iyidir, ikincisi ise uzun olanlar için. Bir değişkenin bir döngüde değişmesi için önce kısa bir süre beklemek ve ardından bekleme uzun olduğunda iş parçacığını bloke etmek genellikle gereklidir. Bu yaklaşım sözde uygulanmaktadır. hibrit yapılar. İşte çekirdek modu ile aynı yapılar, ancak şimdi bir kullanıcı modu döngüsü ile: SemaphorSlim, ManualResetEventSlim vb. Buradaki en popüler tasarım Monitor, Çünkü C#'ta iyi bilinen bir lock sözdizimi. Monitor bu, maksimum değeri 1 (mutex) olan aynı semafordur, ancak döngüde bekleme, özyineleme, Koşul Değişkeni modeli (aşağıda daha fazlası) vb. desteği vardır. Bununla bir çözüme bakalım.

// Спрячем объект для Монитора от всех, чтобы без дедлоков.
private readonly object _lock = new object();
// Время ожидания потока.
private DateTime?[] _waitTimes = new DateTime?[philosophersAmount];

public void Run(int i, CancellationToken token)
{
    while (true)
    {
        TakeForks(i);
        eatenFood[i] = (eatenFood[i] + 1) % (int.MaxValue - 1);
        PutForks(i);
        Think(i);
        if (token.IsCancellationRequested) break;
    }
}

// Наше сложное условие для Condition Variable паттерна.
bool CanIEat(int i)
{
    // Если есть вилки:
    if (forks[Left(i)] != 0 && forks[Right(i)] != 0)
        return false;
    var now = DateTime.Now;
    // Может, если соседи не более голодные, чем текущий.
    foreach(var p in new int[] {LeftPhilosopher(i), RightPhilosopher(i)})
        if (_waitTimes[p] != null && now - _waitTimes[p] > now - _waitTimes[i])
            return false;
    return true;
}

void TakeForks(int i)
{
    // Зайти в Монитор. То же самое: lock(_lock) {..}.
    // Вызываем вне try, чтобы возможное исключение выбрасывалось выше.
    bool lockTaken = false;
    Monitor.Enter(_lock, ref lockTaken);
    try
    {
        _waitTimes[i] = DateTime.Now;
        // Condition Variable паттерн. Освобождаем лок, если не выполненно 
        // сложное условие. И ждем пока кто-нибудь сделает Pulse / PulseAll.
        while (!CanIEat(i))
            Monitor.Wait(_lock); 
        forks[Left(i)] = i + 1;
        forks[Right(i)] = i + 1;
        _waitTimes[i] = null;
    }
    finally
    {
        if (lockTaken) Monitor.Exit(_lock);
    }
}

void PutForks(int i)
{
    // То же самое: lock (_lock) {..}.
    bool lockTaken = false;
    Monitor.Enter(_lock, ref lockTaken);
    try
    {
        forks[Left(i)] = 0;
        forks[Right(i)] = 0;
        // Освободить все потоки в очереди ПОСЛЕ вызова Monitor.Exit.
        Monitor.PulseAll(_lock); 
    }
    finally
    {
        if (lockTaken) Monitor.Exit(_lock);
    }
}

Burada yine çatallara erişim için tüm masayı engelliyoruz, ancak şimdi tüm konuların engellemesini kaldırıyoruz ve biri yemeğini bitirdiğinde komşuları değil. Onlar. önce birisi yiyip komşuları bloke ediyor ve bu birisi bitirip de hemen tekrar yemek yemek istediğinde blokaj yapıp komşularını uyandırıyor çünkü. bekleme süresi daha azdır.

Kilitlenmelerden ve bir filozofun açlığından bu şekilde kaçınırız. Kısa bir bekleme için bir döngü kullanıyoruz ve ipliği uzun bir süre için bloke ediyoruz. Çözümde olduğu gibi, herkesin bir kerede engellemesini kaldırmak, yalnızca komşunun engellemesini kaldırmaktan daha yavaştır. AutoResetEvent, ancak fark büyük olmamalıdır, çünkü iş parçacıkları önce kullanıcı modunda kalmalıdır.

У lock sözdizimi kötü sürprizlere sahiptir. kullanmanızı tavsiye ederim Monitor doğrudan [Richter] [Eric Lippert]. Onlardan biri lock her zaman dışında Monitor, bir istisna olsa bile, bu durumda başka bir iş parçacığı paylaşılan bellek durumunu değiştirebilir. Bu gibi durumlarda, kilitlenmeye gitmek veya programı bir şekilde güvenli bir şekilde sonlandırmak genellikle daha iyidir. Başka bir sürpriz, Monitor'ün senkronizasyon blokları kullanmasıdır (SyncBlock), tüm nesnelerde bulunur. Bu nedenle, uygun olmayan bir nesne seçilirse, kolayca kilitlenebilirsiniz (örneğin, dahili bir diziye kilitlerseniz). Bunun için her zaman gizli olan nesneyi kullanırız.

Koşul Değişkeni modeli, bazı karmaşık koşulların beklentisini daha özlü bir şekilde uygulamanıza olanak tanır. .NET'te bence eksik, çünkü teorik olarak, bir lok üzerinde değil, birkaç değişkende (Posix Threads'de olduğu gibi) birkaç sıra olmalıdır. O zaman onları tüm filozoflar için yapabiliriz. Ancak bu formda bile kodu azaltmanıza izin verir.

Birçok filozof veya async / await

Tamam, artık ileti dizilerini etkili bir şekilde engelleyebiliriz. Peki ya çok sayıda filozofumuz varsa? 100? 10000? Örneğin, web sunucusuna 100000 istek aldık. Her istek için bir iş parçacığı oluşturmak ek yük olacaktır, çünkü pek çok iş parçacığı paralel olarak çalışmaz. Yalnızca mantıksal çekirdek sayısı kadar çalışır (4 tane var). Ve diğer herkes kaynakları elinden alacak. Bu sorunun bir çözümü, zaman uyumsuz /bekleme kalıbıdır. Buradaki fikir, bir şeyin devam etmesi için beklemesi gerekiyorsa işlevin iş parçacığını tutmamasıdır. Ve bir şey yaptığında yürütmeye devam eder (ancak aynı iş parçacığında olması gerekmez!). Bizim durumumuzda çatalı bekleyeceğiz.

SemaphoreSlim bunun için var WaitAsync() yöntem. İşte bu modeli kullanan bir uygulama.

// Запуск такой же, как раньше. Где-нибудь в программе:
Task.Run(() => Run(i, cancelTokenSource.Token));

// Запуск философа.
// Ключевое слово async -- компилятор транслирует этот метот в асинхронный.
public async Task Run(int i, CancellationToken token)
{
    while (true)
    {
        // await -- будем ожидать какого-то события.
        await TakeForks(i);
        // После await, продолжение возможно в другом потоке.
        eatenFood[i] = (eatenFood[i] + 1) % (int.MaxValue - 1);
        // Может быть несколько событий для ожидания.
        await PutForks(i);

        Think(i);

        if (token.IsCancellationRequested) break;
    }
}

async Task TakeForks(int i)
{
    bool hasForks = false;
    while (!hasForks)
    {
        // Взаимоисключающий доступ к столу:
        await _tableSemaphore.WaitAsync();
        if (forks[Left(i)] == 0 && forks[Right(i)] == 0)
        {
            forks[Left(i)] = i+1;
            forks[Right(i)] = i+1;
            hasForks = true;
        }
        _tableSemaphore.Release();
        // Будем ожидать, чтобы сосед положил вилки:
        if (!hasForks)
            await _philosopherSemaphores[i].WaitAsync();
    }
}

// Ждем доступа к столу и кладем вилки.
async Task PutForks(int i)
{
    await _tableSemaphore.WaitAsync();
    forks[Left(i)] = 0;
    // "Пробудить" соседей, если они "спали".
    _philosopherSemaphores[LeftPhilosopher(i)].Release();
    forks[Right(i)] = 0;
    _philosopherSemaphores[RightPhilosopher(i)].Release();
    _tableSemaphore.Release();
}

ile yöntem async / await hemen dahili durumunu döndüren hileli bir durum makinesine çevrilir. Task. Bu sayede, yöntemin tamamlanmasını bekleyebilir, iptal edebilir ve Task ile yapabileceğiniz diğer her şeyi yapabilirsiniz. Yöntemin içinde, durum makinesi yürütmeyi kontrol eder. Sonuç olarak, gecikme yoksa yürütme senkronizedir ve varsa iş parçacığı serbest bırakılır. Bunu daha iyi anlamak için bu durum makinesine bakmak daha iyidir. Bunlardan zincirler oluşturabilirsiniz. async / await yöntemler.

Test edelim. 100 mantıksal çekirdeğe sahip bir makinede 4 saniyede 8 filozofun çalışması. Monitor ile önceki çözüm yalnızca ilk 4 iş parçacığını çalıştırdı ve geri kalanı hiç çalışmadı. Bu 4 iş parçacığının her biri yaklaşık 2 ms boşta kaldı. Ve zaman uyumsuz / bekleme çözümü, her biri ortalama 100 saniye beklemeyle 6.8'ün tamamını çalıştırdı. Elbette gerçek sistemlerde 6 saniye boşta kalmak kabul edilemez ve bu kadar çok isteği işleme koymamak daha iyidir. Monitor ile çözümün hiç ölçeklenebilir olmadığı ortaya çıktı.

Sonuç

Bu küçük örneklerden de görebileceğiniz gibi, .NET birçok eşitleme yapısını destekler. Ancak bunların nasıl kullanılacağı her zaman açık değildir. Umarım bu makale yardımcı olmuştur. Şimdilik bu son, ancak geriye pek çok ilginç şey kaldı, örneğin, iş parçacığı güvenli koleksiyonlar, TPL Veri Akışı, Reaktif programlama, Yazılım İşlem modeli, vb.

kaynaklar

Kaynak: habr.com

Yorum ekle