Сайн хооллож буй философичид эсвэл өрсөлдөх чадвартай .NET програмчлал

Сайн хооллож буй философичид эсвэл өрсөлдөх чадвартай .NET програмчлал

Философчдын хоолны асуудлыг жишээ болгон .Net дээр зэрэгцэн ба зэрэгцээ програмчлал хэрхэн ажилладагийг харцгаая. Төлөвлөгөө нь утас / процессыг синхрончлохоос эхлээд жүжигчний загвар хүртэл (дараагийн хэсгүүдэд). Энэ нийтлэл нь анхны танил эсвэл мэдлэгээ сэргээхэд хэрэгтэй байж магадгүй юм.

Ер нь яагаад тэгдэг юм бэ? Транзисторууд хамгийн бага хэмжээндээ хүрч, Мурын хууль нь гэрлийн хурдны хязгаарлалт дээр тулгуурладаг тул тоо нэмэгдэж байгаа тул илүү олон транзистор хийх боломжтой. Үүний зэрэгцээ өгөгдлийн хэмжээ нэмэгдэж байгаа бөгөөд хэрэглэгчид системээс яаралтай хариу өгөхийг хүлээж байна. Ийм нөхцөлд "хэвийн" програмчлал нь нэг гүйцэтгэх хэлхээтэй байх нь үр дүнтэй байхаа больсон. Та ямар нэгэн байдлаар нэгэн зэрэг эсвэл зэрэгцүүлэн гүйцэтгэх асуудлыг шийдэх хэрэгтэй. Түүнээс гадна энэ асуудал янз бүрийн түвшинд байдаг: урсгалын түвшинд, процессын түвшинд, сүлжээн дэх машинуудын түвшинд (тархсан системүүд). .NET нь ийм асуудлыг хурдан бөгөөд үр дүнтэй шийдвэрлэх өндөр чанартай, цаг хугацаагаар туршсан технологитой.

Зорилго

Эдгер Дайкстра 1965 онд шавь нартаа энэ асуудлыг тавьж байсан. Тогтсон томъёолол нь дараах байдалтай байна. Тодорхой тооны (ихэвчлэн таван) философич, ижил тооны сэрээ байдаг. Тэд дугуй ширээний ард сууж, хооронд нь сэрээ хийдэг. Философичид эцэс төгсгөлгүй хоолоо идэж, бодож эсвэл хүлээх боломжтой. Философич идэхийн тулд та хоёр сэрээ авах хэрэгтэй (сүүлийнх нь сэрээгээ эхнийхтэй хуваалцдаг). Сэрээ авах, тавих нь хоёр тусдаа үйлдэл юм. Бүх философичид чимээгүй байдаг. Даалгавар бол 54 жилийн дараа ч бүгд бодож, цатгалан байх тийм алгоритмыг олох явдал юм.

Эхлээд энэ асуудлыг хуваалцсан орон зайг ашиглах замаар шийдэхийг хичээцгээе. Сэрээ нь нийтлэг ширээн дээр хэвтдэг бөгөөд философичид зүгээр л байхад нь аваад буцааж тавьдаг. Синхрончлолд асуудал гарч байна, яг хэзээ surebets авах вэ? сэрээ байхгүй бол яах вэ? гэх мэт. Гэхдээ эхлээд философичдоос эхэлцгээе.

Threads эхлүүлэхийн тулд бид дамжуулан thread pool ашигладаг Task.Run арга:

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

Thread pool нь утас үүсгэх, устгах ажлыг оновчтой болгох зорилготой юм. Энэ сан нь даалгавар бүхий дараалалтай бөгөөд CLR нь эдгээр даалгаврын тооноос хамааран хэлхээ үүсгэж эсвэл устгадаг. Бүх AppDomains-д зориулсан нэг сан. Энэ усан санг бараг үргэлж ашиглах ёстой, учир нь. Сэдэв үүсгэх, устгах, тэдгээрийн дараалал гэх мэтээр санаа зовох шаардлагагүй. Энэ нь усан сангүйгээр боломжтой, гэхдээ та үүнийг шууд ашиглах хэрэгтэй. Thread, энэ нь та урсгалын тэргүүлэх чиглэлийг өөрчлөх шаардлагатай үед, бид урт ажиллагаатай үед, Foreground thread гэх мэт тохиолдолд хэрэгтэй.

