Yaxşı Qidalanan Filosoflar və ya Rəqabətli .NET Proqramlaşdırması

Yaxşı Qidalanan Filosoflar və ya Rəqabətli .NET Proqramlaşdırması

Nahar filosoflarının problemindən istifadə edərək, .Net-də paralel və paralel proqramlaşdırmanın necə işlədiyinə baxaq. Plan aşağıdakı kimidir, ip/proses sinxronizasiyasından aktyor modelinə qədər (aşağıdakı hissələrdə). Məqalə ilk tanışlıq və ya biliklərinizi yeniləmək üçün faydalı ola bilər.

Niyə bunu necə edəcəyinizi bilirsiniz? Tranzistorlar minimum ölçülərinə çatır, Mur qanunu işıq sürətinin həddinə çatır və buna görə də rəqəmlərdə artım müşahidə olunur, daha çox tranzistorlar hazırlana bilər. Eyni zamanda, məlumatların həcmi artır və istifadəçilər sistemlərdən dərhal cavab gözləyirlər. Belə bir vəziyyətdə, bir icraedici ipimiz olduqda, "normal" proqramlaşdırma artıq effektiv deyil. Biz eyni vaxtda və ya paralel icra problemini birtəhər həll etməliyik. Üstəlik, bu problem müxtəlif səviyyələrdə mövcuddur: ip səviyyəsində, proses səviyyəsində, şəbəkədəki maşınlar səviyyəsində (paylanmış sistemlər). .NET bu cür problemləri tez və effektiv həll etmək üçün yüksək keyfiyyətli, vaxtla sınaqdan keçirilmiş texnologiyalara malikdir.

Tapşırıq

Edsger Dijkstra bu problemi hələ 1965-ci ildə tələbələrinə soruşmuşdu. Təsbit edilmiş formula aşağıdakı kimidir. Müəyyən sayda (adətən beş) filosof və eyni sayda çəngəl var. Dəyirmi masa arxasında otururlar, aralarında çəngəllər. Filosoflar boşqablarından sonsuz yemək yeyə, düşünə və ya gözləyə bilərlər. Yemək üçün bir filosof iki çəngəl götürməlidir (ikincisi birincisi ilə bir çəngəl paylaşır). Çəngəl götürmək və yerə qoymaq iki ayrı hərəkətdir. Bütün filosoflar susur. Vəzifə elə bir alqoritm tapmaqdır ki, onlar 54 ildən sonra belə düşünsünlər və yaxşı qidalansınlar.

Əvvəlcə paylaşılan məkandan istifadə edərək bu problemi həll etməyə çalışaq. Çəngəllər ümumi masanın üstündə uzanır və filosoflar orada olanda onları götürüb geri qoyurlar. Sinxronizasiya ilə bağlı problemlər burada yaranır, çəngəlləri nə vaxt götürmək lazımdır? fiş yoxdursa nə etməli? s. Amma əvvəlcə filosoflardan başlayaq.

Mövzulara başlamaq üçün biz vasitəsilə iplik hovuzundan istifadə edirik Task.Run üsul:

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);
}

İp hovuzu iplərin yaradılması və çıxarılmasını optimallaşdırmaq üçün nəzərdə tutulmuşdur. Bu hovuzda tapşırıqlar növbəsi var və CLR bu tapşırıqların sayından asılı olaraq mövzular yaradır və ya silir. Bütün AppDomains üçün bir hovuz. Bu hovuz demək olar ki, həmişə istifadə edilməlidir, çünki... mövzuları, onların növbələrini və s. yaratmaq və silməklə narahat olmağa ehtiyac yoxdur. Bunu hovuz olmadan da edə bilərsiniz, lakin bundan sonra birbaşa istifadə etməli olacaqsınız. Thread, bu, ipin prioritetini dəyişdirməli olduğumuz hallarda, uzun bir əməliyyatımız olduqda, Ön planda iplik üçün və s.

