Filsuf yang Kaya atau Pemrograman .NET yang Kompetitif

Filsuf yang Kaya atau Pemrograman .NET yang Kompetitif

Mari kita lihat bagaimana pemrograman bersamaan dan paralel bekerja di .Net, menggunakan Masalah Makan Bertuah sebagai contoh. Rencananya begini, dari sinkronisasi utas/proses, hingga model aktor (di bagian berikut). Artikel tersebut mungkin berguna untuk kenalan pertama atau untuk menyegarkan pengetahuan Anda.

Mengapa melakukannya sama sekali? Transistor mencapai ukuran minimumnya, hukum Moore bertumpu pada batasan kecepatan cahaya dan oleh karena itu terlihat peningkatan jumlah, lebih banyak transistor dapat dibuat. Pada saat yang sama, jumlah data bertambah, dan pengguna mengharapkan tanggapan langsung dari sistem. Dalam situasi seperti itu, pemrograman "normal", saat kita memiliki satu utas yang mengeksekusi, tidak lagi efektif. Anda perlu entah bagaimana memecahkan masalah eksekusi simultan atau bersamaan. Selain itu, masalah ini ada di level yang berbeda: di level utas, di level proses, di level mesin di jaringan (sistem terdistribusi). .NET memiliki teknologi berkualitas tinggi yang telah teruji waktu untuk memecahkan masalah tersebut dengan cepat dan efisien.

Tugas

Edsger Dijkstra mengajukan masalah ini kepada murid-muridnya sejak tahun 1965. Rumusan yang ditetapkan adalah sebagai berikut. Ada sejumlah (biasanya lima) filsuf dan jumlah garpu yang sama. Mereka duduk di meja bundar, bercabang di antara mereka. Filsuf dapat makan dari piring makanan mereka yang tak ada habisnya, berpikir atau menunggu. Untuk memakan seorang filsuf, Anda perlu mengambil dua garpu (yang terakhir berbagi garpu dengan yang pertama). Mengambil dan meletakkan garpu adalah dua tindakan terpisah. Semua filsuf diam. Tugasnya adalah menemukan algoritme sedemikian rupa sehingga semuanya akan berpikir dan menjadi penuh bahkan setelah 54 tahun.

Pertama, mari kita coba selesaikan masalah ini melalui penggunaan ruang bersama. Garpu terletak di atas meja bersama dan para filsuf hanya mengambilnya ketika sudah ada dan meletakkannya kembali. Di sini ada masalah dengan sinkronisasi, kapan tepatnya mengambil surebet? bagaimana jika tidak ada garpu? dll. Tapi pertama-tama, mari kita mulai para filsuf.

Untuk memulai utas, kami menggunakan kumpulan utas melalui Task.Run metode:

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 utas dirancang untuk mengoptimalkan pembuatan dan penghapusan utas. Kumpulan ini memiliki antrean dengan tugas dan CLR membuat atau menghapus utas tergantung pada jumlah tugas ini. Satu kumpulan untuk semua AppDomains. Kolam ini harus digunakan hampir selalu, karena. tidak perlu repot membuat, menghapus utas, antreannya, dll. Dimungkinkan tanpa kumpulan, tetapi Anda harus menggunakannya secara langsung Thread, ini berguna untuk kasus-kasus ketika Anda perlu mengubah prioritas utas, ketika kami memiliki operasi yang panjang, untuk utas Foreground, dll.

Dengan kata lain, System.Threading.Tasks.Task kelasnya sama Thread, tetapi dengan segala kemudahan: kemampuan untuk menjalankan tugas setelah blok tugas lain, mengembalikannya dari fungsi, menginterupsi dengan mudah, dan banyak lagi. dll. Mereka diperlukan untuk mendukung konstruksi async / menunggu (Pola Asinkron Berbasis Tugas, gula sintaksis untuk menunggu operasi IO). Kita akan membicarakan ini nanti.

CancelationTokenSource di sini diperlukan agar utas dapat mengakhiri dirinya sendiri atas sinyal utas pemanggil.

Masalah Sinkronisasi

Filsuf yang Diblokir

Oke kita sudah tahu cara membuat thread, mari kita coba makan siang :

