Ahli Falsafah atau Pengaturcaraan .NET yang Berdaya saing

Ahli Falsafah atau Pengaturcaraan .NET yang Berdaya saing

Mari lihat cara pengaturcaraan serentak dan selari berfungsi dalam .Net, menggunakan Masalah Makan Falsafah sebagai contoh. Pelannya adalah ini, daripada penyegerakan benang / proses, kepada model pelakon (dalam bahagian berikut). Artikel itu mungkin berguna untuk kenalan pertama atau untuk menyegarkan pengetahuan anda.

Mengapa melakukannya sama sekali? Transistor mencapai saiz minimumnya, hukum Moore terletak pada had kelajuan cahaya dan oleh itu peningkatan diperhatikan dalam bilangan, lebih banyak transistor boleh dibuat. Pada masa yang sama, jumlah data semakin meningkat, dan pengguna mengharapkan tindak balas segera daripada sistem. Dalam keadaan sedemikian, pengaturcaraan "biasa", apabila kita mempunyai satu utas pelaksana, tidak lagi berkesan. Anda perlu menyelesaikan masalah pelaksanaan serentak atau serentak. Selain itu, masalah ini wujud pada tahap yang berbeza: pada tahap benang, pada tahap proses, pada tahap mesin dalam rangkaian (sistem teragih). .NET mempunyai teknologi berkualiti tinggi yang diuji masa untuk menyelesaikan masalah sedemikian dengan cepat dan cekap.

Petugas

Edsger Dijkstra telah mengemukakan masalah ini kepada pelajarnya seawal tahun 1965. Rumusan yang telah ditetapkan adalah seperti berikut. Terdapat bilangan ahli falsafah tertentu (biasanya lima) dan bilangan garpu yang sama. Mereka duduk di meja bulat, garpu di antara mereka. Ahli falsafah boleh makan dari pinggan makanan mereka yang tidak berkesudahan, berfikir atau menunggu. Untuk makan ahli falsafah, anda perlu mengambil dua garpu (yang terakhir berkongsi garpu dengan yang pertama). Mengambil dan meletakkan garpu adalah dua tindakan yang berasingan. Semua ahli falsafah diam. Tugasnya adalah untuk mencari algoritma sedemikian yang mereka semua akan berfikir dan kenyang walaupun selepas 54 tahun.

Mula-mula, mari cuba selesaikan masalah ini melalui penggunaan ruang kongsi. Garpu terletak di atas meja biasa dan ahli falsafah hanya mengambilnya apabila ada dan meletakkannya kembali. Di sini terdapat masalah dengan penyegerakan, bila tepat untuk mengambil surebets? bagaimana jika tiada garpu? dll. Tetapi pertama, mari kita mulakan ahli falsafah.

Untuk memulakan benang, kami menggunakan kumpulan benang melalui Task.Run kaedah:

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

Kumpulan benang direka untuk mengoptimumkan penciptaan dan pemadaman benang. Kumpulan ini mempunyai baris gilir dengan tugasan dan CLR mencipta atau mengalih keluar benang bergantung pada bilangan tugasan ini. Satu kolam untuk semua AppDomains. Kolam ini harus digunakan hampir selalu, kerana. tidak perlu bersusah payah membuat, memadamkan benang, baris gilirnya, dll. Ia mungkin tanpa kolam, tetapi kemudian anda perlu menggunakannya secara langsung Thread, ini berguna untuk kes apabila anda perlu menukar keutamaan utas, apabila kami menjalankan operasi yang lama, untuk utas latar depan, dsb.

Dalam erti kata lain, System.Threading.Tasks.Task kelas pun sama Thread, tetapi dengan pelbagai jenis kemudahan: keupayaan untuk menjalankan tugas selepas blok tugas lain, mengembalikannya daripada fungsi, mengganggunya dengan mudah dan banyak lagi. dsb. Mereka diperlukan untuk menyokong pembinaan async / menunggu (Corak Asynchronous berasaskan tugas, gula sintaktik untuk menunggu operasi IO). Kita akan bercakap tentang ini kemudian.

CancelationTokenSource di sini ia diperlukan supaya benang boleh menamatkan dirinya sendiri pada isyarat benang panggilan.

Isu Penyegerakan

Ahli Falsafah yang disekat

Baiklah, kami tahu cara membuat rangkaian, mari cuba makan tengah hari:

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

