اچھی طرح سے کھلایا فلاسفرز یا .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/ انتظار کی تعمیرات کی حمایت کرنے کی ضرورت ہے (ٹاسک پر مبنی غیر مطابقت پذیر پیٹرن، 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 (جو وہاں استعمال ہوتا ہے)۔ اب وہ جانتا ہے کہ انتظار کرنے والوں کو کیسے گننا ہے، انہیں تھوڑا سونا ہے، اور بہت کچھ۔ وغیرہ۔ عام طور پر، یہ اصلاح کے لیے ہر ممکن کوشش کرتا ہے۔ لیکن ہمیں یاد رکھنا چاہیے کہ یہ اب بھی وہی فعال لوپ ہے جو پروسیسر کے وسائل کو کھاتا ہے اور ایک دھاگہ رکھتا ہے، جو بھوک کا باعث بن سکتا ہے اگر فلسفیوں میں سے ایک دوسرے کے مقابلے میں زیادہ ترجیح بن جائے، لیکن اس کے پاس سنہری کانٹا نہ ہو (ترجیحی الٹا مسئلہ )۔ لہذا، ہم اسے صرف مشترکہ میموری میں بہت ہی مختصر تبدیلیوں کے لیے استعمال کرتے ہیں، بغیر کسی تھرڈ پارٹی کالز، نیسٹڈ لاک، یا دیگر سرپرائزز کے۔

اچھی طرح سے کھلایا فلاسفرز یا .NET میں مسابقتی پروگرامنگ

کے لیے ڈرائنگ SpinLock. نہریں سنہری کانٹے کے لیے مسلسل "لڑائی" کر رہی ہیں۔ ناکامیاں ہوتی ہیں - تصویر میں نمایاں کردہ علاقہ۔ کور مکمل طور پر استعمال نہیں ہوئے ہیں: ان چار دھاگوں سے صرف 2/3۔

یہاں ایک اور حل صرف استعمال کرنا ہوگا۔ Interlocked.CompareExchange اسی فعال انتظار کے ساتھ جیسا کہ اوپر کوڈ میں دکھایا گیا ہے (بھوک سے مرنے والے فلسفیوں میں)، لیکن یہ، جیسا کہ پہلے ہی کہا گیا ہے، نظریاتی طور پر بلاکنگ کا باعث بن سکتا ہے۔

کے بارے میں Interlocked یہ کہنے کے قابل ہے کہ وہاں صرف نہیں ہے۔ CompareExchange، بلکہ ایٹمی پڑھنے اور لکھنے کے دوسرے طریقے بھی۔ اور تبدیلی کو دہرانے سے، اگر کوئی دوسرا تھریڈ اپنی تبدیلیاں کرنے کا انتظام کرتا ہے (پڑھیں 1، پڑھیں 2، لکھیں 2، لکھیں 1 خراب ہے)، اسے ایک قدر میں پیچیدہ تبدیلیوں کے لیے استعمال کیا جا سکتا ہے (انٹرلاکڈ اینیتھنگ پیٹرن)۔

کرنل موڈ حل

ایک لوپ میں وسائل کو ضائع کرنے سے بچنے کے لیے، آئیے دیکھتے ہیں کہ تھریڈ کو کیسے بلاک کیا جائے۔ دوسرے لفظوں میں، اپنی مثال کو جاری رکھتے ہوئے، آئیے دیکھتے ہیں کہ ویٹر کس طرح فلسفی کو نیند میں ڈالتا ہے اور ضرورت کے وقت ہی اسے جگاتا ہے۔ سب سے پہلے، آئیے دیکھتے ہیں کہ آپریٹنگ سسٹم کے کرنل موڈ کے ذریعے یہ کیسے کیا جائے۔ وہاں کے تمام ڈھانچے اکثر صارف کی جگہ کے مقابلے سست ہوتے ہیں۔ مثال کے طور پر کئی بار آہستہ 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 میں یہ نامکمل ہے، میری رائے میں، کیونکہ... نظریہ میں، متعدد متغیرات پر کئی قطاریں ہونی چاہئیں (جیسا کہ پوسکس تھریڈز میں)، نہ کہ ایک تالا پر۔ پھر ان کو تمام فلسفیوں کے لیے بنانا ممکن ہو گا۔ لیکن اس فارم میں بھی یہ آپ کو کوڈ کو مختصر کرنے کی اجازت دیتا ہے۔

بہت سے فلسفی یا async / await

ٹھیک ہے، اب ہم تھریڈز کو مؤثر طریقے سے روک سکتے ہیں۔ لیکن کیا ہوگا اگر ہمارے پاس بہت سارے فلسفی ہوں؟ 100؟ 10000؟ مثال کے طور پر، ہمیں ویب سرور پر 100000 درخواستیں موصول ہوئیں۔ ہر درخواست کے لیے تھریڈ بنانا مہنگا پڑے گا، کیونکہ بہت سارے دھاگوں کو متوازی طور پر نہیں چلایا جائے گا۔ صرف اتنے ہی منطقی کوروں پر عمل درآمد کیا جائے گا (میرے پاس 4 ہیں)۔ اور باقی سب صرف وسائل چھین لیں گے۔ اس مسئلے کا ایک حل async/await پیٹرن ہے۔ اس کا خیال یہ ہے کہ اگر کسی فنکشن کو کسی چیز کے جاری رہنے کا انتظار کرنے کی ضرورت ہو تو وہ تھریڈ نہیں رکھتا ہے۔ اور جب کچھ ہوتا ہے، تو اس پر عمل درآمد دوبارہ شروع ہو جاتا ہے (لیکن ضروری نہیں کہ ایک ہی دھاگے میں ہو!)۔ ہمارے معاملے میں، ہم ایک کانٹے کا انتظار کریں گے۔

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/await حل نے تمام 100 کام کیے، اوسطاً 6.8 سیکنڈ ہر انتظار کے ساتھ۔ بلاشبہ، حقیقی نظاموں میں، 6 سیکنڈ تک بیکار رہنا ناقابل قبول ہے اور اس طرح اتنی زیادہ درخواستوں پر کارروائی نہ کرنا بہتر ہے۔ مانیٹر کے ساتھ حل بالکل بھی قابل توسیع نہیں نکلا۔

حاصل يہ ہوا

جیسا کہ آپ ان چھوٹی مثالوں سے دیکھ سکتے ہیں، .NET بہت سی ہم آہنگی کی تعمیر کو سپورٹ کرتا ہے۔ تاہم، یہ ہمیشہ واضح نہیں ہے کہ ان کا استعمال کیسے کریں. مجھے امید ہے کہ یہ مضمون مددگار تھا۔ ہم ابھی اسے سمیٹ رہے ہیں، لیکن ابھی بھی بہت ساری دلچسپ چیزیں باقی ہیں، مثلاً تھریڈ سیف کلیکشنز، ٹی پی ایل ڈیٹا فلو، ری ایکٹو پروگرامنگ، سافٹ ویئر ٹرانزیکشن ماڈل وغیرہ۔

ذرائع

ماخذ: www.habr.com

نیا تبصرہ شامل کریں