Filsuf uga-panganan utawa program competitive ing .NET

Filsuf uga-panganan utawa program competitive ing .NET

Ayo kang katon ing carane program bebarengan lan podo dianggo ing .Net, nggunakake conto masalah filsuf lunching. Rencana kasebut kaya ing ngisor iki, saka sinkronisasi benang / proses menyang model aktor (ing bagean ing ngisor iki). Artikel kasebut bisa migunani kanggo kenalan pisanan utawa kanggo nyegerake kawruh.

Apa malah ngerti carane nindakake iki? Transistor wis tekan ukuran minimal, hukum Moore wis tekan wates kacepetan cahya, lan mulane wutah diamati ing nomer; transistor liyane bisa digawe. Ing wektu sing padha, jumlah data saya akeh, lan pangguna ngarepake respon langsung saka sistem. Ing kahanan kaya mengkono, pemrograman "normal", nalika kita duwe thread eksekusi, ora efektif maneh. Kita kudu ngatasi masalah eksekusi simultan utawa bebarengan. Menapa malih, masalah iki ana ing tingkat sing beda-beda: ing tingkat thread, ing tingkat proses, ing tingkat mesin ing jaringan (sistem sing disebarake). .NET nduweni kualitas dhuwur, teknologi sing diuji wektu kanggo ngrampungake masalah kasebut kanthi cepet lan efisien.

Tujuan

Edsger Dijkstra takon masalah iki marang murid-muride nalika taun 1965. Rumusan kang wis ditetepake kaya ing ngisor iki. Ana sawetara filsuf tartamtu (biasane lima) lan jumlah garpu sing padha. Padha njagong ing meja bunder, garpu ing antarane. Para filsuf bisa mangan saka piring panganan tanpa telas, mikir utawa ngenteni. Kanggo mangan, filsuf kudu njupuk rong garpu (sing terakhir nuduhake garpu karo mantan). Njupuk lan nyelehake garpu minangka rong tumindak sing kapisah. Kabeh filsuf meneng. Tugase yaiku nemokake algoritma kasebut supaya kabeh padha mikir lan kepenak sanajan sawise 54 taun.

Pisanan, ayo nyoba ngatasi masalah iki kanthi nggunakake papan sing dienggo bareng. Garpu kasebut ana ing meja umum lan para filsuf mung njupuk nalika ana lan dilebokake maneh. Iki ngendi masalah sinkronisasi muncul, nalika persis kanggo njupuk garpu? apa sing kudu ditindakake yen ora ana plug? lsp Nanging pisanan, ayo miwiti karo para filsuf.

Kanggo miwiti Utas kita nggunakake blumbang Utas liwat Task.Run cara:

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

Kolam benang dirancang kanggo ngoptimalake nggawe lan mbusak benang. Kolam iki duwe antrian tugas lan CLR nggawe utawa mbusak benang gumantung saka jumlah tugas kasebut. Siji blumbang kanggo kabeh AppDomains. Kolam iki kudu digunakake meh tansah, amarga ... ora perlu repot nggawe lan mbusak utas, antrian, lsp. Sampeyan bisa nindakake tanpa blumbang, nanging sampeyan kudu nggunakake langsung. Thread, iki migunani kanggo kasus nalika kita kudu ngganti prioritas thread, nalika kita duwe operasi dawa, kanggo thread Foreground, etc.

Ing tembung liyane, System.Threading.Tasks.Task kelas padha Thread, nanging kanthi macem-macem kepenak: kemampuan kanggo mbukak tugas sawise blok tugas liyane, bali saka fungsi, gampang ngganggu, lan liya-liyane. etc.. Padha dibutuhake kanggo ndhukung konstruksi async / ngenteni (Pola Asynchronous basis Tugas, gula sintaksis kanggo nunggu operasi IO). Kita bakal ngomong babagan iki mengko.

CancelationTokenSource kene iku perlu sing Utas bisa siksa dhewe marang sinyal saka Utas nelpon.

Masalah Sinkronisasi

Filsuf sing diblokir

Oke, kita ngerti carane nggawe utas, ayo nyoba nedha awan:

// ΠšΡ‚ΠΎ ΠΊΠ°ΠΊΠΈΠ΅ Π²ΠΈΠ»ΠΊΠΈ взял. К ΠΏΡ€ΠΈΠΌΠ΅Ρ€Ρƒ: 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();
    }
}