Di sini kita mula-mula cuba mengambil garpu kiri, dan kemudian garpu kanan, dan jika ia berjaya, maka kita makan dan meletakkannya kembali. Mengambil satu garpu adalah atom, i.e. dua benang tidak boleh mengambil satu pada masa yang sama (salah: yang pertama berbunyi bahawa garpu adalah percuma, yang kedua - juga, yang pertama mengambil, yang kedua mengambil). Untuk ini Interlocked.CompareExchange, yang mesti dilaksanakan dengan arahan pemproses (TSL, XCHG), yang mengunci sekeping ingatan untuk membaca dan menulis urutan atom. Dan SpinWait adalah bersamaan dengan binaan while(true) hanya dengan sedikit "ajaib" - benang mengambil pemproses (Thread.SpinWait), tetapi kadangkala memindahkan kawalan ke utas lain (Thread.Yeild) atau tertidur (Thread.Sleep).

Tetapi penyelesaian ini tidak berfungsi, kerana aliran tidak lama lagi (untuk saya dalam satu saat) disekat: semua ahli falsafah mengambil garpu kiri mereka, tetapi bukan yang kanan. Tatasusunan forks kemudiannya mempunyai nilai: 1 2 3 4 5.

Ahli Falsafah atau Pengaturcaraan .NET yang Berdaya saing

Dalam rajah, menyekat benang (kebuntuan). Hijau - pelaksanaan, merah - penyegerakan, kelabu - benang sedang tidur. Ketupat menunjukkan masa mula Tugasan.

Kelaparan Ahli Falsafah

Walaupun tidak perlu untuk berfikir terutamanya banyak makanan, tetapi kelaparan membuat sesiapa sahaja melepaskan falsafah. Mari cuba meniru keadaan kebuluran benang dalam masalah kita. Kelaparan adalah apabila benang berjalan, tetapi tanpa kerja yang penting, dalam erti kata lain, ini adalah kebuntuan yang sama, hanya sekarang benang tidak tidur, tetapi secara aktif mencari sesuatu untuk dimakan, tetapi tidak ada makanan. Untuk mengelakkan sekatan yang kerap, kami akan meletakkan semula garpu jika kami tidak dapat mengambil yang lain.

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

Perkara penting tentang kod ini ialah dua daripada empat ahli falsafah terlupa meletakkan garpu kiri mereka. Dan ternyata mereka makan lebih banyak makanan, sementara yang lain mula kelaparan, walaupun benang mempunyai keutamaan yang sama. Di sini mereka tidak kelaparan sepenuhnya, kerana. ahli falsafah jahat meletakkan garpu mereka kembali kadang-kadang. Ternyata orang yang baik makan lebih kurang 5 kali ganda daripada yang buruk. Jadi ralat kecil dalam kod membawa kepada penurunan prestasi. Perlu juga diperhatikan di sini bahawa situasi yang jarang berlaku apabila semua ahli falsafah mengambil garpu kiri, tidak ada yang kanan, mereka meletakkan kiri, menunggu, mengambil kiri lagi, dll. Keadaan ini juga kebuluran, lebih seperti kebuntuan. Saya gagal mengulanginya. Di bawah adalah gambar untuk situasi di mana dua ahli falsafah jahat telah mengambil kedua-dua garpu dan dua yang baik sedang kelaparan.

Ahli Falsafah atau Pengaturcaraan .NET yang Berdaya saing

Di sini anda boleh melihat bahawa benang terjaga kadangkala dan cuba mendapatkan sumber. Dua daripada empat teras tidak melakukan apa-apa (graf hijau di atas).

Kematian seorang Ahli Falsafah

Nah, satu lagi masalah yang boleh mengganggu makan malam yang mulia ahli falsafah adalah jika salah seorang daripada mereka tiba-tiba mati dengan garpu di tangannya (dan mereka akan menguburkannya seperti itu). Kemudian jiran akan ditinggalkan tanpa makan tengah hari. Anda boleh menghasilkan kod contoh untuk kes ini sendiri, contohnya, ia dibuang NullReferenceException selepas ahli falsafah mengambil garpu. Dan, dengan cara ini, pengecualian tidak akan dikendalikan dan kod panggilan tidak akan menangkapnya sahaja (untuk ini AppDomain.CurrentDomain.UnhandledException dan lain-lain). Oleh itu, pengendali ralat diperlukan dalam benang itu sendiri dan dengan penamatan yang anggun.

Pelayan

Okay, macam mana nak selesaikan masalah kebuntuan, kebuluran dan kematian ini? Kami akan membenarkan hanya seorang ahli falsafah untuk mencapai garpu, menambah pengecualian bersama untuk tempat ini. Bagaimana hendak melakukannya? Katakan seorang pelayan berdiri di sebelah ahli falsafah, yang memberi kebenaran kepada mana-mana ahli falsafah untuk mengambil garpu. Bagaimana kita membuat pelayan ini dan bagaimana ahli falsafah akan bertanya kepadanya, soalan itu menarik.