Өөрөөр хэлбэл, System.Threading.Tasks.Task анги адилхан Thread, гэхдээ бүх төрлийн ая тухыг хангасан: бусад даалгаврын блокийн дараа даалгаврыг ажиллуулах, тэдгээрийг функцээс буцаах, эвтэйхэн тасалдуулах гэх мэт. гэх мэт. Эдгээр нь асинхрон / хүлээгдэж буй бүтээцийг дэмжихэд шаардлагатай (Даалгаварт суурилсан асинхрон загвар, IO үйлдлийг хүлээхэд зориулсан синтаксик сахар). Энэ талаар бид дараа ярих болно.

CancelationTokenSource Энд энэ нь утас нь дуудаж буй утасны дохиогоор өөрөө дуусгавар болохын тулд шаардлагатай.

Синк хийх асуудал

Хоригдсон философичид

За, бид хэрхэн утас үүсгэхээ мэддэг, өдрийн хоол идэхийг хичээцгээе:

// Кто какие вилки взял. К примеру: 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();
    }
}

Энд бид эхлээд зүүн сэрээ, дараа нь баруун сэрээ авахыг хичээдэг, хэрэв энэ нь үр дүнтэй бол бид идэж, буцааж тавьдаг. Нэг сэрээ авах нь атом юм, өөрөөр хэлбэл. хоёр утас нэгэн зэрэг нэгийг авч чадахгүй (буруу: эхнийх нь сэрээ чөлөөтэй гэж уншдаг, хоёр дахь нь - мөн эхнийх нь авдаг, хоёр дахь нь авдаг). Үүний төлөө Interlocked.CompareExchange, үүнийг процессорын заавраар хэрэгжүүлэх ёстой (TSL, XCHG), атомын дараалсан унших, бичих санах ойн хэсгийг түгждэг. Мөн SpinWait нь бүтэцтэй тэнцүү юм while(true) зөвхөн бага зэрэг "ид шид" - утас нь процессорыг авдаг (Thread.SpinWait), гэхдээ заримдаа удирдлагыг өөр хэлхээ рүү шилжүүлдэг (Thread.Yeild) эсвэл унтдаг (Thread.Sleep).

Гэхдээ энэ шийдэл нь ажиллахгүй байна, учир нь Удалгүй урсгалууд (миний хувьд секундын дотор) хаагдана: бүх философичид зүүн салаагаа авдаг боловч баруун тийшээ биш. Дараа нь сэрээний массив нь дараах утгатай байна: 1 2 3 4 5.

Сайн хооллож буй философичид эсвэл өрсөлдөх чадвартай .NET програмчлал

Зураг дээр блоклох утас (гацаа). Ногоон - гүйцэтгэл, улаан - синхрончлол, саарал - утас унтаж байна. Ромбууд нь даалгавар эхлэх цагийг заадаг.

Философичдын өлсгөлөн

Хэдийгээр энэ нь ялангуяа хоол хүнс гэж бодох шаардлагагүй ч өлсгөлөн нь хэнийг ч гүн ухааныг орхиход хүргэдэг. Асуудлынхоо өлсгөлөнгийн нөхцөл байдлыг дуурайж үзье. Өлсгөлөн гэдэг нь утас гүйж байгаа боловч чухал ажил хийлгүй, өөрөөр хэлбэл энэ нь ижил мухардмал, зөвхөн одоо утас унтаагүй, харин идэх зүйлээ идэвхтэй хайж байгаа боловч хоол хүнс байхгүй байна. Байнга бөглөрөхөөс зайлсхийхийн тулд бид өөр сэрээ авч чадахгүй бол буцааж тавина.