Ing kene kita nyoba njupuk sisih kiwa lan banjur garpu tengen, lan yen bisa, kita mangan lan sijine maneh. Njupuk garpu siji iku atom, i.e. loro Utas ora bisa njupuk siji ing wektu sing padha (salah: pisanan maca sing garpu free, kaloro nindakake padha, pisanan njupuk, kaloro njupuk). Kanggo iki Interlocked.CompareExchange, sing kudu dileksanakake nggunakake instruksi prosesor (TSL, XCHG), sing ngunci sepotong memori kanggo maca lan nulis urutan atom. Lan SpinWait padha karo construction while(true) mung karo "sihir" sethitik - thread njupuk prosesor (Thread.SpinWait), nanging kadhangkala liwat kontrol kanggo thread liyane (Thread.Yeild) utawa turu (Thread.Sleep).

Nanging solusi iki ora bisa digunakake, amarga ... Utas rauh (sajrone sekedhik kanggo kula) diblokir: kabeh filsuf njupuk garpu kiwa, nanging ora ana siji tengen. Array forks banjur nduweni nilai: 1 2 3 4 5.

Filsuf uga-panganan utawa program competitive ing .NET

Ing gambar, mblokir thread (deadlock). Ijo nuduhake eksekusi, abang nuduhake sinkronisasi, lan werna abu-abu nuduhake benang turu. Diamonds nuduhake wektu Bukak saka Tugas.

Keluwen para Filsuf

Senajan sampeyan ora perlu akeh pangan kanggo mikir, keluwen bisa meksa sapa kanggo nyerah filsafat. Ayo coba simulasi kahanan keluwen benang ing masalah kita. Keluwen yaiku nalika benang bisa, nanging tanpa karya sing signifikan, kanthi tembung liya, iku buntu sing padha, mung saiki benang ora turu, nanging aktif golek panganan, nanging ora ana panganan. Supaya ora kerep diblokir, kita bakal nyelehake garpu maneh yen ora bisa njupuk liyane.

// Π’ΠΎ ΠΆΠ΅ Ρ‡Ρ‚ΠΎ ΠΈ Π² 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)
    );
}

Sing penting babagan kode iki yaiku loro saka papat filsuf lali nyelehake garpu kiwa. Lan ternyata mangan luwih akeh, lan wong liya wiwit keluwen, sanajan benang duwe prioritas sing padha. Ing kene dheweke ora keluwen babar pisan, amarga ... filsuf ala kadhangkala sijine maneh garpu. Pranyata sing apik mangan kurang 5 kaping saka sing ala. Dadi kesalahan cilik ing kode nyebabake penurunan kinerja. Ing kene uga perlu dicathet menawa kahanan sing langka bisa kedadeyan nalika kabeh filsuf njupuk garpu kiwa, ora ana sing tengen, sijine sing kiwa mudhun, ngenteni, njupuk sing kiwa maneh, lsp. Kahanan iki uga keluwen, luwih kaya sumbatan bebarengan. Aku ora bisa mbaleni. Ing ngisor iki ana gambar kanggo kahanan ing ngendi loro filsuf ala wis njupuk loro garpu, lan loro apik keluwen.

Filsuf uga-panganan utawa program competitive ing .NET

Ing kene sampeyan bisa ndeleng manawa benang kadang tangi lan nyoba golek sumber. Loro saka papat intine ora nindakake apa-apa (grafik ijo ing ndhuwur).

Pati Filsuf

Nah, masalah liyane sing bisa ngganggu nedha bengi para filsuf sing mulya yaiku yen salah sijine tiba-tiba mati kanthi garpu ing tangane (lan dheweke bakal dikubur kaya ngono). Banjur tangga-tanggane bakal ditinggal tanpa nedha awan. Sampeyan bisa teka munggah karo conto kode kanggo kasus iki dhewe, contone dibuwang adoh NullReferenceException sawise filsuf njupuk garpu. Lan, kanthi cara iki, pangecualian ora bakal ditangani lan kode panggilan ora mung bisa nyekel (kanggo iki AppDomain.CurrentDomain.UnhandledException lan lsp). Mulane, panangan kesalahan dibutuhake ing benang dhewe lan kanthi mandap anggun.

Pelayan

Oke, kepiye carane ngatasi masalah buntu, keluwen, lan pati? Kita bakal ngidini mung siji filsuf menyang garpu, lan kita bakal nambah bebarengan exclusion saka Utas kanggo panggonan iki. Carane nindakake? Upamane ing jejere para filsuf ana pelayan sing menehi ijin marang salah sawijining filsuf kanggo njupuk garpu. Kepiye carane nggawe pelayan iki lan kepiye para filsuf bakal takon dheweke minangka pitakonan sing menarik.

