හොඳින් පෝෂණය වූ දාර්ශනිකයන් හෝ තරඟකාරී .NET වැඩසටහන්කරණය

හොඳින් පෝෂණය වූ දාර්ශනිකයන් හෝ තරඟකාරී .NET වැඩසටහන්කරණය

Philosophers Dining Problem උදාහරණයක් ලෙස යොදා ගනිමින් .Net හි සමගාමී සහ සමාන්තර ක්‍රමලේඛනය ක්‍රියා කරන ආකාරය බලමු. සැලැස්ම මෙයයි, නූල් / ක්‍රියාවලි සමමුහුර්ත කිරීමේ සිට නළු ආකෘතිය දක්වා (පහත කොටස් වලින්). ලිපිය පළමු හඳුනන අය සඳහා හෝ ඔබේ දැනුම ප්රබෝධමත් කිරීම සඳහා ප්රයෝජනවත් විය හැකිය.

එය කිසිසේත් කරන්නේ ඇයි? ට්‍රාන්සිස්ටර ඒවායේ අවම ප්‍රමාණයට ළඟා වේ, මුවර්ගේ නියමය ආලෝකයේ වේගය සීමා කිරීම මත රඳා පවතී, එබැවින් සංඛ්‍යාවේ වැඩි වීමක් දක්නට ලැබේ, තවත් ට්‍රාන්සිස්ටර සෑදිය හැක. ඒ සමගම, දත්ත ප්රමාණය වර්ධනය වන අතර, පරිශීලකයන් පද්ධති වෙතින් ක්ෂණික ප්රතිචාරයක් අපේක්ෂා කරයි. එවැනි තත්වයක් තුළ, "සාමාන්ය" ක්රමලේඛනය, අපට එක් ක්රියාත්මක වන නූල් ඇති විට, තවදුරටත් ඵලදායී නොවේ. එකවර හෝ සමගාමීව ක්‍රියාත්මක කිරීමේ ගැටලුව ඔබ කෙසේ හෝ විසඳා ගත යුතුය. එපමණක් නොව, මෙම ගැටළුව විවිධ මට්ටම්වල පවතී: නූල් මට්ටමින්, ක්රියාවලි මට්ටමින්, ජාලයේ යන්ත්ර මට්ටමින් (බෙදා හරින ලද පද්ධති). .NET හි එවැනි ගැටළු ඉක්මනින් හා කාර්යක්ෂමව විසඳීම සඳහා උසස් තත්ත්වයේ, කාලය පරීක්ෂා කරන ලද තාක්ෂණයන් ඇත.

අරමුණු

Edsger Dijkstra විසින් 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 class එක සමානයි 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();
    }
}

මෙන්න අපි මුලින්ම වම් දෙබලක ගැනීමට උත්සාහ කරමු, පසුව දකුණු දෙබලක ගන්න, එය සාර්ථක වුවහොත්, අපි ඒවා අනුභව කර ආපසු තබමු. එක් දෙබලක ගැනීම පරමාණුක වේ, i.e. නූල් දෙකකට එකවර එකක් ගත නොහැක (වැරදියි: දෙබලක නොමිලේ බව පළමු කියවයි, දෙවැන්න - ද, පළමු ගැනීම, දෙවැන්න ගනී). මේ වෙනුවෙන් 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 සහ ආදිය). එබැවින්, නූල්වලම සහ අලංකාර අවසන් කිරීම සමඟ දෝෂ හසුරුවන්න අවශ්ය වේ.

වේටර්

හරි, අපි කොහොමද මේ හිරවීම, කුසගින්න සහ මරණ ප්‍රශ්නය විසඳන්නේ? අපි එක් දාර්ශනිකයෙකුට පමණක් දෙබලකට ළඟා වීමට ඉඩ දෙන්නෙමු, මෙම ස්ථානය සඳහා නූල්වල අන්‍යෝන්‍ය බැහැර කිරීමක් එක් කරන්න. එය කරන්නේ කෙසේද? ඕනෑම දාර්ශනිකයෙකුට ගෑරුප්පු ගැනීමට අවසර දෙන දාර්ශනිකයන් අසල වේටර්වරයකු සිටගෙන සිටී යැයි සිතමු. අපි මෙම වේටර්වරයා කරන්නේ කෙසේද සහ දාර්ශනිකයින් ඔහුගෙන් අසන්නේ කෙසේද, ප්‍රශ්න සිත්ගන්නා සුළුය.