// То же что и в 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)
    );
}

Энэ кодын хамгийн чухал зүйл бол дөрвөн философийн хоёр нь зүүн сэрээгээ тавихаа мартдаг явдал юм. Тэд илүү их хоол иддэг бол бусад нь өлсөж эхэлдэг ч утаснууд нь ижил ач холбогдолтой байдаг. Энд тэд бүрэн өлсөөгүй байна, учир нь. Муу философичид заримдаа сэрээгээ буцааж тавьдаг. Сайн хүмүүс муу хүмүүсээс 5 дахин бага иддэг нь харагдаж байна. Тиймээс кодын жижиг алдаа нь гүйцэтгэлийн уналтад хүргэдэг. Бүх философичид зүүн салаа авах, баруун тал нь байхгүй, зүүнийг тавих, хүлээх, зүүнийг дахин авах гэх мэт ховор нөхцөл байдал боломжтой гэдгийг энд тэмдэглэх нь зүйтэй. Энэ байдал нь мөн л өлсгөлөн, гацаа шиг. Би давтаж чадаагүй. Хоёр муу философич хоёулаа сэрээ авч, хоёр сайн нь өлсөж буй нөхцөл байдлын зургийг доор харуулав.

Сайн хооллож буй философичид эсвэл өрсөлдөх чадвартай .NET програмчлал

Эндээс та утаснууд заримдаа сэрж, эх сурвалжийг олж авахыг оролдож байгааг харж болно. Дөрвөн цөмийн хоёр нь юу ч хийхгүй (дээрх ногоон график).

Философичийн үхэл

Гүн ухаантнуудын оройн зоогийг тасалдуулж болох өөр нэг асуудал бол тэдний нэг нь гэнэт сэрээгээ бариад үхвэл (тэд түүнийг оршуулах болно). Дараа нь хөршүүд үдийн хоолгүй үлдэх болно. Та энэ хэргийн жишээ кодыг өөрөө гаргаж ирж болно, жишээлбэл, үүнийг хаясан NullReferenceException философич сэрээ авсны дараа. Дашрамд хэлэхэд, үл хамаарах зүйлийг зохицуулахгүй бөгөөд дуудлагын код үүнийг зүгээр л барихгүй (үүнд AppDomain.CurrentDomain.UnhandledException гэх мэт). Тиймээс алдаа зохицуулагчид утаснуудад өөрсдөө, гоёмсог төгсгөлтэй байх шаардлагатай.

Зөөгч

За, бид энэ мухардмал, өлсгөлөн, үхлийн асуудлыг хэрхэн шийдэх вэ? Бид зөвхөн нэг философичийг салаа хэсэгт хүрэхийг зөвшөөрч, энэ газарт зориулсан утаснуудын харилцан хамаарлыг нэмж оруулах болно. Үүнийг хэрхэн хийх вэ? Гүн ухаантнуудын хажууд аль нэг философид сэрээ авах зөвшөөрөл өгдөг зөөгч байна гэж бодъё. Бид энэ зөөгчийг яаж хийх вэ, философичид түүнээс хэрхэн асуух вэ гэдэг асуултууд сонирхолтой байна.

Хамгийн энгийн арга бол философичид зөөгчөөс сэрээ рүү нэвтрэхийг байнга асуух явдал юм. Тэдгээр. Одоо философичид ойр хавьд сэрээ хүлээхгүй, хүлээх эсвэл зөөгчөөс асуугаарай. Эхлээд бид үүний тулд зөвхөн Хэрэглэгчийн орон зайг ашигладаг бөгөөд үүнд бид цөмөөс ямар нэгэн процедурыг дуудахын тулд тасалдлыг ашигладаггүй (доорх тухай).

Хэрэглэгчийн орон зай дахь шийдлүүд

