چڱي طرح فيلسوف يا مقابلي واري .NET پروگرامنگ

چڱي طرح فيلسوف يا مقابلي واري .NET پروگرامنگ

اچو ته ڏسون ته .Net ۾ سمورو ۽ متوازي پروگرامنگ ڪيئن ڪم ڪري ٿي، لنچنگ فلسفي جي مسئلي جو مثال استعمال ڪندي. منصوبي هيٺ ڏنل آهي، ڌاڳو / پروسيس جي هم وقت سازي کان اداڪار ماڊل تائين (هيٺ ڏنل حصن ۾). مضمون شايد پهرين واقفيت لاءِ ڪارائتو ٿي سگهي ٿو يا توهان جي علم کي تازو ڪرڻ لاءِ.

ڇو ته اهو ڪيئن ڪجي؟ ٽرانزيسٽر پنهنجي گھٽ ۾ گھٽ سائيز تائين پهچي رهيا آهن، مور جو قانون روشني جي رفتار جي حد تائين پهچي رهيو آهي، ۽ ان ڪري انگن ۾ واڌارو ڏٺو وڃي ٿو؛ وڌيڪ ٽرانزيسٽر ٺاهي سگھجن ٿا. ساڳئي وقت، ڊيٽا جو مقدار وڌي رهيو آهي، ۽ صارفين کي سسٽم کان فوري جواب جي توقع آهي. اهڙي صورتحال ۾، "عام" پروگرامنگ، جڏهن اسان وٽ هڪ عمل ڪرڻ وارو موضوع آهي، وڌيڪ اثرائتو ناهي. اسان کي ڪنهن به طرح سان گڏ يا هڪجهڙائي واري عمل جي مسئلي کي حل ڪرڻ جي ضرورت آهي. ان کان علاوه، هي مسئلو مختلف سطحن تي موجود آهي: موضوع جي سطح تي، پروسيس جي سطح تي، نيٽ ورڪ تي مشينن جي سطح تي (ورهايل سسٽم). .NET وٽ اعليٰ معيار جي، وقت تي آزمائشي ٽيڪنالاجيون آهن، جيئن اهڙن مسئلن کي جلدي ۽ موثر طريقي سان حل ڪرڻ لاءِ.

مقصد

Edsger Dijkstra اهو مسئلو 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، پر سڀني قسمن جي سهولتن سان: ٻين ڪمن جي بلاڪ کان پوءِ هڪ ڪم شروع ڪرڻ جي صلاحيت، انهن کي ڪمن مان واپس آڻيو، آسانيءَ سان انهن کي روڪيو، ۽ گهڻو ڪجهه. وغيره. انهن کي سپورٽ ڪرڻ جي ضرورت آهي async/انتظار جي تعميرات (ٽاسڪ جي بنياد تي Asynchronous پيٽرن، IO عملن جي انتظار لاءِ syntactic شوگر). اسان ان بابت بعد ۾ ڳالهائينداسين.

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 پروگرامنگ

تصوير ۾، موضوعن کي بلاڪ ڪرڻ (deadlock). سائو اشارو ڏئي ٿو عملدرآمد، ڳاڙهو اشارو هم وقت سازي، ۽ گرين اشارو ڏئي ٿو ڌاڳو سمهي رهيو آهي. هيرن ٽاسڪ جي لانچ جو وقت ظاهر ڪري ٿو.

فلاسفرن جي بک

جيتوڻيڪ توهان کي سوچڻ لاءِ تمام گهڻو کاڌ خوراڪ جي ضرورت ناهي، بک ڪنهن کي به فلسفي کي ڇڏي ڏيڻ تي مجبور ڪري سگهي ٿي. اچو ته ڪوشش ڪريون ته ٿريڊ اسٽوڊيوشن جي صورتحال کي اسان جي مسئلي ۾ سمائي. فاقو تڏهن ٿيندو آهي جڏهن ڪو ڌاڳو ڪم ڪري، پر ڪنهن خاص ڪم کان سواءِ، ٻين لفظن ۾، اهو ساڳيو تعطل آهي، رڳو هاڻي ڌاڳو سمهي نه رهيو آهي، پر فعال طور تي کائڻ لاءِ ڪجهه ڳولي رهيو آهي، پر کاڌو ناهي. بار بار بلاڪ ٿيڻ کان بچڻ لاءِ، اسان ڪانٽو واپس رکي ڇڏينداسين جيڪڏهن اسان ٻيو ڪو نه وٺي سگهون.