Başqa sözlə, System.Threading.Tasks.Task sinif eynidir Thread, lakin hər cür rahatlıqla: digər tapşırıqlar blokundan sonra tapşırığı işə salmaq, onları funksiyalardan geri qaytarmaq, rahat şəkildə kəsmək və s. Onlar asinxron/gözləmə konstruksiyalarını dəstəkləmək üçün lazımdır (Tapşırıq əsaslı Asinxron Nümunə, IO əməliyyatlarını gözləmək üçün sintaktik şəkər). Bu barədə sonra danışacağıq.

CancelationTokenSource burada ipin çağıran ipdən gələn siqnalla özünü dayandıra bilməsi lazımdır.

Sinxronizasiya Problemləri

Bloklanmış filosoflar

Yaxşı, biz iplər yaratmağı bilirik, gəlin nahar etməyə cəhd edək:

// Кто какие вилки взял. К примеру: 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 əvvəlcə sol, sonra sağ çəngəl götürməyə çalışırıq, əgər işləyirsə, yeyib geri qoyuruq. Bir çəngəl götürmək atomikdir, yəni. iki ip eyni anda birini götürə bilməz (səhv: birincisi çəngənin boş olduğunu oxuyur, ikincisi eyni şeyi edir, birinci alır, ikinci alır). Bunun üçün Interlocked.CompareExchangeprosessor təlimatından istifadə etməklə həyata keçirilməlidir (TSL, XCHG), atom ardıcıl oxumaq və yazmaq üçün yaddaş parçasını kilidləyir. Və SpinWait tikintiyə bərabərdir while(true) yalnız bir az "sehrli" ilə - ip prosessoru tutur (Thread.SpinWait), lakin bəzən nəzarəti başqa mövzuya keçir (Thread.Yeild) və ya yuxuya gedir (Thread.Sleep).

Amma bu həll işləmir, çünki... iplər tezliklə (mənim üçün bir saniyə ərzində) bağlanır: bütün filosoflar sol çəngəlini götürürlər, amma sağı yoxdur. Sonra çəngəllər massivi aşağıdakı dəyərlərə malikdir: 1 2 3 4 5.

Yaxşı Qidalanan Filosoflar və ya Rəqabətli .NET Proqramlaşdırması

Şəkildə iplərin bloklanması (çıxmaz). Yaşıl icranı, qırmızı sinxronizasiyanı, boz isə ipin yatdığını göstərir. Brilyantlar Tapşırıqların başlama vaxtını göstərir.

Filosofların aclığı

Düşünmək üçün çox yemək lazım olmasa da, aclıq hər kəsi fəlsəfədən əl çəkməyə məcbur edə bilər. Problemimizdə iplik aclığı vəziyyətini simulyasiya etməyə çalışaq. Aclıq, bir ip işlədiyi zaman, lakin əhəmiyyətli bir iş olmadan, başqa sözlə, eyni çıxılmaz vəziyyətdir, yalnız indi ip yatmır, ancaq aktiv olaraq yemək üçün bir şey axtarır, amma yemək yoxdur. Tez-tez tıxanmamaq üçün başqasını ala bilməsək, çəngəli geri qoyacağıq.

// То же что и в 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 bağlı vacib olan odur ki, hər dörd filosofdan ikisi sol çəngəli yerə qoymağı unudur. Və belə çıxır ki, onlar daha çox yemək yeyirlər və digərləri ac qalmağa başlayır, baxmayaraq ki, iplər eyni prioritetə ​​malikdir. Burada onlar tamamilə ac qalmırlar, çünki... pis filosoflar bəzən çəngəllərini geri qoyurlar. Belə çıxır ki, yaxşılar pislərdən təxminən 5 dəfə az yeyirlər. Beləliklə, koddakı kiçik bir səhv performansın azalmasına səbəb olur. Burada onu da qeyd etmək yerinə düşər ki, bütün filosoflar sol çəngəli götürəndə nadir bir vəziyyət mümkündür, sağı yoxdur, solunu yerə qoyurlar, gözləyin, solu yenidən götürürlər və s. Bu vəziyyət də aclıq, daha çox qarşılıqlı tıxanma kimidir. Mən bunu təkrarlaya bilmədim. Aşağıda iki pis filosofun hər iki çəngəli götürdüyü, iki yaxşı filosofun isə ac qaldığı bir vəziyyət üçün bir şəkil var.