Cara sing paling gampang yaiku para filsuf mung terus-terusan takon marang pelayan kanggo ngakses garpu. Sing. Saiki filsuf ora bakal ngenteni garpu ing cedhak, nanging ngenteni utawa takon pelayan. Ing wiwitan, kita mung nggunakake Ruang Pangguna kanggo iki; ing kono kita ora nggunakake interupsi kanggo nelpon prosedur apa wae saka kernel (liyane ing ngisor iki).

Solusi ruang pangguna

Kene kita bakal nindakake bab sing padha sadurunge karo siji garpu lan loro filsuf, kita bakal muter ing daur ulang lan ngenteni. Nanging saiki bakal kabeh filsuf lan, kaya, mung siji garpu, i.e. kita bisa ngomong sing mung filsuf sing njupuk iki "garpu emas" saka waiter bakal mangan. Kanggo nindakake iki kita nggunakake SpinLock.

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 iki blocker, karo, kira-kira ngandika, padha while(true) { if (!lock) break; }, nanging karo malah luwih "sihir" saka ing SpinWait (sing digunakake ing kono). Saiki dheweke ngerti carane ngetung sing nunggu, turu sethithik, lan liya-liyane. etc Umum, iku kabeh bisa kanggo ngoptimalake. Nanging kita kudu ngelingi sing iki isih daur ulang aktif padha mangan sumber daya prosesor lan terus thread, kang bisa mimpin kanggo keluwen yen salah siji saka filsuf dadi prioritas luwih saka liyane, nanging ora duwe garpu emas (Priority Inversion problem). ). Mulane, kita nggunakake mung kanggo owah-owahan cendhak banget ing memori sambungan, tanpa telpon pihak katelu, kunci nested, utawa surprises liyane.

Filsuf uga-panganan utawa program competitive ing .NET

Nggambar kanggo SpinLock. Aliran terus-terusan "perang" kanggo garpu emas. Gagal kedadeyan - wilayah sing disorot ing gambar kasebut. Intine ora digunakake kanthi lengkap: mung udakara 2/3 saka papat benang kasebut.

Solusi liyane ing kene yaiku mung nggunakake Interlocked.CompareExchange karo Enteni aktif padha minangka ditampilake ing kode ndhuwur (ing filsuf kaliren), nanging iki, minangka wis ngandika, teori bisa mimpin kanggo mblokir.

ing Interlocked iku worth ngomong sing ana ora mung CompareExchange, nanging uga cara liya kanggo maca lan nulis atom. Lan kanthi mbaleni owah-owahan kasebut, yen thread liyane bisa nggawe owah-owahan (maca 1, maca 2, nulis 2, nulis 1 ala), bisa digunakake kanggo owah-owahan kompleks kanggo siji nilai (pola Interlocked Anything).

Solusi mode kernel

Supaya ora mbuang sumber daya ing loop, ayo goleki carane mblokir thread. Ing tembung liyane, terus conto kita, ayo kang ndeleng carane waiter nempatno filsuf turu lan mung tangi yen perlu. Pisanan, ayo goleki carane nindakake iki liwat mode kernel sistem operasi. Kabeh struktur ing kono asring dadi luwih alon tinimbang ing ruang pangguna. Alon kaping pirang-pirang, contone AutoResetEvent bisa 53 kaping luwih alon SpinLock [Richter]. Nanging kanthi bantuan, sampeyan bisa nyinkronake proses ing kabeh sistem, dikelola utawa ora.

Desain dhasar ing kene yaiku semafor, sing diusulake dening Dijkstra luwih saka setengah abad kepungkur. A semaphore, mung sijine, integer positif kontrol dening sistem, lan loro operasi ing - nambah lan ngurangi. Yen ora bisa nyuda nol, benang panggilan diblokir. Nalika nomer tambah dening sawetara Utas aktif / proses liyane, banjur Utas liwati lan semafore maneh sudo dening nomer liwati. Sampeyan bisa mbayangno sepur ing bottleneck karo semafor. .NET nawakake sawetara konstruksi kanthi fungsi sing padha: AutoResetEvent, ManualResetEvent, Mutex lan aku Semaphore. Kita bakal nggunakake AutoResetEvent, iki minangka konstruksi sing paling gampang: mung rong nilai 0 lan 1 (palsu, bener). Metode dheweke WaitOne() mblokir thread nelpon yen nilai 0, lan yen 1, banjur demotes menyang 0 lan skip. A metode Set() mundhak kanggo 1 lan ngijini siji wong liwat, sing maneh sudo kanggo 0. Tumindak kaya turnstile ing subway.