සරලම ක්‍රමය නම් දාර්ශනිකයන් විසින් ගෑරුප්පු වෙත ප්‍රවේශ වීම සඳහා වේටර්වරයාගෙන් නිරන්තරයෙන් අසනු ඇත. එම. දැන් දාර්ශනිකයන් අසල දෙබලක් එනතෙක් බලා නොසිටිනු ඇත, නමුත් බලා සිටින්න හෝ වේටර්ගෙන් විමසන්න. මුලදී, අපි මේ සඳහා භාවිතා කරන්නේ පරිශීලක අවකාශය පමණි, එහි අපි කර්නලයෙන් කිසිදු ක්‍රියා පටිපාටියක් ඇමතීමට බාධා කිරීම් භාවිතා නොකරමු (පහත ඒවා ගැන).

පරිශීලක අවකාශයේ විසඳුම්

මෙතනදි අපි එක ගෑරුප්පුවකුයි දාර්ශනිකයො දෙන්නකුයි කරන විදියටම චක්‍රයක් කරකවලා බලාගෙන ඉන්නවා. නමුත් දැන් එය සියලු දාර්ශනිකයින් වනු ඇති අතර, එය මෙන්, එකම දෙබලක, i.e. මෙම "රන් දෙබල" වේටර්ගෙන් ගත් දාර්ශනිකයා පමණක් අනුභව කරන බව පැවසිය හැකිය. මේ සඳහා අපි 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 (එහි භාවිතා වේ). දැන් ඔහු බලා සිටින අය ගණන් කිරීමට, ඔවුන් ටිකක් නිදා ගැනීමට සහ තවත් බොහෝ දේ දනී. ආදිය පොදුවේ, ප්රශස්ත කිරීමට හැකි සෑම දෙයක්ම කරයි. නමුත් මෙය තවමත් ප්‍රොසෙසර සම්පත් අනුභව කරන සහ ප්‍රවාහය පවත්වා ගෙන යන ක්‍රියාකාරී චක්‍රය බව අප මතක තබා ගත යුතුය, එක් දාර්ශනිකයෙකු අනෙක් අයට වඩා ප්‍රමුඛත්වය ලබා ගන්නේ නම් කුසගින්න ඇති විය හැකි නමුත් රන් දෙබලක් නොමැති නම් (ප්‍රමුඛතා ප්‍රතිලෝම ගැටළුව) . එමනිසා, අපි එය භාවිතා කරන්නේ කිසිදු තෙවන පාර්ශවීය ඇමතුම්, කැදලි අගුලු සහ වෙනත් විස්මයන් නොමැතිව බෙදාගත් මතකයේ ඉතා කෙටි වෙනස්කම් සඳහා පමණි.

හොඳින් පෝෂණය වූ දාර්ශනිකයන් හෝ තරඟකාරී .NET වැඩසටහන්කරණය

සඳහා ඇඳීම SpinLock. ධාරා රන් දෙබල සඳහා නිරන්තරයෙන් "සටන්" කරයි. අසාර්ථක වීම් ඇත - රූපයේ, තෝරාගත් ප්රදේශය. හරය සම්පූර්ණයෙන්ම භාවිතා නොවේ: මෙම නූල් හතරෙන් 2/3 ක් පමණ වේ.

මෙහි තවත් විසඳුමක් වනුයේ භාවිතා කිරීම පමණි Interlocked.CompareExchange ඉහත කේතයේ පෙන්වා ඇති ආකාරයටම ක්‍රියාකාරී බලා සිටීමෙන් (කුසගින්නෙන් පෙළෙන දාර්ශනිකයන් තුළ), නමුත් මෙය දැනටමත් පවසා ඇති පරිදි න්‍යායාත්මකව අවහිර කිරීමට හේතු විය හැක.