// То же что и в 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 (جيڪو اتي استعمال ٿيندو آهي). هاڻي هو ڄاڻي ٿو ته انهن انتظار ڪندڙن کي ڪيئن ڳڻيو، انهن کي ٿوري ننڊ ۾ وجهي، ۽ گهڻو ڪجهه. وغيره. عام طور تي، اهو بهتر ڪرڻ لاءِ هر ممڪن ڪوشش ڪندو آهي. پر اسان کي ياد رکڻ گهرجي ته اهو اڃا تائين ساڳيو فعال لوپ آهي جيڪو پروسيسر وسيلن کي کائي ٿو ۽ هڪ ڌاڳو رکي ٿو، جيڪو بکيو ٿي سگهي ٿو جيڪڏهن هڪ فلسفي کي ٻين کان وڌيڪ ترجيح ڏني وڃي، پر سونهن ڪانٽو نه آهي (ترجيح جي ڦيرڦار جو مسئلو. ). تنهن ڪري، اسان ان کي استعمال ڪريون ٿا صرف گڏيل ياداشت ۾ تمام ٿوري تبديلين لاءِ، بغير ڪنهن ٽئين پارٽي جي ڪالن، نيسٽ ٿيل لاڪ، يا ٻين تعجب جي.

چڱي طرح فيلسوف يا مقابلي واري .NET پروگرامنگ

ڊرائنگ لاءِ SpinLock. سونهري ڪانٽو لاءِ وهڪرو مسلسل ”وڙهندا“ رهيا آهن. ناڪاميون ٿينديون آهن - شڪل ۾ نمايان ٿيل علائقو. ڪور مڪمل طور تي استعمال نه ڪيا ويا آهن: صرف اٽڪل 2/3 انهن چئن موضوعن ذريعي.

ٻيو حل هتي صرف استعمال ڪرڻ لاء هوندو Interlocked.CompareExchange ساڳئي فعال انتظار سان جيئن مٿي ڏنل ڪوڊ ۾ ڏيکاريل آهي (بخاري فلاسفرن ۾)، پر اهو، جيئن اڳ ۾ چيو ويو آهي، نظرياتي طور تي بلاڪ ٿي سگهي ٿو.

تي Interlocked اهو چوڻ جي قابل آهي ته اتي ئي نه آهي CompareExchange، پر ايٽمي پڙهڻ ۽ لکڻ لاءِ ٻيا طريقا پڻ. ۽ تبديلي کي ورجائڻ سان، جيڪڏهن ڪو ٻيو ٿريڊ پنهنجون تبديليون ڪرڻ جو انتظام ڪري ٿو (پڙهڻ 1، پڙهو 2، لکو 2، لکڻ 1 خراب آهي)، اهو استعمال ڪري سگهجي ٿو پيچيده تبديلين لاءِ هڪ قدر ۾ (Interlocked Anything pattern).

ڪرنل موڊ حل

لوپ ۾ وسيلن کي ضايع ڪرڻ کان بچڻ لاء، اچو ته ڏسو ته ڪيئن هڪ موضوع کي بلاڪ ڪجي. ٻين لفظن ۾، اسان جي مثال کي جاري رکندي، اچو ته ڏسو ته ڪيئن انتظار ڪندڙ فلسفي کي ننڊ ۾ آڻيندو آهي ۽ صرف ضروري هجي ته کيس جاڳائي ٿو. پهرين، اچو ته ڏسو ته اهو ڪيئن ڪجي آپريٽنگ سسٽم جي ڪرنل موڊ ذريعي. اتي موجود سڀئي اڏاوتون اڪثر ڪري ختم ٿي وينديون آهن انهن جي ڀيٽ ۾ صارف جي جڳهه ۾. سست ڪيترائي ڀيرا، مثال طور AutoResetEvent شايد 53 ڀيرا سست SpinLock [ريڪٽر]. پر انهن جي مدد سان، توهان سڄي سسٽم ۾ عمل کي هم وقت سازي ڪري سگهو ٿا، منظم يا نه.