Ayo dadi rumit solusi lan nggunakake pamblokiran kanggo saben filsuf, lan ora bebarengan. Sing. Saiki sawetara filsuf bisa mangan bebarengan, lan ora mung siji. Nanging maneh mblokir akses menyang meja supaya bisa njupuk garpu kanthi bener, ngindhari kahanan balapan.

// Для блокирования ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ философа.
// Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠ΅Ρ‚ΡΡ: 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();
}

Kanggo mangerteni apa sing kedadeyan ing kene, coba dipikirake yen filsuf gagal njupuk garpu, banjur tumindake kaya ing ngisor iki. Dheweke ngenteni akses menyang meja. Sawise nampa, dheweke nyoba njupuk garpu. Ora bisa metu. Iku menehi adoh akses menyang meja (saling pengecualian). Lan dheweke ngliwati "turnstile" (AutoResetEvent) (ing kawitan padha mbukak). Iku tumiba ing siklus maneh, amarga dheweke ora duwe garpu. Dheweke nyoba njupuk lan mandheg ing "turnstile" dheweke. Sawetara pepadhamu sing luwih begja ing sisih tengen utawa kiwa, sawise rampung mangan, bakal mbukak blokir filsuf kita kanthi "mbukak turnstile." Filsuf kita ngliwati (lan nutup ing mburine) kaping pindho. Nyoba kaping telune kanggo njupuk garpu. kasil. Lan dheweke ngliwati turnstile kanggo nedha awan.

Yen ana kesalahan acak ing kode kasebut (padha tansah ana), contone, pepadhamu bakal salah kasebut utawa obyek sing padha bakal digawe AutoResetEvent kanggo kabeh (Enumerable.Repeat), banjur para filsuf bakal ngenteni pangembang, amarga Nemokake kesalahan ing kode kasebut minangka tugas sing angel. Masalah liyane karo solusi iki yaiku ora njamin yen sawetara filsuf ora bakal keluwen.

Solusi hibrida

Kita nyawang loro pendekatan kanggo sinkronisasi, nalika kita tetep ing mode pangguna lan muter ing daur ulang lan nalika kita mblokir thread liwat kernel. Cara pisanan apik kanggo blok sing cendhak, sing kapindho kanggo sing dawa. Asring sampeyan kudu ngenteni sedhela kanggo owah-owahan variabel ing daur ulang, banjur mblokir thread nalika ngenteni dawa. Pendekatan iki dileksanakake ing supaya disebut-. desain hibrida. Nduwe konstruksi sing padha karo mode kernel, nanging saiki nganggo loop mode pangguna: SemaphorSlim, ManualResetEventSlim lsp. Desain sing paling populer ing kene yaiku Monitor, amarga ing C # ana kondhang lock sintaksis. Monitor iki semaphore padha karo Nilai maksimum 1 (mutex), nanging karo support kanggo nunggu ing daur ulang, recursion, Pola Variabel Kondisi (liyane ing ngisor iki), etc.. Ayo kang katon ing solusi karo.

// БпрячСм ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ для ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€Π° ΠΎΡ‚ всСх, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π±Π΅Π· Π΄Π΅Π΄Π»ΠΎΠΊΠΎΠ².
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);
    }
}

Ing kene maneh mblokir kabeh meja supaya ora ngakses garpu, nanging saiki kita mbukak blokir kabeh benang bebarengan, tinimbang tanggi nalika ana wong sing wis rampung mangan. Sing. Kaping pisanan, ana wong sing mangan lan ngalangi tangga-tanggane, lan yen wis rampung, ana sing kepengin mangan maneh, banjur mlebu ing blok lan nggugah tanggane, amarga wektu nunggu sawijining kurang.

Kanthi cara iki kita ngindhari deadlocks lan keluwen sawetara filsuf. Kita nggunakake daur ulang kanggo ngenteni wektu cendhak lan mblokir thread kanggo dangu. Mbukak blokir kabeh wong bebarengan luwih alon tinimbang yen mung pepadhamu ora diblokir, kaya ing solusi AutoResetEvent, nanging prabΓ©dan ngirim ora amba, amarga Utas kudu tetep ing mode pangguna dhisik.