// ΠšΡ‚ΠΎ ΠΊΠ°ΠΊΠΈΠ΅ Π²ΠΈΠ»ΠΊΠΈ взял. К ΠΏΡ€ΠΈΠΌΠ΅Ρ€Ρƒ: 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 pertama-tama kita coba ambil garpu kiri, lalu garpu kanan, dan jika berhasil, baru kita makan dan taruh kembali. Mengambil satu garpu adalah atom, mis. dua utas tidak dapat mengambil satu pada saat yang sama (salah: yang pertama berbunyi bahwa garpu itu gratis, yang kedua - juga, yang pertama diambil, yang kedua diambil). Untuk ini Interlocked.CompareExchange, yang harus diimplementasikan dengan instruksi prosesor (TSL, XCHG), yang mengunci sepotong memori untuk membaca dan menulis sekuensial atom. Dan SpinWait setara dengan konstruksinya while(true) hanya dengan sedikit "sihir" - utas mengambil prosesor (Thread.SpinWait), tetapi terkadang mentransfer kontrol ke utas lain (Thread.Yeild) atau tertidur (Thread.Sleep).

Tetapi solusi ini tidak berhasil, karena aliran segera (untuk saya dalam satu detik) diblokir: semua filsuf mengambil garpu kiri mereka, tetapi bukan yang kanan. Forks array kemudian memiliki nilai: 1 2 3 4 5.

Filsuf yang Kaya atau Pemrograman .NET yang Kompetitif

Pada gambar, memblokir utas (kebuntuan). Hijau - eksekusi, merah - sinkronisasi, abu-abu - utas sedang tidur. Belah ketupat menunjukkan waktu mulai Tugas.

Kelaparan Para Filsuf

Meski tidak perlu memikirkan makanan apalagi banyak, namun rasa lapar membuat siapapun menyerah pada filosofi. Mari kita coba simulasikan situasi kelaparan utas dalam masalah kita. Kelaparan adalah saat utas berjalan, tetapi tanpa kerja yang berarti, dengan kata lain, ini adalah jalan buntu yang sama, hanya sekarang utas tidak tidur, tetapi aktif mencari sesuatu untuk dimakan, tetapi tidak ada makanan. Untuk menghindari pemblokiran yang sering, kami akan mengembalikan 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)
    );
}

Hal penting tentang kode ini adalah bahwa dua dari empat filsuf lupa meletakkan garpu kirinya. Dan ternyata mereka makan lebih banyak, sementara yang lain mulai kelaparan, meski utasnya memiliki prioritas yang sama. Di sini mereka tidak sepenuhnya kelaparan, karena. filsuf yang buruk kadang-kadang mengembalikan garpu mereka. Ternyata orang baik makan sekitar 5 kali lebih sedikit daripada orang jahat. Jadi kesalahan kecil dalam kode menyebabkan penurunan kinerja. Perlu juga dicatat di sini bahwa situasi yang jarang terjadi ketika semua filsuf mengambil garpu kiri, tidak ada garpu kanan, mereka meletakkan kiri, menunggu, mengambil kiri lagi, dll. Situasi ini juga merupakan kelaparan, lebih seperti kebuntuan. Saya gagal mengulanginya. Di bawah ini adalah gambaran situasi di mana dua filsuf jahat telah mengambil kedua garpu dan dua filsuf baik kelaparan.

Filsuf yang Kaya atau Pemrograman .NET yang Kompetitif

Di sini Anda dapat melihat bahwa utas kadang-kadang bangun dan mencoba mendapatkan sumber daya. Dua dari empat inti tidak melakukan apa-apa (grafik hijau di atas).

Kematian Seorang Filsuf

Nah, masalah lain yang bisa mengganggu jamuan makan malam para filsuf adalah jika salah satu dari mereka tiba-tiba mati dengan garpu di tangannya (dan mereka akan menguburnya seperti itu). Maka tetangga akan dibiarkan tanpa makan siang. Anda dapat membuat sendiri contoh kode untuk kasus ini, misalnya, dibuang NullReferenceException setelah filsuf mengambil garpu. Dan, omong-omong, pengecualian tidak akan ditangani dan kode panggilan tidak akan menangkapnya begitu saja (untuk ini AppDomain.CurrentDomain.UnhandledException dan sebagainya.). Oleh karena itu, penangan kesalahan diperlukan di utas itu sendiri dan dengan penghentian yang baik.

Pelayan

Oke, bagaimana kita mengatasi masalah kebuntuan, kelaparan, dan kematian ini? Kami hanya akan mengizinkan satu filsuf untuk mencapai percabangan, menambahkan pengecualian utas bersama untuk tempat ini. Bagaimana cara melakukannya? Misalkan seorang pelayan berdiri di samping para filsuf, yang memberikan izin kepada salah satu filsuf untuk mengambil garpu. Bagaimana kita membuat pelayan ini dan bagaimana para filsuf akan menanyakannya, pertanyaannya menarik.