Энд бид нэг сэрээ, хоёр философичтой хийдэг байсан шигээ хийх болно, бид мөчлөгт эргэлдэж, хүлээх болно. Харин одоо энэ нь бүх философичид байх болно, яг л нэг сэрээ, өөрөөр хэлбэл. Энэ “алтан сэрээ”-г зөөгчөөс авсан гүн ухаантан л иднэ гэж хэлж болно. Үүний тулд бид 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 Энэ бол блоклогч бөгөөд ойролцоогоор хэлэхэд адилхан юм while(true) { if (!lock) break; }, гэхдээ үүнээс ч илүү "ид шидтэй" SpinWait (энэ нь тэнд ашиглагддаг). Одоо тэр хүлээж байгаа хүмүүсийг хэрхэн тоолох, бага зэрэг унтуулах гэх мэтийг мэддэг болсон. гэх мэт ерөнхийдөө оновчтой болгохын тулд боломжтой бүхнийг хийдэг. Гэхдээ энэ нь процессорын нөөцийг идэж, урсгалыг хадгалж байдаг идэвхтэй мөчлөг хэвээр байдгийг бид санаж байх ёстой бөгөөд хэрэв философичдын аль нэг нь бусдаас илүү тэргүүлэх байр суурь эзэлдэг боловч алтан сэрээ байхгүй бол өлсгөлөнд хүргэж болзошгүй (Priority Inversion problem) . Тиймээс бид үүнийг гуравдагч этгээдийн дуудлага, түгжээ болон бусад гэнэтийн зүйлгүйгээр хуваалцсан санах ойд маш богино хугацаанд өөрчлөхөд ашигладаг.

Сайн хооллож буй философичид эсвэл өрсөлдөх чадвартай .NET програмчлал

Зураг зурах SpinLock. Гол горхинууд алтан сэрээний төлөө тасралтгүй "зодолдож" байдаг. Алдаанууд байна - зураг дээр сонгосон газар. Цөмүүд нь бүрэн ашиглагдаагүй байна: эдгээр дөрвөн утаснаас ердөө 2/3 орчим нь.

Энд байгаа өөр нэг шийдэл нь зөвхөн ашиглах явдал юм Interlocked.CompareExchange Дээрх кодонд үзүүлсэн шиг идэвхтэй хүлээлттэй (өлсөж буй философичдын хувьд), гэхдээ энэ нь аль хэдийн хэлсэнчлэн онолын хувьд блоклоход хүргэж болзошгүй юм.

дээр Interlocked Зөвхөн биш гэдгийг тэмдэглэх нь зүйтэй CompareExchange, гэхдээ атомын унших, бичих бусад аргууд. Мөн өөрчлөлтийн давталтаар дамжуулан өөр thread-д өөрчлөлт оруулах цаг гарвал (1-ийг унш, 2-ыг унш, 2-ыг бичих, 1-ийг бичих нь муу) тохиолдолд үүнийг нэг утгад (Interlocked Anything загвар) төвөгтэй өөрчлөлт хийхэд ашиглаж болно.

Цөмийн горимын шийдэл

Гогцоонд нөөцийг дэмий үрэхгүйн тулд хэлхээг хэрхэн хаахыг харцгаая. Өөрөөр хэлбэл, бидний үлгэр жишээг үргэлжлүүлж, зөөгч философичийг хэрхэн унтуулж, зөвхөн шаардлагатай үед сэрээхийг харцгаая. Эхлээд үйлдлийн системийн цөмийн горимоор дамжуулан үүнийг хэрхэн хийхийг харцгаая. Тэнд байгаа бүх бүтэц нь хэрэглэгчийн орон зайнхаас удаан байдаг. Жишээлбэл, хэд хэдэн удаа удаан AutoResetEvent магадгүй 53 дахин удаан SpinLock [Рихтер]. Гэхдээ тэдний тусламжтайгаар та удирдаж байгаа эсэхээс үл хамааран систем дэх процессуудыг синхрончлох боломжтой.