Yaxşı Qidalanan Filosoflar və ya Rəqabətli .NET Proqramlaşdırması

Burada mövzuların bəzən oyandığını və resurs əldə etməyə çalışdığını görə bilərsiniz. Dörd nüvədən ikisi heç nə etmir (yuxarıdakı yaşıl qrafik).

Filosofun ölümü

Bəli, filosofların şanlı şam yeməyini kəsə biləcək daha bir problem, onlardan birinin qəfildən əlində çəngəl ilə ölməsidir (və o, belə dəfn olunacaq). Sonra qonşular naharsız qalacaqlar. Bu iş üçün özünüz nümunə kodu tapa bilərsiniz, məsələn, atılır NullReferenceException filosof çəngəlləri götürdükdən sonra. Yeri gəlmişkən, istisna işlənməyəcək və zəng kodu onu sadəcə tutmayacaq (bunun üçün AppDomain.CurrentDomain.UnhandledException və s.). Buna görə də, xətlərin özlərində və zərif xitamla səhv idarəçilərinə ehtiyac var.

Garson

Yaxşı, bu çıxılmaz vəziyyət, aclıq və ölüm problemini necə həll edək? Çəngəllərə yalnız bir filosof icazə verəcəyik və bu yer üçün iplərin qarşılıqlı istisnasını əlavə edəcəyik. Bunu necə etmək olar? Tutaq ki, filosofların yanında bir filosofa çəngəl götürməyə icazə verən bir ofisiant var. Bu ofisiantı necə etməliyik və filosofların ona necə sual verəcəyi maraqlı suallardır.

Ən sadə yol, filosofların sadəcə olaraq ofisiantdan çəngəllərə giriş istəmələridir. Bunlar. İndi filosoflar yaxınlıqda çəngəl gözləməyəcək, ya gözləyin, ya da ofisiantdan soruşun. Əvvəlcə bunun üçün yalnız İstifadəçi Məkanından istifadə edirik, orada nüvədən hər hansı prosedurları çağırmaq üçün kəsilmələrdən istifadə etmirik (aşağıda daha ətraflı).

İstifadəçi məkanı həlləri

Burada bir çəngəl və iki filosofla əvvəl etdiyimizin eynisini edəcəyik, bir döngədə fırlanıb gözləyəcəyik. Ancaq indi bütün filosoflar və olduğu kimi, yalnız bir çəngəl olacaq, yəni. deyə bilərik ki, ancaq bu “qızıl çəngəl”i ofisiantdan alan filosof yeyəcək. Bunun üçün SpinLock istifadə edirik.

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 blokerdir, kobud desək, eynidir while(true) { if (!lock) break; }, lakin daha çox "sehrli" ilə SpinWait (orada istifadə olunur). İndi o, gözləyənləri necə saymağı, bir az yatdırmağı və daha çox şey bilir. və s. Ümumiyyətlə, optimallaşdırmaq üçün mümkün olan hər şeyi edir. Ancaq yadda saxlamalıyıq ki, bu, hələ də prosessor resurslarını yeyən və ip saxlayan eyni aktiv döngədir, əgər filosoflardan biri digərlərindən daha çox prioritet olarsa, lakin qızıl çəngəl yoxdursa aclığa səbəb ola bilər (Prioritetin İnversiya problemi). ). Buna görə də, biz onu üçüncü tərəf zəngləri, iç-içə kilidlər və ya digər sürprizlər olmadan yalnız paylaşılan yaddaşda çox qısa dəyişikliklər üçün istifadə edirik.

Yaxşı Qidalanan Filosoflar və ya Rəqabətli .NET Proqramlaşdırması