Cara paling sederhana adalah ketika para filsuf terus-menerus meminta akses ke garpu kepada pelayan. Itu. sekarang para filsuf tidak akan menunggu garpu di dekatnya, tetapi menunggu atau bertanya kepada pelayan. Pada awalnya, kami hanya menggunakan Ruang Pengguna untuk ini, di dalamnya kami tidak menggunakan interupsi untuk memanggil prosedur apa pun dari kernel (tentangnya di bawah).

Solusi di ruang pengguna

Di sini kita akan melakukan hal yang sama seperti yang biasa kita lakukan dengan satu garpu dan dua filsuf, kita akan berputar dalam satu siklus dan menunggu. Tetapi sekarang semuanya akan menjadi filsuf dan, seolah-olah, hanya satu garpu, yaitu. dapat dikatakan bahwa hanya filsuf yang mengambil "garpu emas" ini dari pelayan yang 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 pemblokir, dengan, secara kasar, sama while(true) { if (!lock) break; }, tetapi dengan lebih banyak "sihir" daripada di SpinWait (yang digunakan di sana). Sekarang dia tahu bagaimana menghitung mereka yang menunggu, menidurkan mereka sedikit, dan banyak lagi. dll. Secara umum, apakah segala kemungkinan untuk dioptimalkan. Tetapi kita harus ingat bahwa ini masih merupakan siklus aktif yang sama yang memakan sumber daya prosesor dan menjaga arus, yang dapat menyebabkan kelaparan jika salah satu filsuf menjadi lebih diprioritaskan daripada yang lain, tetapi tidak memiliki garpu emas (masalah Pembalikan Prioritas) . Oleh karena itu, kami menggunakannya hanya untuk perubahan yang sangat singkat dalam memori bersama, tanpa panggilan pihak ketiga, kunci bersarang, dan kejutan lainnya.

Filsuf yang Kaya atau Pemrograman .NET yang Kompetitif

Menggambar untuk SpinLock. Aliran terus-menerus "berjuang" untuk garpu emas. Ada kegagalan - pada gambar, area yang dipilih. Inti tidak digunakan sepenuhnya: hanya sekitar 2/3 dari keempat utas ini.

Solusi lain di sini adalah dengan menggunakan saja Interlocked.CompareExchange dengan menunggu aktif yang sama seperti yang ditunjukkan pada kode di atas (pada filsuf yang kelaparan), tetapi ini, seperti yang telah dikatakan, secara teoritis dapat menyebabkan pemblokiran.

Tentang Interlocked Perlu dicatat bahwa tidak hanya ada CompareExchange, tetapi juga metode lain untuk pembacaan dan penulisan atomik. Dan melalui pengulangan perubahan, jika utas lain memiliki waktu untuk melakukan perubahannya (baca 1, baca 2, tulis 2, tulis 1 buruk), itu dapat digunakan untuk perubahan kompleks pada satu nilai (pola Apa Pun yang Berinterlock).

Solusi Mode Kernel

Untuk menghindari pemborosan sumber daya dalam satu lingkaran, mari kita lihat bagaimana kita dapat memblokir sebuah utas. Dengan kata lain, melanjutkan contoh kita, mari kita lihat bagaimana pelayan menidurkan filsuf dan membangunkannya hanya jika diperlukan. Pertama, mari kita lihat bagaimana melakukannya melalui mode kernel sistem operasi. Semua struktur di sana seringkali lebih lambat daripada yang ada di ruang pengguna. Beberapa kali lebih lambat, misalnya AutoResetEvent mungkin 53 kali lebih lambat SpinLock [Richter]. Tetapi dengan bantuan mereka, Anda dapat menyinkronkan proses di seluruh sistem, dikelola atau tidak.

Konstruksi dasar di sini adalah semaphore yang diusulkan oleh Dijkstra lebih dari setengah abad yang lalu. Sederhananya, semafor adalah bilangan bulat positif yang dikelola oleh sistem, dan dua operasi di atasnya, kenaikan dan penurunan. Jika gagal berkurang, nol, maka utas panggilan diblokir. Ketika nomor bertambah oleh beberapa utas/proses aktif lainnya, maka utas dilewati dan semafor kembali dikurangi dengan nomor yang diteruskan. Orang bisa membayangkan kereta di kemacetan dengan semaphore. .NET menawarkan beberapa konstruksi dengan fungsionalitas serupa: AutoResetEvent, ManualResetEvent, Mutex dan diriku sendiri Semaphore. Kami akan menggunakan AutoResetEvent, ini adalah konstruksi yang paling sederhana: hanya dua nilai 0 dan 1 (salah, benar). Metodenya WaitOne() memblokir utas panggilan jika nilainya 0, dan jika 1, turunkan ke 0 dan lewati. Sebuah metode Set() naik ke 1 dan membiarkan satu pelayan lewat, yang turun lagi ke 0. Bertindak seperti pintu putar kereta bawah tanah.