Энд байгаа үндсэн бүтэц нь хагас зуун жилийн өмнө Дийкстрагийн санал болгосон семафор юм. Семафор гэдэг нь энгийнээр хэлбэл, системээр удирддаг эерэг бүхэл тоо бөгөөд үүн дээр нэмэх, багасгах гэсэн хоёр үйлдэл юм. Хэрэв энэ нь буурч чадахгүй бол тэг, дараа нь дуудлагын утас хаагдсан байна. Тоо нь өөр идэвхтэй урсгал/процессоор нэмэгдэхэд хэлхээ алгасаж, дамжуулсан тоогоор семафор дахин буурна. Галт тэрэгнүүд нь гацсан хоолойтой, семафороор төсөөлж болно. .NET нь ижил төстэй ажиллагаатай хэд хэдэн бүтцийг санал болгодог: AutoResetEvent, ManualResetEvent, Mutex мөн би Semaphore. Бид ашиглах болно AutoResetEvent, энэ нь эдгээр бүтцийн хамгийн энгийн нь: зөвхөн хоёр утга 0 ба 1 (худал, үнэн). Түүний арга WaitOne() Хэрэв утга 0 байсан бол дуудаж буй утсыг блоклож, 1 бол 0 болгож, алгасах болно. Нэг арга Set() 1 хүртэл өсгөж, нэг зөөгчийг нэвтрүүлэх боломжийг олгодог бөгөөд тэр нь дахин 0 хүртэл бууруулна. Метроны турник шиг ажилладаг.

Шийдвэрийг хүндрүүлж, түгжээг нэг дор биш, харин гүн ухаантан болгонд ашиглацгаая. Тэдгээр. Одоо нэг биш хэд хэдэн философич байж болно. Гэхдээ бид уралдаанаас (уралдааны нөхцөл) зайлсхийхийн тулд зөв ​​хүснэгт рүү орохыг дахин хааж, итгэлтэйгээр хүлээн авна уу.

// Для блокирования отдельного философа.
// Инициализируется: 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();
}

Энд юу болж байгааг ойлгохын тулд философич сэрээ авч чадаагүй тохиолдолд түүний үйлдэл дараах байдалтай байх болно. Тэр ширээнд орохыг хүлээж байна. Үүнийг хүлээн авсны дараа тэр сэрээ авахыг хичээдэг. Бүтсэнгүй. Энэ нь хүснэгтэд хандах боломжийг олгодог (бие биенээ хасах). Тэгээд түүний "турникет" өнгөрч байна (AutoResetEvent) (тэд анх нээлттэй байна). Энэ нь эргээд мөчлөгт ордог, учир нь түүнд сэрээ байхгүй. Тэр тэднийг авахыг оролдож, "турник" дээрээ зогсдог. Баруун эсвэл зүүн талд байгаа зарим нэг азтай хөрш хоолоо идэж дуусаад манай философийн түгжээг тайлж, "турникээ нээв". Манай философич үүнийг хоёр дахь удаагаа дамжуулдаг (мөн ард нь хаадаг). Тэр гурав дахь удаагаа сэрээ авах гэж оролдов. Амжилт хүсье. Тэгээд тэр хоол идэхээр турникээ өнгөрөөдөг.

Ийм кодонд санамсаргүй алдаа гарсан тохиолдолд (тэдгээр нь үргэлж байдаг), жишээлбэл, хөршийг буруу зааж өгсөн эсвэл ижил объект үүсгэсэн. AutoResetEvent бүгдэд нь (Enumerable.Repeat), тэгвэл философичид хөгжүүлэгчдийг хүлээж байх болно, учир нь Ийм кодын алдааг олох нь нэлээд хэцүү ажил юм. Энэ шийдлийн өөр нэг асуудал бол зарим философич өлсөхгүй гэсэн баталгаа биш юм.