هتي جو بنيادي نمونو هڪ سيمفور آهي، جيڪو اڌ صدي کان به وڌيڪ اڳ Dijkstra پاران تجويز ڪيو ويو هو. هڪ سيمفور آهي، آسانيء سان، سسٽم طرفان ڪنٽرول هڪ مثبت عدد، ۽ ان تي ٻه آپريشن - وڌاء ۽ گهٽتائي. جيڪڏهن اهو ممڪن ناهي ته صفر کي گهٽائڻ، پوء ڪالنگ موضوع کي بلاڪ ڪيو ويو آهي. جڏهن تعداد ڪنهن ٻئي فعال ٿريڊ/پروسيس ذريعي وڌايو ويندو آهي، ته پوءِ ٿريڊ پاس ڪيا ويندا آهن ۽ سيمفور ٻيهر پاس ٿيل نمبر کان گهٽجي ويندو آهي. توهان تصور ڪري سگهو ٿا ٽرينن کي هڪ رڪاوٽ ۾ هڪ سيمفور سان. .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، ڇاڪاڻ ته سي # ۾ هڪ مشهور آهي 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 موضوعن ۾) هجڻ گهرجي، ۽ هڪ تالا تي نه. پوء اهو ممڪن ٿيندو ته انهن کي سڀني فلسفين لاء ٺاهيو وڃي. پر اڃا به هن فارم ۾ اهو توهان کي ڪوڊ ننڍو ڪرڻ جي اجازت ڏئي ٿو.

ڪيترائي فيلسوف يا async / await

ٺيڪ آهي، هاڻي اسان مؤثر طريقي سان موضوعن کي بلاڪ ڪري سگهون ٿا. پر ڇا جيڪڏهن اسان وٽ ڪيترائي فيلسوف آهن؟ 100؟ 10000؟ مثال طور، اسان ويب سرور تي 100000 درخواستون وصول ڪيون. هر درخواست لاء هڪ موضوع ٺاهڻ مهانگو ٿيندو، ڇاڪاڻ ته تمام گھڻا سلسلا متوازي طور تي عمل نه ڪيا ويندا. صرف ڪيترا ئي منطقي ڪور تي عمل ڪيو ويندو (مون وٽ 4 آهي). ۽ ٻيو هرڪو صرف وسيلا کڻي ويندا. ھن مسئلي جو ھڪڙو حل آھي async / انتظار جو نمونو. ان جو خيال اهو آهي ته هڪ فنڪشن هڪ موضوع نه رکندو آهي جيڪڏهن ان کي جاري رکڻ لاء ڪجهه انتظار ڪرڻ جي ضرورت آهي. ۽ جڏهن ڪجهه ٿئي ٿو، اهو ان جي عمل کي ٻيهر شروع ڪري ٿو (پر ضروري ناهي ته ساڳئي سلسلي ۾!). اسان جي حالت ۾، اسان هڪ ڪانٽو جو انتظار ڪنداسين.

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 موضوعن مان هر هڪ اٽڪل 2ms لاءِ بيڪار هو. ۽ async / انتظار جو حل سڀ 100 ڪيو، اوسط 6.8 سيڪنڊن سان هر هڪ انتظار. يقينا، حقيقي سسٽم ۾، 6 سيڪنڊن لاء بيڪار ٿيڻ ناقابل قبول آهي ۽ اهو بهتر ناهي ته هن طريقي سان ڪيتريون ئي درخواستون پروسيس نه ڪيون وڃن. مانيٽر سان حل اهو نڪتو ته سڀ ڪجهه اسڪيلبل نه آهي.

ٿڪل

جيئن توهان انهن ننڍڙن مثالن مان ڏسي سگهو ٿا، .NET ڪيترن ئي هم وقت سازي جي تعمير کي سپورٽ ڪري ٿو. بهرحال، اهو هميشه واضح ناهي ته انهن کي ڪيئن استعمال ڪجي. مون کي اميد آهي ته هي مضمون مددگار هو. اسان هن وقت تائين ان کي ختم ڪري رهيا آهيون، پر اڃا تائين تمام گهڻيون دلچسپ شيون رهجي ويون آهن، مثال طور، ٿريڊ-سيف مجموعا، TPL Dataflow، Reactive programming، Software Transaction model، وغيره.

ذريعو

جو ذريعو: www.habr.com

تبصرو شامل ڪريو