Cara paling mudah ialah apabila ahli falsafah akan sentiasa meminta pelayan untuk mengakses garpu. Itu. sekarang ahli falsafah tidak akan menunggu garpu berhampiran, tetapi tunggu atau tanya pelayan. Pada mulanya, kami hanya menggunakan Ruang Pengguna untuk ini, di dalamnya kami tidak menggunakan gangguan untuk memanggil sebarang prosedur dari kernel (tentangnya di bawah).

Penyelesaian dalam ruang pengguna

Di sini kita akan melakukan perkara yang sama seperti yang biasa kita lakukan dengan satu garpu dan dua ahli falsafah, kita akan berputar dalam kitaran dan menunggu. Tetapi sekarang ia akan menjadi semua ahli falsafah dan, seolah-olah, hanya satu garpu, i.e. boleh dikatakan hanya ahli falsafah yang mengambil "garpu emas" ini daripada pelayan akan makan. Untuk ini kami menggunakan 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 ini adalah penghalang, dengan, secara kasarnya, sama while(true) { if (!lock) break; }, tetapi dengan lebih "ajaib" daripada dalam SpinWait (yang digunakan di sana). Sekarang dia tahu cara mengira mereka yang menunggu, menidurkan mereka sedikit, dan banyak lagi. dll. Secara umum, melakukan segala yang mungkin untuk mengoptimumkan. Tetapi kita mesti ingat bahawa ini masih kitaran aktif yang sama yang memakan sumber pemproses dan mengekalkan aliran, yang boleh menyebabkan kebuluran jika salah seorang ahli falsafah menjadi lebih keutamaan daripada yang lain, tetapi tidak mempunyai garpu emas (masalah Penyongsangan Keutamaan) . Oleh itu, kami menggunakannya hanya untuk perubahan yang sangat singkat dalam memori dikongsi, tanpa sebarang panggilan pihak ketiga, kunci bersarang dan kejutan lain.

Ahli Falsafah atau Pengaturcaraan .NET yang Berdaya saing

Melukis untuk SpinLock. Aliran sungai sentiasa "berjuang" untuk garpu emas. Terdapat kegagalan - dalam rajah, kawasan yang dipilih. Teras tidak digunakan sepenuhnya: hanya kira-kira 2/3 oleh empat benang ini.

Penyelesaian lain di sini ialah menggunakan sahaja Interlocked.CompareExchange dengan menunggu aktif yang sama seperti yang ditunjukkan dalam kod di atas (dalam ahli falsafah yang kelaparan), tetapi ini, seperti yang telah dikatakan, secara teorinya boleh menyebabkan penyekatan.

pada Interlocked Perlu diingatkan bahawa terdapat bukan sahaja CompareExchange, tetapi juga kaedah lain untuk membaca DAN menulis atom. Dan melalui pengulangan perubahan, sekiranya benang lain mempunyai masa untuk membuat perubahannya (baca 1, baca 2, tulis 2, tulis 1 adalah buruk), ia boleh digunakan untuk perubahan kompleks kepada satu nilai (corak Apa-apa Saling Terkunci).

Penyelesaian Mod Kernel

Untuk mengelakkan pembaziran sumber dalam satu gelung, mari lihat cara kita boleh menyekat benang. Dengan kata lain, meneruskan contoh kita, mari kita lihat bagaimana pelayan itu menidurkan ahli falsafah dan membangunkannya hanya apabila perlu. Mula-mula, mari kita lihat bagaimana untuk melakukan ini melalui mod kernel sistem pengendalian. Semua struktur di sana selalunya lebih perlahan daripada struktur di ruang pengguna. Beberapa kali lebih perlahan, sebagai contoh AutoResetEvent mungkin 53 kali lebih perlahan SpinLock [Lebih kaya]. Tetapi dengan bantuan mereka, anda boleh menyegerakkan proses di seluruh sistem, diuruskan atau tidak.