Üçün rəsm SpinLock. Axınlar daim qızıl çəngəl üçün “mübarizə aparır”. Uğursuzluqlar baş verir - şəkildəki vurğulanan sahə. Nüvələr tam istifadə edilmir: bu dörd iplə yalnız təxminən 2/3.

Burada başqa bir həll yalnız istifadə etmək olardı Interlocked.CompareExchange yuxarıdakı kodda göstərildiyi kimi eyni aktiv gözləmə ilə (aclıqdan ölən filosoflarda), lakin bu, artıq deyildiyi kimi, nəzəri olaraq blokadaya səbəb ola bilər.

haqqında Interlocked təkcə olmadığını söyləməyə dəyər CompareExchange, həm də atom oxumaq və yazmaq üçün digər üsullar. Dəyişikliyi təkrarlayaraq, əgər başqa bir ip öz dəyişikliklərini etməyi bacarırsa (1-i oxuyun, 2-ni oxuyun, 2-ni yazın, 1-i yazın pisdir), ondan bir dəyərə kompleks dəyişikliklər üçün istifadə edilə bilər (Interlocked Anything model).

Kernel rejimi həlləri

Bir döngədə resursları israf etməmək üçün mövzunu necə bloklayacağımıza baxaq. Yəni misalımıza davam edərək, görək ofisiant filosofu necə yatdırır və yalnız lazım olanda onu oyadır. Əvvəlcə əməliyyat sisteminin nüvə rejimi vasitəsilə bunu necə edəcəyinə baxaq. Oradakı bütün strukturlar çox vaxt istifadəçi məkanında olanlardan daha yavaş olur. Məsələn, bir neçə dəfə yavaşlayın AutoResetEvent bəlkə 53 dəfə yavaş SpinLock [Rixter]. Lakin onların köməyi ilə siz idarə olunan və ya olmayan bütün sistem üzrə prosesləri sinxronizasiya edə bilərsiniz.

Burada əsas dizayn yarım əsrdən çox əvvəl Dijkstra tərəfindən təklif olunan semafordur. Semafor, sadə dildə desək, sistem tərəfindən idarə olunan müsbət tam ədəddir və onun üzərində iki əməliyyat - artım və azalma. Sıfırı azaltmaq mümkün deyilsə, zəng edən ip bloklanır. Nömrə bəzi digər aktiv ip/proseslə artırıldıqda, iplər ötürülür və semafor yenidən keçən saya görə azalır. Qatarları semaforla darboğazda təsəvvür edə bilərsiniz. .NET oxşar funksionallığı olan bir neçə konstruksiya təklif edir: AutoResetEvent, ManualResetEvent, Mutex və özüm Semaphore. istifadə edəcəyik AutoResetEvent, bu konstruksiyaların ən sadəsidir: yalnız iki dəyər 0 və 1 (yanlış, doğru). Onun metodu WaitOne() dəyər 0 olarsa, zəng edən mövzunu bloklayır və 1 olarsa, onu 0-a endirir və onu atlayır. Bir üsul Set() 1-ə yüksəlir və bir nəfəri buraxır, o da yenidən 0-a enir. Metroda turniket kimi fəaliyyət göstərir.

Gəlin həlli çətinləşdirək və blokdan hər bir filosof üçün istifadə edək, birdən-birə yox. Bunlar. İndi bir deyil, bir neçə filosof eyni anda yeyə bilər. Ancaq yarış şəraitindən qaçaraq çəngəlləri düzgün götürmək üçün yenidən masaya girişi bloklayırıq.