Гибрид шийдэл

Бид хэрэглэгчийн горим болон давталтад үлдэх, цөмөөр дамжуулан урсгалыг хаах гэсэн хоёр аргыг авч үзсэн. Эхний арга нь богино цоожтой, хоёр дахь нь урт түгжээнд тохиромжтой. Эхлээд хувьсагчийг гогцоонд өөрчлөхийг богино хугацаанд хүлээх шаардлагатай бөгөөд дараа нь удаан хүлээх үед утсыг блоклох шаардлагатай байдаг. Энэ арга гэж нэрлэгддэг зүйлд хэрэгждэг. эрлийз бүтэц. Цөмийн горимтой ижил бүтэцүүд энд байна, гэхдээ одоо хэрэглэгчийн горимын давталттай: SemaphorSlim, ManualResetEventSlim гэх мэт Энд хамгийн алдартай загвар юм Monitor, учир нь C# хэл дээр сайн мэддэг зүйл байдаг lock синтакс. Monitor Энэ бол хамгийн ихдээ 1 (мутекс) утгатай ижил семафор боловч давталт, рекурс, Нөхцөл хувьсагчийн загвар (доорх талаар дэлгэрэнгүй) гэх мэтийг хүлээх дэмжлэгтэй. Түүнтэй шийдлийг харцгаая.

// Спрячем объект для Монитора от всех, чтобы без дедлоков.
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);
    }
}

Энд бид дахин сэрээ рүү нэвтрэхийн тулд ширээг бүхэлд нь хааж байгаа боловч одоо хэн нэгэн хоол идэж дуусахад хөрш биш харин бүх утсыг нэг дор тайлж байна. Тэдгээр. Нэгдүгээрт, хэн нэгэн хөршөө идэж, хаадаг бөгөөд энэ нь хэн нэгэн дуусмагц, тэр даруй дахин идэхийг хүсэх үед тэрээр хаалтанд орж, хөршүүдээ сэрээдэг, учир нь. түүний хүлээх хугацаа бага байна.

Ингэж л бид мухардалд орж, зарим философичдын өлсгөлөнгөөс зайлсхийдэг. Бид богино хугацаанд гогцоо ашиглаж, утасыг удаан хугацаагаар хаадаг. Бүгдийг нэг дор блокоос гаргах нь зөвхөн хөршийн блокыг тайлснаас илүү удаан байдаг AutoResetEvent, гэхдээ ялгаа нь их байх ёсгүй, учир нь thread нь эхлээд хэрэглэгчийн горимд байх ёстой.

У lock синтакс нь таагүй гэнэтийн зүйлтэй байдаг. Ашиглахыг зөвлөж байна Monitor шууд [Ричтер] [Эрик Липперт]. Тэдний нэг нь тэр lock үргэлж гадуур Monitor, Хэдийгээр үл хамаарах зүйл байсан ч энэ тохиолдолд өөр хэлхээ хуваалцсан санах ойн төлөвийг өөрчилж болно. Ийм тохиолдолд мухардалд орох эсвэл ямар нэгэн байдлаар програмыг зогсоох нь дээр. Өөр нэг гэнэтийн зүйл бол Monitor нь синхрончлолын блокуудыг ашигладаг (SyncBlock), бүх объектод байдаг. Тиймээс, хэрэв тохиромжгүй объект сонгогдвол та амархан түгжрэлд орох боломжтой (жишээлбэл, хэрэв та интернтэй утсан дээр түгжигдсэн бол). Үүний тулд бид үргэлж далд объектыг ашигладаг.

Нөхцөл байдлын хувьсагчийн загвар нь зарим нарийн төвөгтэй нөхцөл байдлын хүлээлтийг илүү товч хэрэгжүүлэх боломжийг олгодог. .NET-д энэ нь бүрэн бус, миний бодлоор, учир нь Онолын хувьд нэг лок дээр биш хэд хэдэн хувьсагч дээр (Posix Threads шиг) хэд хэдэн дараалал байх ёстой. Дараа нь тэдгээрийг бүх философичдод зориулж хийж болно. Гэхдээ энэ хэлбэрээр ч гэсэн кодыг багасгах боломжийг олгодог.