Mari kita perumit solusinya dan gunakan kunci untuk setiap filsuf, dan bukan untuk semuanya sekaligus. Itu. sekarang bisa ada beberapa filsuf sekaligus, dan bukan satu. Tapi kami kembali memblokir akses ke meja agar benar, menghindari balapan (kondisi balapan), ambil surebet.

// Для блокирования ΠΎΡ‚Π΄Π΅Π»ΡŒΠ½ΠΎΠ³ΠΎ философа.
// Π˜Π½ΠΈΡ†ΠΈΠ°Π»ΠΈΠ·ΠΈΡ€ΡƒΠ΅Ρ‚ΡΡ: 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 terjadi di sini, perhatikan kasus ketika filsuf gagal mengambil garpu, maka tindakannya adalah sebagai berikut. Dia sedang menunggu akses ke meja. Setelah menerimanya, dia mencoba mengambil garpu. Tidak berhasil. Ini memberikan akses ke tabel (pengecualian bersama). Dan melewati "pintu putar" nya (AutoResetEvent) (mereka awalnya terbuka). Itu masuk ke siklus lagi, karena dia tidak punya garpu. Dia mencoba mengambilnya dan berhenti di "pintu putar" -nya. Beberapa tetangga yang lebih beruntung di kanan atau kiri, setelah selesai makan, membuka kunci filsuf kita, "membuka pintu putar". Filsuf kita melewatinya (dan menutup di belakangnya) untuk kedua kalinya. Dia mencoba untuk ketiga kalinya mengambil garpu. Semoga beruntung. Dan dia melewati pintu putar untuk makan.

Ketika ada kesalahan acak dalam kode tersebut (selalu ada), misalnya, tetangga salah ditentukan atau objek yang sama dibuat AutoResetEvent untuk semua (Enumerable.Repeat), maka para filsuf akan menunggu para pengembangnya, karena Menemukan kesalahan dalam kode semacam itu adalah tugas yang cukup sulit. Masalah lain dengan solusi ini adalah tidak menjamin bahwa beberapa filsuf tidak akan kelaparan.

Solusi hibrid

Kami telah melihat dua pendekatan untuk pengaturan waktu, saat kami tetap dalam mode pengguna dan loop, dan saat kami memblokir utas melalui kernel. Metode pertama bagus untuk kunci pendek, yang kedua untuk kunci panjang. Seringkali perlu menunggu sebentar untuk variabel berubah dalam satu lingkaran, dan kemudian memblokir utas saat menunggu lama. Pendekatan ini diimplementasikan dalam apa yang disebut. struktur hibrida. Berikut adalah konstruksi yang sama untuk mode kernel, tetapi sekarang dengan loop mode pengguna: SemaphorSlim, ManualResetEventSlim dll. Desain paling populer di sini adalah Monitor, Karena di C # ada yang terkenal lock sintaksis. Monitor ini adalah semafor yang sama dengan nilai maksimum 1 (mutex), tetapi dengan dukungan untuk menunggu dalam satu lingkaran, rekursi, pola Variabel Kondisi (lebih lanjut di bawah), dll. Mari kita lihat solusinya.

// БпрячСм ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ для ΠœΠΎΠ½ΠΈΡ‚ΠΎΡ€Π° ΠΎΡ‚ всСх, Ρ‡Ρ‚ΠΎΠ±Ρ‹ Π±Π΅Π· Π΄Π΅Π΄Π»ΠΎΠΊΠΎΠ².
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 memblokir seluruh tabel untuk akses ke garpu, tetapi sekarang kami membuka blokir semua utas sekaligus, dan bukan tetangga saat seseorang selesai makan. Itu. pertama, seseorang makan dan memblokir tetangganya, dan ketika seseorang selesai, tetapi ingin makan lagi segera, dia memblokir dan membangunkan tetangganya, karena. waktu tunggunya kurang.

