Well-Fed Philosophers же атаандаштыкка жөндөмдүү .NET программалоо

Well-Fed Philosophers же атаандаштыкка жөндөмдүү .NET программалоо

Мисал катары Philosophers Dining проблемасын колдонуп, .Netте параллелдүү жана параллелдүү программалоо кандай иштээрин карап көрөлү. План бул, жиптерди / процесстерди синхрондоштуруудан баштап, актер моделине чейин (кийинки бөлүктерде). Макала биринчи таанышуу үчүн же билимиңизди жаңылоо үчүн пайдалуу болушу мүмкүн.

Дегеле эмне үчүн? Транзисторлор минималдуу өлчөмдөрүнө жетет, Мур мыйзамы жарыктын ылдамдыгын чектөөгө таянат жана ошондуктан алардын саны көбөйөт, транзисторлорду көбүрөөк жасоого болот. Ошол эле учурда, маалыматтардын көлөмү өсүп, колдонуучулар системалардан дароо жооп күтүшөт. Мындай кырдаалда, бизде бир аткаруучу жип болгондо "нормалдуу" программалоо мындан ары эффективдүү болбой калат. Сиз кандайдыр бир жол менен бир убакта же бир убакта аткаруу маселесин чечүү керек. Мындан тышкары, бул көйгөй ар кандай деңгээлде бар: жиптердин деңгээлинде, процесстердин деңгээлинде, тармактагы машиналардын деңгээлинде (бөлүштүрүлгөн системалар). .NET мындай көйгөйлөрдү тез жана натыйжалуу чечүү үчүн сапаттуу, убакыт сынагынан өткөн технологияларга ээ.

маселе

Эдгер Дейкстра бул маселени 1965-жылы эле өзүнүн окуучуларына койгон. Белгиленген формула төмөнкүчө. Белгилүү (көбүнчө беш) философтор жана бирдей сандагы айрылар бар. Алар тегерек столдо отурушат, ортолорунда айрылар. Философтор табактарынан чексиз тамак жей алышат, ойлонуп же күтө алышат. Философду жеш үчүн эки вилканы алыш керек (акыркысы вилканы биринчиси менен бөлүшөт). Айрыкчаны алуу жана коюу эки башка иш. Бардык философтор унчукпайт. Алардын баары 54 жылдан кийин да ойлонуп, ток боло тургандай алгоритмди табуу милдети турат.

Биринчиден, бул маселени жалпы мейкиндикти колдонуу аркылуу чечүүгө аракет кылалы. Айрылар жалпы үстөлдүн үстүндө жатат жана философтор аларды жөн эле турганда алып, кайра коюшат. Бул жерде синхрондоштурууда көйгөйлөр бар, так качан surebets алуу керек? айры жок болсочы? ж.б. Бирок адегенде философтордон баштайлы.

Жиптерди баштоо үчүн биз жип бассейнин колдонобуз 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);
}

Жип бассейни жипти түзүүнү жана жок кылууну оптималдаштыруу үчүн иштелип чыккан. Бул бассейнде тапшырмалар менен кезек бар жана CLR бул тапшырмалардын санына жараша жиптерди түзөт же жок кылат. Бардык AppDomains үчүн бир бассейн. Бул бассейн дээрлик ар дайым колдонулушу керек, анткени. жиптерди, алардын кезектерин түзүү, жок кылуу, ж.б.у.с. менен убара болуунун кереги жок. Бул бассейнсиз да мүмкүн, бирок андан кийин аны түз колдонуу керек. 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.

Well-Fed Philosophers же атаандаштыкка жөндөмдүү .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 эсе аз жейт экен. Ошентип, коддогу кичинекей ката аткаруунун төмөндөшүнө алып келет. Бул жерде дагы айта кетчү нерсе, сейрек кездешүүчү жагдай бардык философтор сол айрыны алып, оңу жок, солду коюшат, күтүшөт, солду кайра алышат ж.б.у.с. Бул абал да ачарчылык, туюктук сыяктуу. Мен аны кайталай алган жокмун. Төмөндө эки жаман философтун эки вилкасын алып, эки жакшысы ачка отурган кырдаалдын сүрөтү.

Well-Fed Philosophers же атаандаштыкка жөндөмдүү .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 problemi) . Ошондуктан, биз аны жалпы эстутумдагы өтө кыска өзгөртүүлөр үчүн гана колдонобуз, эч кандай үчүнчү тараптын чалуулары, уяланган кулпулары жана башка күтүүсүз нерселер.

Well-Fed Philosophers же атаандаштыкка жөндөмдүү .NET программалоо

үчүн тартуу SpinLock. Агымдар алтын айры үчүн тынымсыз «күрөшүүдө». кемчиликтер бар - сүрөттө, тандалган аймак. Өзөктөр толук пайдаланылбайт: бул төрт жиптин 2/3 бөлүгү гана.