Π£ lock sintaksis duwe sawetara kejutan sing ora nyenengake. Dianjurake kanggo nggunakake Monitor langsung [Richter] [Eric Lippert]. Salah sijine yaiku lock tansah metu Monitor, sanajan ana pangecualian, banjur thread liyane bisa ngganti negara memori sambungan. Ing kasus kaya mengkono, iku asring luwih apik kanggo pindhah menyang deadlock utawa piye wae aman siksa program. Kaget liyane yaiku Monitor nggunakake blok jam (SyncBlock), sing ana ing kabeh obyek. Mulane, yen obyek sing ora cocog dipilih, sampeyan bisa kanthi gampang entuk deadlock (contone, yen sampeyan ngunci senar interned). Kita tansah nggunakake obyek sing didhelikake kanggo iki.

Pola Variabel Kondisi ngidini sampeyan ngleksanakake pangarepan sawetara kondisi sing rumit kanthi luwih ringkes. Ing .NET iku ora lengkap, ing mratelakake panemume, amarga ... Ing teori, kudu ana sawetara antrian ing sawetara variabel (kaya ing Posix Threads), lan ora ing siji kunci. Banjur iku bakal bisa kanggo nggawe kanggo kabeh filsuf. Nanging sanajan ing wangun iki ngidini sampeyan nyepetake kode kasebut.

Akeh filsuf utawa async / await

Oke, saiki kita bisa mblokir thread kanthi efektif. Nanging apa yen kita duwe akeh filsuf? 100? 10000? Contone, kita nampa 100000 panjalukan menyang server web. Nggawe thread kanggo saben request bakal larang, amarga supaya akeh Utas ora bakal kaleksanan ing podo karo. Mung minangka akeh intine logis bakal kaleksanan (Aku duwe 4). Lan wong liya mung bakal njupuk sumber daya. Salah sawijining solusi kanggo masalah iki yaiku pola async / await. Ide kasebut yaiku fungsi ora duwe benang yen kudu ngenteni apa sing diterusake. Lan nalika ana kedadeyan, diterusake eksekusi (nanging ora kudu ing benang sing padha!). Ing kasus kita, kita bakal ngenteni garpu.

SemaphoreSlim wis kanggo iki WaitAsync() cara. Punika implementasine nggunakake pola iki.

// Запуск Ρ‚Π°ΠΊΠΎΠΉ ΠΆΠ΅, ΠΊΠ°ΠΊ Ρ€Π°Π½ΡŒΡˆΠ΅. Π“Π΄Π΅-Π½ΠΈΠ±ΡƒΠ΄ΡŒ Π² ΠΏΡ€ΠΎΠ³Ρ€Π°ΠΌΠΌΠ΅:
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();
}

Metode karo async / await dijarwakake menyang mesin negara winates licik, kang langsung bali internal Task. Liwat, sampeyan bisa ngenteni cara rampung, mbatalake, lan kabeh sing bisa ditindakake karo Tugas. Ing cara kasebut, mesin negara ngontrol eksekusi. Ing ngisor iki yen ora ana wektu tundha, banjur eksekusi sinkron, lan yen ana, benang kasebut dibebasake. Kanggo luwih ngerti iki, iku luwih apik kanggo dipikir iki mesin negara. Sampeyan bisa nggawe rantai saka iki async / await cara.

Ayo padha nyoba. Karya 100 filsuf ing mesin karo 4 intine logis, 8 detik. Solusi sadurunge karo Monitor mung nglakokake 4 utas pisanan lan ora nglakokake liyane. Saben 4 utas iki nganggur watara 2ms. Lan solusi async / ngenteni nindakake kabeh 100, kanthi rata-rata 6.8 detik saben ngenteni. Mesthine, ing sistem nyata, nganggur nganti 6 detik ora bisa ditampa lan luwih becik ora ngolah panjaluk kanthi cara iki. Solusi karo Monitor ternyata ora bisa diukur.

kesimpulan

Nalika sampeyan bisa ndeleng saka conto cilik iki, ndhukung .NET akeh sinkronisasi mbangun. Nanging, ora mesthi jelas carane nggunakake. Muga-muga artikel iki migunani. Saiki kita mbungkus iki, nanging isih akeh barang sing menarik, contone, koleksi aman thread, TPL Dataflow, program Reaktif, model Transaksi Piranti Lunak, lsp.

Sumber informasi

Source: www.habr.com

Add a comment