Konstruk asas di sini ialah semafor yang dicadangkan oleh Dijkstra lebih setengah abad yang lalu. Semaphore ialah, secara ringkasnya, integer positif yang diuruskan oleh sistem, dan dua operasi padanya, kenaikan dan pengurangan. Jika gagal menurun, sifar, maka benang panggilan disekat. Apabila nombor itu dinaikkan oleh beberapa utas/proses aktif lain, maka utas dilangkau dan semaphore sekali lagi dikurangkan dengan nombor yang diluluskan. Orang boleh bayangkan kereta api dalam kesesakan dengan semafor. .NET menawarkan beberapa binaan dengan fungsi yang serupa: AutoResetEvent, ManualResetEvent, Mutex dan saya sendiri Semaphore. Kami akan gunakan AutoResetEvent, ini adalah binaan yang paling mudah: hanya dua nilai 0 dan 1 (salah, benar). Kaedah beliau WaitOne() menyekat utas panggilan jika nilainya ialah 0, dan jika 1, menurunkannya kepada 0 dan melangkaunya. Satu kaedah Set() dinaikkan kepada 1 dan membolehkan seorang pelayan melalui, yang sekali lagi menurunkan kepada 0. Bertindak seperti turnstile kereta bawah tanah.

Mari kita rumitkan penyelesaian dan gunakan kunci untuk setiap ahli falsafah, dan bukan untuk semua sekaligus. Itu. kini boleh ada beberapa ahli falsafah sekaligus, dan bukan seorang. Tetapi kami sekali lagi menyekat akses ke meja untuk betul, mengelakkan perlumbaan (keadaan perlumbaan), mengambil surebets.

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

Untuk memahami apa yang berlaku di sini, pertimbangkan kes apabila ahli falsafah gagal mengambil garpu, maka tindakannya akan seperti berikut. Dia sedang menunggu akses ke meja. Setelah menerimanya, dia cuba mengambil garpu. Tidak berjaya. Ia memberikan akses kepada jadual (pengecualian bersama). Dan melepasi "turnstile"nya (AutoResetEvent) (mereka pada mulanya terbuka). Ia masuk ke dalam kitaran semula, kerana dia tidak mempunyai garpu. Dia cuba mengambilnya dan berhenti di "turnstile"nya. Beberapa jiran yang lebih bernasib baik di sebelah kanan atau kiri, setelah selesai makan, membuka kunci ahli falsafah kami, "membuka pintu putarnya." Ahli falsafah kita melepasinya (dan ia menutup di belakangnya) untuk kali kedua. Dia cuba buat kali ketiga untuk mengambil garpu. Semoga berjaya. Dan dia melepasi pintu putarnya untuk menjamu selera.

Apabila terdapat ralat rawak dalam kod tersebut (ia sentiasa wujud), sebagai contoh, jiran tidak dinyatakan dengan betul atau objek yang sama dicipta AutoResetEvent untuk semua (Enumerable.Repeat), maka ahli falsafah akan menunggu pemaju, kerana Mencari ralat dalam kod sedemikian adalah tugas yang agak sukar. Satu lagi masalah dengan penyelesaian ini ialah ia tidak menjamin bahawa sesetengah ahli falsafah tidak akan kelaparan.

Penyelesaian Hibrid

Kami telah melihat dua pendekatan untuk pemasaan, apabila kami kekal dalam mod dan gelung pengguna, dan apabila kami menyekat benang melalui kernel. Kaedah pertama adalah baik untuk kunci pendek, yang kedua untuk yang panjang. Selalunya perlu menunggu sebentar untuk pembolehubah berubah dalam gelung, dan kemudian menyekat benang apabila menunggu lama. Pendekatan ini dilaksanakan dalam apa yang dipanggil. struktur hibrid. Berikut adalah binaan yang sama seperti untuk mod kernel, tetapi kini dengan gelung mod pengguna: SemaphorSlim, ManualResetEventSlim dan lain-lain. Reka bentuk yang paling popular di sini ialah Monitor, kerana dalam C# ada yang terkenal lock sintaks. Monitor ini adalah semafor yang sama dengan nilai maksimum 1 (mutex), tetapi dengan sokongan untuk menunggu dalam gelung, rekursi, corak Pembolehubah Keadaan (lebih lanjut mengenainya di bawah), dan lain-lain. Mari kita lihat penyelesaian dengannya.

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

Di sini kami sekali lagi menyekat seluruh meja untuk akses kepada garpu, tetapi kini kami membuka sekatan semua benang sekaligus, dan bukan jiran apabila seseorang selesai makan. Itu. pertama, seseorang makan dan menyekat jiran, dan apabila ini seseorang selesai, tetapi ingin makan lagi segera, dia pergi ke menyekat dan membangunkan jirannya, kerana. masa menunggunya kurang.

Inilah cara kita mengelakkan kebuntuan dan kelaparan sesetengah ahli falsafah. Kami menggunakan gelung untuk menunggu sebentar dan menyekat benang untuk tempoh yang lama. Menyahsekat semua orang sekaligus adalah lebih perlahan daripada jika hanya jiran yang dinyahsekat, seperti dalam penyelesaian dengan AutoResetEvent, tetapi perbezaannya tidak sepatutnya besar, kerana benang mesti kekal dalam mod pengguna dahulu.