Beginilah cara kami menghindari kebuntuan dan kelaparan beberapa filsuf. Kami menggunakan loop untuk menunggu sebentar dan memblokir utas untuk waktu yang lama. Membuka blokir semua orang sekaligus lebih lambat daripada jika hanya tetangga yang dibuka blokirnya, seperti dalam solusi dengan AutoResetEvent, tetapi perbedaannya tidak boleh besar, karena utas harus tetap dalam mode pengguna terlebih dahulu.

Π£ lock sintaks memiliki kejutan yang tidak menyenangkan. Merekomendasikan untuk digunakan Monitor langsung [Richter] [Eric Lippert]. Salah satunya adalah itu lock selalu keluar dari Monitor, bahkan jika ada pengecualian, dalam hal ini utas lain dapat mengubah status memori bersama. Dalam kasus seperti itu, seringkali lebih baik pergi ke kebuntuan atau entah bagaimana menghentikan program dengan aman. Kejutan lainnya adalah Monitor menggunakan blok sinkronisasi (SyncBlock), yang ada di semua objek. Oleh karena itu, jika objek yang tidak sesuai dipilih, Anda dapat dengan mudah mengalami kebuntuan (misalnya, jika Anda mengunci string yang diinternir). Kami menggunakan objek selalu tersembunyi untuk ini.

Pola Variabel Kondisi memungkinkan Anda untuk mengimplementasikan ekspektasi dari beberapa kondisi kompleks secara lebih ringkas. Di .NET kurang lengkap menurut saya karena secara teori, harus ada beberapa antrian pada beberapa variabel (seperti pada Posix Threads), dan bukan pada satu lok. Kemudian seseorang dapat membuatnya untuk semua filsuf. Tetapi bahkan dalam bentuk ini memungkinkan Anda untuk mengurangi kode.

banyak filsuf atau async / await

Oke, sekarang kita bisa memblokir utas secara efektif. Tetapi bagaimana jika kita memiliki banyak filsuf? 100? 10000? Misalnya, kami menerima 100000 permintaan ke server web. Ini akan menjadi overhead untuk membuat utas untuk setiap permintaan, karena begitu banyak utas tidak akan berjalan secara paralel. Hanya akan berjalan sebanyak inti logis (saya punya 4). Dan semua orang hanya akan mengambil sumber daya. Salah satu solusi untuk masalah ini adalah pola async / await. Idenya adalah bahwa fungsi tersebut tidak menahan utas jika perlu menunggu sesuatu untuk dilanjutkan. Dan ketika ia melakukan sesuatu, ia melanjutkan eksekusinya (tetapi tidak harus di utas yang sama!). Dalam kasus kami, kami akan menunggu garpu.

SemaphoreSlim memiliki untuk ini WaitAsync() metode. Berikut adalah implementasi menggunakan pola 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();
}

Metode dengan async / await diterjemahkan ke dalam mesin keadaan rumit yang segera mengembalikan internalnya Task. Melalui itu, Anda dapat menunggu penyelesaian metode, membatalkannya, dan semua hal lain yang dapat Anda lakukan dengan Task. Di dalam metode, mesin negara mengontrol eksekusi. Intinya adalah jika tidak ada penundaan, maka eksekusi akan sinkron, dan jika ada, maka utas dilepaskan. Untuk pemahaman yang lebih baik tentang ini, lebih baik melihat mesin negara ini. Anda dapat membuat rantai dari ini async / await metode.

Mari kita uji. Karya 100 filsuf pada mesin dengan 4 inti logis, 8 detik. Solusi sebelumnya dengan Monitor hanya menjalankan 4 utas pertama dan sisanya tidak berjalan sama sekali. Masing-masing dari 4 utas ini menganggur selama sekitar 2 md. Dan solusi async / await berjalan 100, dengan rata-rata menunggu masing-masing 6.8 detik. Tentu saja, dalam sistem nyata, menganggur selama 6 detik tidak dapat diterima dan lebih baik tidak memproses begitu banyak permintaan seperti ini. Solusi dengan Monitor ternyata tidak dapat diskalakan sama sekali.

Kesimpulan

Seperti yang Anda lihat dari contoh kecil ini, .NET mendukung banyak konstruksi sinkronisasi. Namun, tidak selalu jelas bagaimana menggunakannya. Semoga artikel ini bermanfaat. Untuk saat ini, ini adalah akhir, tetapi masih banyak hal menarik yang tersisa, misalnya koleksi thread-safe, TPL Dataflow, Pemrograman reaktif, model Transaksi Perangkat Lunak, dll.

sumber

Sumber: www.habr.com

Tambah komentar