// Для блокирования отдельного философа.
// Инициализируется: 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 nə baş verdiyini başa düşmək üçün filosofun çəngəlləri götürə bilməməsi halına nəzər salın, onda onun hərəkətləri aşağıdakı kimi olacaq. Masaya girməyi gözləyir. Onu aldıqdan sonra çəngəlləri götürməyə çalışır. alınmadı. Cədvəldən çıxış imkanı verir (qarşılıqlı istisna). Və o, "turniketini" keçir (AutoResetEvent) (əvvəlcə onlar açıqdır). Yenidən dövrəyə düşür, çünki onun çəngəlləri yoxdur. Onları götürməyə çalışır və öz “turniketində” dayanır. Sağda və ya solda olan bəzi şanslı qonşu yemək yedikdən sonra “turniketini açaraq” filosofumuzun qarşısını açacaq. Bizim filosof ikinci dəfə bunun içindən keçir (və onun arxasınca bağlanır). Üçüncü dəfə çəngəlləri götürməyə çalışır. Uğurlu. Və nahar etmək üçün turniketindən keçir.

Belə kodda təsadüfi səhvlər olduqda (onlar həmişə mövcuddur), məsələn, qonşu səhv göstəriləcək və ya eyni obyekt yaradılacaq. AutoResetEvent hamı üçün (Enumerable.Repeat), onda filosoflar tərtibatçıları gözləyəcəklər, çünki Bu cür kodda səhvləri tapmaq olduqca çətin işdir. Bu həllin başqa bir problemi odur ki, o, bəzi filosofların aclıqdan ölməyəcəyinə zəmanət vermir.

Hibrid həllər

Biz sinxronizasiyaya iki yanaşmaya baxdıq, istifadəçi rejimində qaldıqda və bir döngədə fırlananda və nüvədən ipi bloklayanda. Birinci üsul qısa bloklar üçün yaxşıdır, ikincisi uzun olanlar üçün. Tez-tez əvvəlcə dəyişənin bir döngədə dəyişməsini qısa müddətə gözləmək lazımdır, sonra gözləmə uzun olduqda mövzunu bloklamaq lazımdır. Bu yanaşma sözdə həyata keçirilir. hibrid dizaynlar. O, kernel rejimi ilə eyni konstruksiyalara malikdir, lakin indi istifadəçi rejimi döngəsi ilə: SemaphorSlim, ManualResetEventSlim və s. Burada ən məşhur dizayn Monitor, çünki C#-da məşhur bir var lock sintaksis. Monitor bu, maksimum dəyəri 1 (mutex) olan eyni semafordur, lakin dövrədə gözləmə dəstəyi, rekursiya, Şərt Dəyişən nümunəsi (aşağıda daha ətraflı) və s. Gəlin onunla həll yoluna baxaq.

// Спрячем объект для Монитора от всех, чтобы без дедлоков.
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 biz yenidən bütün masanın çəngəllərə daxil olmasını əngəlləyirik, amma indi kimsə yemək yeyəndə qonşuları deyil, bütün mövzuları bir anda açırıq. Bunlar. birincisi, kimsə yeyib qonşuları bloklayır və bu kimsə bitirib, amma dərhal yemək istəyəndə bloka girib qonşularını oyadır, çünki onun gözləmə müddəti azdır.

Bu yolla biz çıxılmaz vəziyyətlərdən və filosofun aclığından qaçırıq. Qısa müddət gözləmək və ipi uzun müddət bloklamaq üçün bir döngə istifadə edirik. Hamını bir anda blokdan çıxarmaq, həlldə olduğu kimi, qonşunun blokdan çıxarılmasından daha yavaşdır AutoResetEvent, lakin fərq böyük olmamalıdır, çünki mövzular əvvəlcə istifadəçi rejimində qalmalıdır.

У lock sintaksis bəzi xoşagəlməz sürprizlərə malikdir. İstifadə etmək tövsiyə olunur Monitor birbaşa [Rixter] [Erik Lippert]. Onlardan biri də budur lock həmişə çıxır Monitor, istisna olsa belə, və sonra başqa bir mövzu paylaşılan yaddaşın vəziyyətini dəyişə bilər. Belə hallarda, çox vaxt dalana girmək və ya proqramı birtəhər təhlükəsiz şəkildə dayandırmaq daha yaxşıdır. Başqa bir sürpriz, Monitorun saat bloklarından istifadə etməsidir (SyncBlock), bütün obyektlərdə mövcuddur. Buna görə də, uyğun olmayan bir obyekt seçilərsə, asanlıqla çıxılmaz vəziyyətə düşə bilərsiniz (məsələn, interned simli kilidləsəniz). Bunun üçün həmişə gizli obyektdən istifadə edirik.

