వెల్-ఫెడ్ ఫిలాసఫర్స్ లేదా కాంపిటేటివ్ .NET ప్రోగ్రామింగ్

వెల్-ఫెడ్ ఫిలాసఫర్స్ లేదా కాంపిటేటివ్ .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 తరగతి ఒకటే 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 (ఇది అక్కడ ఉపయోగించబడుతుంది). ఇప్పుడు వేచి ఉన్నవారిని ఎలా లెక్కించాలో, వారిని కొంచెం నిద్రలోకి మరియు మరిన్నింటిని ఎలా లెక్కించాలో అతనికి తెలుసు. మొదలైనవి సాధారణంగా, ఆప్టిమైజ్ చేయడానికి సాధ్యమయ్యే ప్రతిదాన్ని చేస్తుంది. అయితే ఇది ఇప్పటికీ అదే యాక్టివ్ సైకిల్ అని గుర్తుంచుకోవాలి, ఇది ప్రాసెసర్ వనరులను తినేస్తుంది మరియు ప్రవాహాన్ని ఉంచుతుంది, ఇది తత్వవేత్తలలో ఒకరు ఇతరులకన్నా ఎక్కువ ప్రాధాన్యతనిస్తే ఆకలికి దారి తీస్తుంది, కానీ గోల్డెన్ ఫోర్క్ లేకపోతే (ప్రాధాన్యత విలోమ సమస్య) . అందువల్ల, మేము భాగస్వామ్య మెమరీలో చాలా తక్కువ మార్పుల కోసం మాత్రమే దీన్ని ఉపయోగిస్తాము, ఎటువంటి మూడవ పక్ష కాల్‌లు, సమూహ తాళాలు మరియు ఇతర ఆశ్చర్యకరమైనవి లేకుండా.

వెల్-ఫెడ్ ఫిలాసఫర్స్ లేదా కాంపిటేటివ్ .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 ఉన్నాయి) ఉన్నన్ని మాత్రమే అమలు అవుతుంది. మరియు ప్రతి ఒక్కరూ కేవలం వనరులను తీసివేస్తారు. ఈ సమస్యకు ఒక పరిష్కారం అసమకాలీకరణ / వేచి ఉండే నమూనా. ఏదైనా కొనసాగడం కోసం వేచి ఉండాల్సిన అవసరం ఉన్నట్లయితే, ఫంక్షన్ థ్రెడ్‌ను కలిగి ఉండదని దీని ఆలోచన. మరియు అది ఏదైనా చేసినప్పుడు, అది దాని అమలును పునఃప్రారంభిస్తుంది (కానీ అదే థ్రెడ్‌లో అవసరం లేదు!). మా విషయంలో, మేము ఫోర్క్ కోసం వేచి ఉంటాము.

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

ఒక వ్యాఖ్యను జోడించండి