මත Interlocked පමණක් නොවන බව සඳහන් කළ යුතුය CompareExchange, නමුත් පරමාණුක කියවීම සහ ලිවීම සඳහා වෙනත් ක්රම. වෙනස් කිරීම පුනරාවර්තනය කිරීම හරහා, වෙනත් නූලට එහි වෙනස්කම් කිරීමට කාලය තිබේ නම් (1 කියවන්න, 2 කියවන්න, ලියන්න 2, ලියන්න 1 නරකයි), එය තනි අගයකට සංකීර්ණ වෙනස්කම් සඳහා භාවිතා කළ හැකිය (Interlocked Anything pattern) .

කර්නල් මාදිලියේ විසඳුම්

ලූප් එකක සම්පත් නාස්ති වීම වළක්වා ගැනීම සඳහා, අපි නූල් අවහිර කරන්නේ කෙසේදැයි බලමු. වෙනත් වචන වලින් කිවහොත්, අපගේ උදාහරණය දිගටම කරගෙන යමින්, වේටර්වරයා දාර්ශනිකයා නිදි කරවන ආකාරය සහ අවශ්‍ය විට පමණක් ඔහුව අවදි කරන්නේ කෙසේදැයි බලමු. පළමුව, මෙහෙයුම් පද්ධතියේ කර්නල් මාදිලිය හරහා මෙය කරන්නේ කෙසේදැයි බලමු. එහි ඇති සියලුම ව්‍යුහයන් බොහෝ විට පරිශීලක අවකාශයේ ඇති ඒවාට වඩා මන්දගාමී වේ. උදාහරණයක් ලෙස, කිහිප වතාවක් මන්දගාමී වේ AutoResetEvent සමහර විට 53 ගුණයකින් මන්දගාමී වේ SpinLock [රිච්ටර්]. නමුත් ඔවුන්ගේ සහාය ඇතිව, ඔබට පද්ධතිය පුරා ක්‍රියාවලි සමමුහුර්ත කළ හැකිය, කළමනාකරණය කළත් නැතත්.

මෙහි මූලික නිර්මිතය වන්නේ අඩ සියවසකට පෙර Dijkstra විසින් යෝජනා කරන ලද semaphore ය. semaphore යනු, සරලව කිවහොත්, පද්ධතිය විසින් කළමනාකරණය කරනු ලබන ධන නිඛිලයක් වන අතර, එය මත ක්‍රියාවන් දෙකක්, වැඩි කිරීම සහ අඩු කිරීම. එය අඩු කිරීමට අපොහොසත් වුවහොත්, ශුන්‍යය, එවිට ඇමතුම් නූල් අවහිර වේ. වෙනත් සක්‍රිය නූල්/ක්‍රියාවලියකින් සංඛ්‍යාව වැඩි කළ විට, නූල් මඟ හරිනු ලබන අතර සම්මත කළ සංඛ්‍යාවෙන් සෙමාෆෝරය නැවත අඩු වේ. සෙමාෆෝරයක් සහිත බෝතල් බෙල්ලක දුම්රිය ගැන කෙනෙකුට සිතාගත හැකිය. .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 (mutex) උපරිම අගයක් සහිත එකම semaphore වේ, නමුත් ලූපයක රැඳී සිටීම සඳහා සහය ඇතිව, පුනරාවර්තනය, තත්ත්ව විචල්‍ය රටාව (පහත එය ගැන වැඩි විස්තර) යනාදිය. අපි එය සමඟ විසඳුමක් දෙස බලමු.

// Спрячем объект для Монитора от всех, чтобы без дедлоков.
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 නූල්වල මෙන්) පෝලිම් කිහිපයක් තිබිය යුතු අතර, එක් ලොක් මත නොවේ. එවිට කෙනෙකුට ඒවා සියලු දාර්ශනිකයන් සඳහා සෑදිය හැකිය. නමුත් මෙම ආකෘතියේ පවා, කේතය අඩු කිරීමට ඔබට ඉඩ සලසයි.

