Жақсы тамақтандырылған философтар немесе бәсекеге қабілетті .NET бағдарламалау

Жақсы тамақтандырылған философтар немесе бәсекеге қабілетті .NET бағдарламалау

Философтардың тамақтану мәселесін мысал ретінде пайдаланып, .Net жүйесінде бір мезгілде және параллельді бағдарламалау қалай жұмыс істейтінін көрейік. Жоспар - бұл ағындарды/үдерістерді синхрондаудан бастап, актер моделіне дейін (келесі бөліктерде). Мақала бірінші танысу үшін немесе біліміңізді жаңарту үшін пайдалы болуы мүмкін.

Неліктен мұны істеу керек? Транзисторлар өздерінің минималды өлшеміне жетеді, Мур заңы жарық жылдамдығының шектелуіне негізделген, сондықтан санның өсуі байқалады, транзисторларды көбірек жасауға болады. Сонымен қатар, деректер көлемі өсіп келеді және пайдаланушылар жүйелерден дереу жауап күтеді. Мұндай жағдайда, бізде бір орындалатын ағын болған кезде «қалыпты» бағдарламалау енді тиімді болмайды. Бір мезгілде немесе бір мезгілде орындау мәселесін қандай да бір жолмен шешу керек. Сонымен қатар, бұл мәселе әртүрлі деңгейлерде бар: ағындар деңгейінде, процестер деңгейінде, желідегі машиналар деңгейінде (үлестірілген жүйелер). .NET-те мұндай мәселелерді тез және тиімді шешуге арналған жоғары сапалы, уақытпен тексерілген технологиялар бар.

Мақсаты

Эдгер Дейкстра бұл мәселені 1965 жылы өз студенттеріне қойды. Белгіленген тұжырымы келесідей. Философтардың белгілі (әдетте бес) саны және айыр саны бірдей. Олар дөңгелек үстелге отырады, олардың арасында шанышқылар бар. Философтар өз табақтарынан шексіз тамақ жей алады, ойлана алады немесе күте алады. Философты жеу үшін екі шанышқыны алу керек (соңғысы шанышқыны біріншімен бөліседі). Шанышқыны көтеру және қою екі бөлек әрекет. Барлық философтар үндемейді. Олардың барлығы 54 жылдан кейін де ойланып, толғанатындай алгоритмді табу міндеті тұр.

Алдымен, ортақ кеңістікті пайдалану арқылы бұл мәселені шешуге тырысайық. Шанышқылар ортақ үстелде жатыр, ал философтар оларды тұрған кезде алып, орнына қояды. Бұл жерде синхрондау проблемалары бар, субеттерді нақты қашан қабылдау керек? шанышқы болмаса ше? т.б. Бірақ алдымен философтардан бастайық.

Ағындарды бастау үшін біз жіп пулын пайдаланамыз 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.

Жақсы тамақтандырылған философтар немесе бәсекеге қабілетті .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 мәселесі) . Сондықтан біз оны үшінші тарап қоңыраулары, кірістірілген құлыптар және басқа тосын сыйларсыз ортақ жадтағы өте қысқа өзгерістер үшін ғана қолданамыз.

Жақсы тамақтандырылған философтар немесе бәсекеге қабілетті .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, тіпті ерекше жағдай болса да, бұл жағдайда басқа ағын ортақ жад күйін өзгерте алады. Мұндай жағдайларда көбінесе тығырықтан шығу немесе бағдарламаны қауіпсіз түрде тоқтату жақсы. Тағы бір таңқаларлық, Монитор синхрондау блоктарын пайдаланады (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 деректер ағыны, реактивті бағдарламалау, бағдарламалық жасақтама транзакциясы моделі және т.б.

Ақпарат көздері

Ақпарат көзі: www.habr.com

пікір қалдыру