олон философич эсвэл async / await

За, одоо бид хэлхээг үр дүнтэй хаах боломжтой боллоо. Гэхдээ бид олон философичтой бол яах вэ? 100? 10000? Жишээлбэл, бид вэб сервер рүү 100000 хүсэлт хүлээн авсан. Хүсэлт бүрд thread үүсгэхэд ачаалал их байх болно, учир нь маш олон утас зэрэгцээ ажиллахгүй. Логик цөм байгаа тоогоор л ажиллана (надад 4 байна). Тэгээд бусад нь зүгээр л нөөцийг булаана. Энэ асуудлыг шийдэх нэг шийдэл бол асинк / хүлээх загвар юм. Үүний санаа нь ямар нэг зүйлийг үргэлжлүүлэхийг хүлээх шаардлагатай бол функц нь утсыг барихгүй байх явдал юм. Мөн ямар нэгэн зүйл хийх үед тэр гүйцэтгэлээ үргэлжлүүлнэ (гэхдээ заавал нэг утсан дээр биш!). Манай тохиолдолд бид сэрээ хүлээх болно.

SemaphoreSlim үүний төлөө байна WaitAsync() арга. Энэ загварыг ашигласан хэрэгжилтийг энд үзүүлэв.

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

-тэй хийх арга async / await нь нэн даруй дотооддоо буцаж ирдэг зальтай төрийн машин руу орчуулагддаг Task. Түүгээр дамжуулан та аргыг дуусгахыг хүлээх, цуцлах болон Task-тай хийж болох бусад бүх зүйлийг хийх боломжтой. Аргын дотор төрийн машин гүйцэтгэлийг хянадаг. Хамгийн гол нь хэрэв саатал байхгүй бол гүйцэтгэл нь синхрон, хэрэв байгаа бол утас чөлөөлөгдөнө. Үүнийг илүү сайн ойлгохын тулд энэ төрийн машиныг харах нь зүйтэй. Та эдгээрээс гинж үүсгэж болно async / await аргууд.

Туршилт хийцгээе. 100 философийн 4 логик цөмтэй машин дээр хийсэн ажил, 8 секунд. Монитортой өмнөх шийдэл нь зөвхөн эхний 4 хэлхээг ажиллуулж, үлдсэн хэсэг нь огт ажиллаагүй. Эдгээр 4 хэлхээ бүр 2 мс орчим идэвхгүй байсан. Мөн async / await шийдэл нь 100-ыг бүгдийг нь ажиллуулж, тус бүр дунджаар 6.8 секунд хүлээх болно. Мэдээжийн хэрэг, бодит системд 6 секундын турш сул зогсох нь хүлээн зөвшөөрөгдөхгүй бөгөөд ийм олон хүсэлтийг боловсруулахгүй байх нь дээр. Монитортой шийдэл нь өргөтгөх боломжгүй болсон.

дүгнэлт

Эдгээр жижиг жишээнүүдээс харахад .NET нь олон синхрончлолын бүтцийг дэмждэг. Гэсэн хэдий ч тэдгээрийг хэрхэн ашиглах нь үргэлж тодорхой байдаггүй. Энэ нийтлэл хэрэг болсон гэж найдаж байна. Одоохондоо энэ бол төгсгөл, гэхдээ маш олон сонирхолтой зүйлс үлдсэн хэвээр байна, тухайлбал, thread-аюулгүй цуглуулгууд, TPL Dataflow, Reactive programming, Software Transaction загвар гэх мэт.

Эх сурвалжууд

Эх сурвалж: www.habr.com

сэтгэгдэл нэмэх