Π£ lock sintaks mempunyai kejutan yang tidak menyenangkan. Mengesyorkan untuk digunakan Monitor secara langsung [Richter] [Eric Lippert]. Salah satunya ialah itu lock selalu keluar dari Monitor, walaupun terdapat pengecualian, dalam hal ini urutan lain boleh menukar keadaan memori yang dikongsi. Dalam kes sedemikian, selalunya lebih baik untuk pergi ke kebuntuan atau entah bagaimana menamatkan program dengan selamat. Satu lagi kejutan ialah Monitor menggunakan blok penyegerakan (SyncBlock), yang terdapat dalam semua objek. Oleh itu, jika objek yang tidak sesuai dipilih, anda boleh mendapatkan jalan buntu dengan mudah (contohnya, jika anda mengunci pada rentetan yang diinternet). Kami menggunakan objek yang sentiasa tersembunyi untuk ini.

Corak Pembolehubah Keadaan membolehkan anda melaksanakan jangkaan beberapa keadaan kompleks dengan lebih ringkas. Dalam .NET, ia tidak lengkap, pada pendapat saya, kerana secara teori, perlu ada beberapa baris gilir pada beberapa pembolehubah (seperti dalam Posix Threads), dan bukan pada satu lok. Kemudian seseorang boleh membuatnya untuk semua ahli falsafah. Tetapi walaupun dalam bentuk ini, ia membolehkan anda mengurangkan kod.

ramai ahli falsafah atau async / await

Okay, sekarang kita boleh menyekat benang dengan berkesan. Tetapi bagaimana jika kita mempunyai ramai ahli falsafah? 100? 10000? Sebagai contoh, kami menerima 100000 permintaan kepada pelayan web. Ia akan menjadi overhed untuk membuat urutan untuk setiap permintaan, kerana begitu banyak benang tidak akan berjalan selari. Hanya akan berjalan seberapa banyak yang terdapat teras logik (saya mempunyai 4). Dan orang lain hanya akan mengambil sumber. Satu penyelesaian kepada masalah ini ialah corak async / await. Ideanya ialah fungsi itu tidak memegang benang jika ia perlu menunggu sesuatu untuk diteruskan. Dan apabila ia melakukan sesuatu, ia menyambung semula pelaksanaannya (tetapi tidak semestinya pada urutan yang sama!). Dalam kes kami, kami akan menunggu garpu.

SemaphoreSlim mempunyai untuk ini WaitAsync() kaedah. Berikut ialah pelaksanaan menggunakan corak ini.

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

Kaedah dengan async / await diterjemahkan ke dalam mesin keadaan rumit yang segera mengembalikan dalamannya Task. Melaluinya, anda boleh menunggu penyelesaian kaedah, membatalkannya, dan semua perkara lain yang boleh anda lakukan dengan Tugas. Di dalam kaedah, mesin negeri mengawal pelaksanaan. Intinya ialah jika tidak ada kelewatan, maka pelaksanaan adalah segerak, dan jika ada, maka benang dilepaskan. Untuk pemahaman yang lebih baik tentang ini, adalah lebih baik untuk melihat mesin keadaan ini. Anda boleh membuat rantai daripada ini async / await kaedah.

Jom uji. Kerja 100 ahli falsafah pada mesin dengan 4 teras logik, 8 saat. Penyelesaian sebelumnya dengan Monitor hanya menjalankan 4 utas pertama dan selebihnya tidak berjalan sama sekali. Setiap satu daripada 4 utas ini melahu selama kira-kira 2ms. Dan penyelesaian async / await menjalankan semua 100, dengan purata menunggu 6.8 saat setiap satu. Sudah tentu, dalam sistem sebenar, melahu selama 6 saat tidak boleh diterima dan adalah lebih baik untuk tidak memproses begitu banyak permintaan seperti ini. Penyelesaian dengan Monitor ternyata tidak berskala sama sekali.

Kesimpulan

Seperti yang anda boleh lihat daripada contoh kecil ini, .NET menyokong banyak binaan penyegerakan. Walau bagaimanapun, ia tidak selalunya jelas cara menggunakannya. Saya harap artikel ini berguna. Buat masa ini, ini adalah penghujungnya, tetapi masih terdapat banyak perkara menarik yang tinggal, contohnya, koleksi selamat benang, TPL Dataflow, Pengaturcaraan Reaktif, model Transaksi Perisian, dsb.

sumber

Sumber: www.habr.com

Tambah komen