බොහෝ දාර්ශනිකයන් හෝ async / await

හරි, දැන් අපට ඵලදායී ලෙස නූල් අවහිර කළ හැක. නමුත් අපට බොහෝ දාර්ශනිකයන් සිටී නම් කුමක් කළ යුතුද? 100? 10000? උදාහරණයක් ලෙස, අපට වෙබ් සේවාදායකයට ඉල්ලීම් 100000 ක් ලැබුණි. එක් එක් ඉල්ලීම සඳහා නූලක් සෑදීම සඳහා එය ඉහලින් වනු ඇත, මන්ද බොහෝ නූල් සමාන්තරව ධාවනය නොවේ. තාර්කික හරයන් ඇති තරම් පමණක් ධාවනය වනු ඇත (මා සතුව 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. එය හරහා, ඔබට ක්‍රමය අවසන් වන තෙක් බලා සිටිය හැකිය, එය අවලංගු කළ හැකිය, සහ ඔබට කාර්යය සමඟ කළ හැකි සියල්ල. ක්‍රමය ඇතුළත, රාජ්‍ය යන්ත්‍රය ක්‍රියාත්මක කිරීම පාලනය කරයි. අවසාන කරුණ නම් ප්‍රමාදයක් නොමැති නම්, ක්‍රියාත්මක කිරීම සමමුහුර්ත වන අතර, තිබේ නම්, නූල් මුදා හරිනු ලැබේ. මේ පිළිබඳ වඩා හොඳ අවබෝධයක් සඳහා, මෙම රාජ්ය යන්ත්රය දෙස බැලීම වඩා හොඳය. මේවායින් දාම සෑදිය හැක async / await ක්රම.

අපි ටෙස්ට් කරමු. තාර්කික හර 100ක් සහිත යන්ත්‍රයක දාර්ශනිකයන් 4 දෙනෙකුගේ වැඩ තත්පර 8 යි. Monitor සමඟ පෙර විසඳුම පළමු නූල් 4 පමණක් ධාවනය වූ අතර ඉතිරිය කිසිසේත් ධාවනය නොවීය. මෙම නූල් 4 න්ම 2ms පමණ අක්‍රියව පැවතුනි. සාමාන්‍ය තත්පර 100 බැගින් රැඳී සිටීමත් සමඟ අසමමුහුර්ත / බලා සිටීමේ විසඳුම 6.8ම ධාවනය විය. ඇත්ත වශයෙන්ම, සැබෑ පද්ධතිවල තත්පර 6 ක් අයිඩල් කිරීම පිළිගත නොහැකි අතර මෙවැනි බොහෝ ඉල්ලීම් සැකසීමට ඉඩ නොදීම වඩා හොඳය. මොනිටරය සමඟ ඇති විසඳුම කිසිසේත් පරිමාණය කළ නොහැකි බව පෙනී ගියේය.

නිගමනය

මෙම කුඩා උදාහරණ වලින් ඔබට පෙනෙන පරිදි, .NET බොහෝ සමමුහුර්ත ඉදිකිරීම් සඳහා සහය දක්වයි. කෙසේ වෙතත්, ඒවා භාවිතා කරන්නේ කෙසේද යන්න සැමවිටම පැහැදිලි නැත. මෙම ලිපිය ප්රයෝජනවත් වූ බව මම බලාපොරොත්තු වෙමි. දැනට, මෙය අවසානයයි, නමුත් තවමත් රසවත් දේවල් ගොඩක් ඉතිරිව ඇත, උදාහරණයක් ලෙස, නූල්-ආරක්ෂිත එකතු කිරීම්, TPL Dataflow, Reactive programming, Software Transaction model, ආදිය.

මුලාශ්‍ර

මූලාශ්රය: www.habr.com

අදහස් එක් කරන්න