Vəziyyət Dəyişən nümunəsi bəzi mürəkkəb vəziyyətin gözləntilərini daha qısa şəkildə həyata keçirməyə imkan verir. .NET-də mənim fikrimcə natamamdır, çünki... Teorik olaraq, bir kiliddə deyil, bir neçə dəyişəndə ​​(Posix Threads-də olduğu kimi) bir neçə növbə olmalıdır. Onda onları bütün filosoflar üçün etmək olardı. Ancaq hətta bu formada kodu qısaltmağa imkan verir.

Bir çox filosoflar və ya async / await

Yaxşı, indi mövzuları effektiv şəkildə bloklaya bilərik. Bəs bizdə filosoflar çoxdursa necə? 100? 10000? Məsələn, veb serverə 100000 sorğu aldıq. Hər sorğu üçün mövzu yaratmaq bahalı olacaq, çünki o qədər çox mövzu paralel olaraq icra olunmayacaq. Yalnız bir çox məntiqi nüvə yerinə yetiriləcək (məndə 4 var). Qalan hər kəs sadəcə resursları əlindən alacaq. Bu problemin həlli yollarından biri async/wait modelidir. Onun ideyası ondan ibarətdir ki, funksiya nəyinsə davam etməsini gözləmək lazımdırsa, ipi saxlamır. Və bir şey baş verəndə, icrasını davam etdirir (lakin eyni mövzuda deyil!). Bizim vəziyyətimizdə bir çəngəl gözləyəcəyik.

SemaphoreSlim bunun üçün var WaitAsync() üsul. Budur, bu nümunədən istifadə edən bir tətbiq.

// Запуск такой же, как раньше. Где-нибудь в программе:
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();
}

ilə üsul async / await hiyləgər sonlu vəziyyət maşınına çevrilir və dərhal daxilini qaytarır Task. Onun vasitəsilə siz metodun tamamlanmasını gözləyə, onu ləğv edə və Tapşırıq ilə edə biləcəyiniz hər şeyi edə bilərsiniz. Metodun içərisində dövlət maşını icraya nəzarət edir. Nəticə budur ki, gecikmə yoxdursa, icra sinxrondur, əgər varsa, ip buraxılır. Bunu daha yaxşı başa düşmək üçün bu dövlət maşınına baxmaq daha yaxşıdır. Bunlardan zəncirlər yarada bilərsiniz async / await üsulları.

Test edək. 100 filosofun 4 məntiqi nüvəli maşında işi, 8 saniyə. Monitor ilə əvvəlki həll yalnız ilk 4 mövzunu icra etdi, qalanlarını isə ümumiyyətlə icra etmədi. Bu 4 başlığın hər biri təxminən 2 ms boş qaldı. Async/await həlli isə hər gözləməyə orta hesabla 100 saniyə olmaqla 6.8-ün hamısını etdi. Təbii ki, real sistemlərdə 6 saniyə boş qalmaq yolverilməzdir və bu qədər sorğunu bu şəkildə emal etməmək daha yaxşıdır. Monitor ilə həll ümumiyyətlə miqyaslana bilməz.

Nəticə

Bu kiçik nümunələrdən göründüyü kimi, .NET bir çox sinxronizasiya konstruksiyalarını dəstəkləyir. Ancaq onlardan necə istifadə ediləcəyi həmişə aydın deyil. Ümid edirəm bu məqalə faydalı oldu. Biz bunu hələlik yekunlaşdırırıq, lakin hələ də çox maraqlı şeylər qalıb, məsələn, iplə təhlükəsiz kolleksiyalar, TPL Dataflow, Reaktiv proqramlaşdırma, Proqram Əməliyyatı modeli və s.

İnformasiya qaynaqları

Mənbə: www.habr.com

Добавить комментарий