Бул жерде дагы бир чечим гана колдонуу болот Interlocked.CompareExchange жогорудагы коддо көрсөтүлгөндөй активдүү күтүү менен (ачка калган философтордо), бирок бул, мурда айтылгандай, теориялык жактан бөгөт коюуга алып келиши мүмкүн.

боюнча Interlocked гана эмес экенин белгилей кетүү керек CompareExchange, ошондой эле атомдук окуу жана жазуу үчүн башка ыкмалар. Жана өзгөртүүнү кайталоо аркылуу, эгер башка жип өз өзгөртүүлөрдү киргизүүгө үлгүрсө (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, бирок айырма чоң болбошу керек, анткени жиптер адегенде колдонуучу режиминде калышы керек.

У lock синтаксисте жагымсыз сюрприз бар. колдонууну сунуштайбыз Monitor түздөн-түз [Рихтер] [Эрик Липперт]. Алардын бири ошол lock дайыма чыгып Monitor, өзгөчө учур болсо дагы, бул учурда башка жип бөлүшүлгөн эстутум абалын өзгөртө алат. Мындай учурларда, көп учурда туюкка баруу же кандайдыр бир жол менен программаны коопсуз токтотуу жакшы. Дагы бир таң калыштуусу, Monitor синхрондоштуруу блокторун колдонот (SyncBlock), бардык объекттерде бар. Ошондуктан, ылайыксыз объект тандалган болсо, сиз оңой эле туюкка учурасаңыз болот (мисалы, эгер сиз интернедик жипке бекитип алсаңыз). Бул үчүн биз ар дайым жашырылган объектти колдонобуз.

Шарт өзгөрмө үлгүсү кандайдыр бир татаал шарттын күтүүсүн кыскараак ишке ашырууга мүмкүндүк берет. .NET-жылы, менин оюмча, ал толук эмес, анткени теория боюнча, бир локто эмес, бир нече өзгөрмөлөр боюнча (Posix Threads сыяктуу) бир нече кезек болушу керек. Ошондо аларды бардык философтор үчүн жасоого болот. Бирок бул формада да кодду кыскартууга мүмкүндүк берет.

көптөгөн философтор же async / await

Макул, эми биз жиптерди натыйжалуу бөгөттөй алабыз. Бирок бизде философтор көп болсочу? 100? 10000? Мисалы, биз веб-серверге 100000 4 суроо-талаптарды алдык. Бул ар бир суроо үчүн жип түзүү ашыкча болот, анткени ушунчалык көп жиптер параллелдүү иштебейт. Логикалык өзөктөр канчалык көп болсо, ошончо гана иштейт (менде XNUMX бар). Ал эми башкалардын баары ресурстарды тартып алышат. Бул маселени чечүүнүн бири - асинхрондоштуруу/күтүү үлгүсү. Анын идеясы, эгер бир нерсенин уланышын күтүш керек болсо, функция жипти кармабайт. Жана ал бир нерсе кылганда, анын аткарылышын улантат (бирок ошол эле жипте сөзсүз эмес!). Биздин учурда айрыны күтөбүз.

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. Ал аркылуу сиз ыкманын бүтүшүн күтө аласыз, аны жокко чыгара аласыз жана Тапшырма менен жасай турган нерселердин бардыгын жасай аласыз. Методдун ичинде мамлекеттик машина аткарылышын көзөмөлдөйт. Жыйынтык, эгерде кечигүү болбосо, анда аткаруу синхрондуу, ал эми бар болсо, жип бошотулат. Муну жакшыраак түшүнүү үчүн бул мамлекеттик машинаны карап көргөнүбүз оң. Булардан чынжырларды түзө аласыз async / await ыкмалары.

сынап көрөлү. 100 философтун 4 логикалык өзөгү бар машинада иштөөсү, 8 секунд. Монитор менен мурунку чечим биринчи 4 жипти гана иштетип, калгандары такыр иштеген жок. Бул 4 жиптин ар бири болжол менен 2 мс бош турган. Ал эми асинхрондуу / күтүү чечими 100дүн бардыгын иштетип, ар биринин орточо күтүүсү 6.8 секунд. Албетте, реалдуу системаларда 6 секунд бош туруп калууга жол берилбейт жана ушуга окшогон көптөгөн суроо-талаптарды иштебеген жакшы. Монитор менен чечим такыр масштабдуу эмес болуп чыкты.

жыйынтыктоо

Бул кичинекей мисалдардан көрүнүп тургандай, .NET көптөгөн синхрондоштуруу конструкцияларын колдойт. Бирок, аларды кантип колдонуу дайыма эле ачык-айкын боло бербейт. Бул макала пайдалуу болду деп үмүттөнөм. Азырынча бул бүттү, бирок дагы эле көп кызыктуу нерселер калды, мисалы, жип менен коопсуз коллекциялар, TPL Dataflow, Реактивдүү программалоо, Программалык камсыздоо транзакциясынын модели ж.б.

булактар

Source: www.habr.com

Комментарий кошуу