ProHoster > blog > Utawala > Wanafalsafa Waliolishwa Vizuri au Utayarishaji wa NET
Wanafalsafa Waliolishwa Vizuri au Utayarishaji wa NET
Hebu tuone jinsi upangaji programu kwa wakati mmoja na sambamba unavyofanya kazi katika .Net, kwa kutumia Tatizo la Kula la Wanafalsafa kama mfano. Mpango ni huu, kutoka kwa maingiliano ya nyuzi / michakato, hadi mfano wa mwigizaji (katika sehemu zifuatazo). Nakala hiyo inaweza kuwa muhimu kwa marafiki wa kwanza au ili kuburudisha maarifa yako.
Kwa nini kufanya hivyo wakati wote? Transistors hufikia ukubwa wao wa chini, sheria ya Moore inategemea ukomo wa kasi ya mwanga na kwa hiyo ongezeko linazingatiwa kwa idadi, transistors zaidi yanaweza kufanywa. Wakati huo huo, kiasi cha data kinaongezeka, na watumiaji wanatarajia majibu ya haraka kutoka kwa mifumo. Katika hali kama hiyo, programu "ya kawaida", tunapokuwa na uzi mmoja wa kutekeleza, haifai tena. Unahitaji kwa namna fulani kutatua tatizo la utekelezaji wa wakati mmoja au wa wakati mmoja. Aidha, tatizo hili lipo katika viwango tofauti: katika ngazi ya nyuzi, katika ngazi ya taratibu, katika ngazi ya mashine katika mtandao (mifumo iliyosambazwa). NET ina ubora wa juu, teknolojia zilizojaribiwa kwa wakati kwa kutatua shida kama hizo haraka na kwa ufanisi.
Kazi
Edsger Dijkstra alitoa tatizo hili kwa wanafunzi wake mapema kama 1965. Uundaji ulioanzishwa ni kama ifuatavyo. Kuna idadi fulani (kawaida tano) ya wanafalsafa na idadi sawa ya uma. Wanakaa kwenye meza ya pande zote, uma kati yao. Wanafalsafa wanaweza kula kutoka kwa sahani zao za chakula kisicho na mwisho, kufikiri au kusubiri. Ili kula mwanafalsafa, unahitaji kuchukua uma mbili (wa mwisho anashiriki uma na wa kwanza). Kuokota na kuweka uma ni vitendo viwili tofauti. Wanafalsafa wote wako kimya. Kazi ni kupata algorithm ambayo wote wangefikiria na kuwa kamili hata baada ya miaka 54.
Kwanza, hebu tujaribu kutatua tatizo hili kwa kutumia nafasi ya pamoja. Uma ziko kwenye meza ya kawaida na wanafalsafa huzichukua tu zilipo na kuzirudisha. Hapa kuna matatizo na ulandanishi, ni wakati gani hasa wa kuchukua surebets? nini ikiwa hakuna uma? nk Lakini kwanza, tuanze wanafalsafa.
Ili kuanza nyuzi, tunatumia dimbwi la nyuzi kupitia Task.Run njia:
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);
}
Dimbwi la nyuzi limeundwa ili kuboresha uundaji na ufutaji wa nyuzi. Bwawa hili lina foleni na majukumu na CLR huunda au kuondoa nyuzi kulingana na idadi ya majukumu haya. Dimbwi moja kwa AppDomains zote. Bwawa hili linapaswa kutumika karibu kila mara, kwa sababu. hakuna haja ya kujisumbua na kuunda, kufuta nyuzi, foleni zao, nk. Inawezekana bila bwawa, lakini basi lazima uitumie moja kwa moja. Thread, hii ni muhimu kwa kesi wakati unahitaji kubadilisha kipaumbele cha thread, wakati tuna operesheni ndefu, kwa thread ya Foreground, nk.
Kwa maneno mengine, System.Threading.Tasks.Task darasa ni sawa Thread, lakini kwa kila aina ya urahisi: uwezo wa kuendesha kazi baada ya kizuizi cha kazi zingine, kuzirudisha kutoka kwa kazi, kuzisumbua kwa urahisi, na zaidi. n.k. Zinahitajika ili kusaidia miundo isiyolingana/kungoja (Mchoro wa Asynchronous unaotegemea Kazi, sukari ya kisintaksia kwa kusubiri shughuli za IO). Tutazungumza juu ya hili baadaye.
CancelationTokenSource hapa inahitajika ili thread iweze kukomesha yenyewe kwa ishara ya thread ya wito.
Masuala ya Usawazishaji
Wanafalsafa Waliozuiwa
Sawa, tunajua jinsi ya kuunda nyuzi, wacha tujaribu kula chakula cha mchana:
Hapa sisi kwanza tunajaribu kuchukua uma wa kushoto, na kisha uma wa kulia, na ikiwa inafanya kazi, basi tunakula na kuwaweka tena. Kuchukua uma moja ni atomiki, i.e. nyuzi mbili haziwezi kuchukua moja kwa wakati mmoja (sio sahihi: ya kwanza inasoma kwamba uma ni bure, pili - pia, ya kwanza inachukua, ya pili inachukua). Kwa hii; kwa hili Interlocked.CompareExchange, ambayo lazima itekelezwe kwa maagizo ya processor (TSL, XCHG), ambayo hufunga kipande cha kumbukumbu kwa usomaji na uandishi wa mpangilio wa atomiki. Na SpinWait ni sawa na ujenzi while(true) tu na "uchawi" kidogo - thread inachukua processor (Thread.SpinWait), lakini wakati mwingine huhamisha udhibiti kwa uzi mwingine (Thread.Yeild) au analala (Thread.Sleep).
Lakini suluhisho hili halifanyi kazi, kwa sababu mtiririko wa hivi karibuni (kwa ajili yangu ndani ya sekunde) umezuiwa: wanafalsafa wote huchukua uma wao wa kushoto, lakini sio sawa. Safu ya uma basi ina maadili: 1 2 3 4 5.
Katika takwimu, kuzuia threads (deadlock). Kijani - utekelezaji, nyekundu - maingiliano, kijivu - thread inalala. Rombuses zinaonyesha wakati wa kuanza kwa Kazi.
Njaa ya Wanafalsafa
Ingawa sio lazima kufikiria chakula kingi, lakini njaa hufanya mtu yeyote aache falsafa. Wacha tujaribu kuiga hali ya njaa ya nyuzi kwenye shida yetu. Njaa ni wakati thread inaendesha, lakini bila kazi kubwa, kwa maneno mengine, hii ni msuguano sawa, sasa tu thread haijalala, lakini inatafuta kwa bidii kitu cha kula, lakini hakuna chakula. Ili kuzuia kuzuia mara kwa mara, tutarudisha uma ikiwa hatukuweza kuchukua nyingine.
Jambo muhimu kuhusu kanuni hii ni kwamba wanafalsafa wawili kati ya wanne husahau kuweka chini uma wao wa kushoto. Na zinageuka kuwa wanakula chakula zaidi, wakati wengine wanaanza kufa na njaa, ingawa nyuzi zina kipaumbele sawa. Hapa hawana njaa kabisa, kwa sababu. wanafalsafa wabaya hurudisha uma zao nyuma wakati mwingine. Inageuka kuwa watu wazuri hula karibu mara 5 kuliko mbaya. Kwa hivyo hitilafu ndogo katika msimbo husababisha kushuka kwa utendaji. Pia ni muhimu kuzingatia hapa kwamba hali ya nadra inawezekana wakati wanafalsafa wote wanachukua uma wa kushoto, hakuna moja ya haki, wanaweka kushoto, kusubiri, kuchukua kushoto tena, nk. Hali hii pia ni njaa, zaidi kama mkwamo. Nilishindwa kurudia. Chini ni picha ya hali ambapo wanafalsafa wawili wabaya wamechukua uma na wawili wazuri wana njaa.
Hapa unaweza kuona kwamba nyuzi zinaamka wakati mwingine na kujaribu kupata rasilimali. Cores mbili kati ya nne hazifanyi chochote (grafu ya kijani kibichi hapo juu).
Kifo cha Mwanafalsafa
Naam, tatizo jingine linaloweza kukatiza karamu tukufu ya wanafalsafa ni iwapo mmoja wao atakufa ghafla akiwa na uma mikononi mwake (na watamzika hivyo). Kisha majirani wataachwa bila chakula cha mchana. Unaweza kuja na msimbo wa mfano wa kesi hii mwenyewe, kwa mfano, inatupwa nje NullReferenceException baada ya mwanafalsafa kuchukua uma. Na, kwa njia, ubaguzi hautashughulikiwa na nambari ya simu haitaipata tu (kwa hili AppDomain.CurrentDomain.UnhandledException na nk). Kwa hivyo, vidhibiti vya makosa vinahitajika kwenye nyuzi zenyewe na kwa uondoaji mzuri.
Kijitabu
Sawa, je, tunatatuaje tatizo hili la mkwamo, njaa na kifo? Tutaruhusu mwanafalsafa mmoja tu kufikia uma, tuongeze kutengwa kwa nyuzi mahali hapa. Jinsi ya kufanya hivyo? Tuseme kwamba mhudumu amesimama karibu na wanafalsafa, ambaye hutoa ruhusa kwa mwanafalsafa yeyote kuchukua uma. Tunafanyaje mhudumu huyu na jinsi wanafalsafa watakavyomuuliza, maswali yanavutia.
Njia rahisi ni wakati wanafalsafa watauliza tu mhudumu kila wakati kwa ufikiaji wa uma. Wale. sasa wanafalsafa hawatasubiri uma karibu, lakini subiri au waulize mhudumu. Mara ya kwanza, tunatumia Nafasi ya Mtumiaji tu kwa hili, ndani yake hatutumii usumbufu kuita taratibu zozote kutoka kwa kernel (kuhusu wao hapa chini).
Suluhisho katika nafasi ya mtumiaji
Hapa tutafanya vile vile tulivyokuwa tukifanya kwa uma moja na wanafalsafa wawili, tutazunguka katika mzunguko na kusubiri. Lakini sasa itakuwa wanafalsafa wote na, kama ilivyokuwa, uma moja tu, i.e. inaweza kusemwa kwamba ni mwanafalsafa tu ambaye alichukua "uma wa dhahabu" kutoka kwa mhudumu ndiye atakayekula. Kwa hili tunatumia SpinLock.
SpinLock hii ni blocker, na, takriban kusema, sawa while(true) { if (!lock) break; }, lakini kwa "uchawi" zaidi kuliko ndani SpinWait (ambayo inatumika hapo). Sasa anajua jinsi ya kuhesabu wale wanaosubiri, kuwaweka usingizi kidogo, na zaidi. nk Kwa ujumla, hufanya kila linalowezekana ili kuboresha. Lakini lazima tukumbuke kuwa hii bado ni mzunguko wa kazi ambao unakula rasilimali za wasindikaji na kuweka mtiririko, ambayo inaweza kusababisha njaa ikiwa mmoja wa wanafalsafa atakuwa kipaumbele zaidi kuliko wengine, lakini hana uma wa dhahabu ( Tatizo la Inversion ya Kipaumbele) . Kwa hivyo, tunaitumia tu kwa mabadiliko mafupi sana katika kumbukumbu iliyoshirikiwa, bila simu za mtu wa tatu, kufuli zilizowekwa kiota, na mshangao mwingine.
Kuchora kwa SpinLock. Mito ni mara kwa mara "kupigana" kwa uma ya dhahabu. Kuna kushindwa - katika takwimu, eneo lililochaguliwa. Cores hazitumiki kikamilifu: karibu 2/3 tu na nyuzi hizi nne.
Suluhisho lingine hapa litakuwa kutumia tu Interlocked.CompareExchange na kungojea sawa kama inavyoonyeshwa kwenye nambari iliyo hapo juu (katika wanafalsafa wenye njaa), lakini hii, kama ilivyosemwa tayari, inaweza kusababisha kuzuia.
juu ya Interlocked Ikumbukwe kwamba hakuna tu CompareExchange, lakini pia njia zingine za kusoma na kuandika kwa atomiki. Na kupitia marudio ya mabadiliko, ikiwa nyuzi nyingine ina wakati wa kufanya mabadiliko yake (soma 1, soma 2, andika 2, andika 1 ni mbaya), inaweza kutumika kwa mabadiliko magumu kwa thamani moja (Iliyounganishwa Chochote muundo) .
Suluhisho za Njia ya Kernel
Ili kuepuka kupoteza rasilimali katika kitanzi, hebu tuone jinsi tunaweza kuzuia thread. Yaani tukiendelea na mfano wetu, tuone mhudumu anavyomlaza mwanafalsafa na kumwamsha inapobidi tu. Kwanza, hebu tuangalie jinsi ya kufanya hivyo kupitia hali ya kernel ya mfumo wa uendeshaji. Miundo yote huko mara nyingi ni polepole kuliko ile iliyo kwenye nafasi ya mtumiaji. Mara kadhaa polepole, kwa mfano AutoResetEvent labda mara 53 polepole SpinLock [Richter]. Lakini kwa msaada wao, unaweza kusawazisha michakato katika mfumo mzima, kusimamiwa au la.
Muundo wa kimsingi hapa ni semaphore iliyopendekezwa na Dijkstra zaidi ya nusu karne iliyopita. Semaphore ni, kwa urahisi, nambari kamili inayodhibitiwa na mfumo, na shughuli mbili juu yake, ongezeko na kupungua. Ikiwa inashindwa kupungua, sifuri, basi thread ya kupiga simu imefungwa. Nambari inapoongezwa na uzi/mchakato mwingine unaotumika, basi nyuzi kurukwa na semaphore inapunguzwa tena na nambari iliyopitishwa. Mtu anaweza kufikiria treni kwenye kizuizi na semaphore. .NET hutoa miundo kadhaa yenye utendakazi sawa: AutoResetEvent, ManualResetEvent, Mutex na mimi mwenyewe Semaphore. Tutatumia AutoResetEvent, hii ndiyo rahisi zaidi ya ujenzi huu: maadili mawili tu 0 na 1 (uongo, kweli). Mbinu Yake WaitOne() huzuia uzi wa kupiga simu ikiwa thamani ilikuwa 0, na ikiwa 1, inaishusha hadi 0 na kuiruka. Mbinu Set() huongeza hadi 1 na kuruhusu mhudumu mmoja apite, ambaye tena hupungua hadi 0. Hufanya kazi kama njia ya kugeuza chini ya ardhi.
Wacha tufanye ugumu wa suluhisho na kutumia kufuli kwa kila mwanafalsafa, na sio kwa wote mara moja. Wale. sasa kunaweza kuwa na wanafalsafa kadhaa mara moja, na sio mmoja. Lakini tunazuia tena ufikiaji wa meza ili kwa usahihi, epuka jamii (hali za mbio), kuchukua dhamana.
Ili kuelewa kinachotokea hapa, fikiria kesi wakati mwanafalsafa alishindwa kuchukua uma, basi matendo yake yatakuwa kama ifuatavyo. Anasubiri upatikanaji wa meza. Baada ya kuipokea, anajaribu kuchukua uma. Haikufanikiwa. Inatoa ufikiaji wa jedwali (kutengwa kwa pande zote). Na hupita "turnstile" yake (AutoResetEvent) (hapo awali ziko wazi). Inaingia kwenye mzunguko tena, kwa sababu hana uma. Anajaribu kuwachukua na kuacha kwenye "turnstile" yake. Jirani wengine wa bahati zaidi kulia au kushoto, baada ya kumaliza kula, anafungua mwanafalsafa wetu, "kufungua zamu yake." Mwanafalsafa wetu anaipitisha (na inafunga nyuma yake) kwa mara ya pili. Anajaribu kwa mara ya tatu kuchukua uma. Bahati njema. Naye hupitisha zamu yake kula.
Wakati kuna makosa ya nasibu katika nambari kama hiyo (zipo kila wakati), kwa mfano, jirani imeainishwa vibaya au kitu sawa kimeundwa. AutoResetEvent kwa wote (Enumerable.Repeat), basi wanafalsafa watasubiri watengenezaji, kwa sababu Kupata makosa katika nambari kama hiyo ni kazi ngumu sana. Shida nyingine na suluhisho hili ni kwamba haihakikishi kuwa mwanafalsafa fulani hatakufa njaa.
Suluhisho la Mseto
Tumeangalia mbinu mbili za kuweka muda, tunapokaa katika hali ya mtumiaji na kitanzi, na tunapozuia uzi kupitia kernel. Njia ya kwanza ni nzuri kwa kufuli fupi, ya pili kwa muda mrefu. Mara nyingi ni muhimu kwanza kusubiri kwa ufupi kwa kutofautiana kwa mabadiliko katika kitanzi, na kisha kuzuia thread wakati kusubiri ni muda mrefu. Mbinu hii inatekelezwa katika kinachojulikana. miundo ya mseto. Hapa kuna muundo sawa na wa hali ya kernel, lakini sasa na kitanzi cha hali ya mtumiaji: SemaphorSlim, ManualResetEventSlim nk Muundo maarufu zaidi hapa ni Monitor, kwa sababu katika C # kuna inayojulikana lock sintaksia. Monitor hii ni semaphore sawa na thamani ya juu ya 1 (mutex), lakini kwa usaidizi wa kusubiri katika kitanzi, kujirudia, muundo wa Hali ya Kubadilika (zaidi juu ya hapo chini), nk. Hebu tuangalie suluhisho nayo.
Hapa tunazuia tena meza nzima kwa ufikiaji wa uma, lakini sasa tunafungua nyuzi zote mara moja, na sio majirani wakati mtu anamaliza kula. Wale. kwanza, mtu anakula na kuzuia majirani, na wakati mtu huyu anapomaliza, lakini anataka kula tena mara moja, anaingia katika kuzuia na kuamsha majirani zake, kwa sababu. muda wake wa kusubiri ni mdogo.
Hivi ndivyo tunavyoepuka mikwaruzo na njaa ya mwanafalsafa fulani. Tunatumia kitanzi kwa kusubiri kwa muda mfupi na kuzuia thread kwa muda mrefu. Kufungua kila mtu mara moja ni polepole kuliko ikiwa tu jirani alifunguliwa, kama kwenye suluhisho na AutoResetEvent, lakini tofauti haipaswi kuwa kubwa, kwa sababu nyuzi lazima zibaki katika hali ya mtumiaji kwanza.
Π£ lock syntax ina mshangao mbaya. Pendekeza kutumia Monitor moja kwa moja [Richter] [Eric Lippert]. Mmoja wao ni kwamba lock daima nje ya Monitor, hata ikiwa kulikuwa na ubaguzi, kwa hali ambayo nyuzi nyingine inaweza kubadilisha hali ya kumbukumbu iliyoshirikiwa. Katika hali kama hizi, mara nyingi ni bora kwenda kwa msuguano au kwa njia fulani kusitisha programu kwa usalama. Mshangao mwingine ni kwamba Monitor hutumia vizuizi vya maingiliano (SyncBlock), ambazo zipo katika vitu vyote. Kwa hivyo, ikiwa kitu kisichofaa kimechaguliwa, unaweza kupata kizuizi kwa urahisi (kwa mfano, ikiwa utafunga kwenye kamba iliyoingiliana). Tunatumia kitu kilichofichwa kila wakati kwa hili.
Muundo wa Kubadilika kwa Hali hukuruhusu kutekeleza kwa ufupi zaidi matarajio ya hali fulani changamano. Katika NET, haijakamilika, kwa maoni yangu, kwa sababu kwa nadharia, kunapaswa kuwa na foleni kadhaa kwenye anuwai kadhaa (kama kwenye Posix Threads), na sio kwenye lok moja. Kisha mtu anaweza kuwafanya kwa wanafalsafa wote. Lakini hata katika fomu hii, inakuwezesha kupunguza msimbo.
wanafalsafa wengi au async / await
Sawa, sasa tunaweza kuzuia nyuzi. Lakini vipi ikiwa tuna wanafalsafa wengi? 100? 10000? Kwa mfano, tulipokea maombi 100000 kwa seva ya wavuti. Itakuwa juu sana kuunda thread kwa kila ombi, kwa sababu nyuzi nyingi sana hazitaenda sambamba. Nitaendesha tu kama vile kuna alama za kimantiki (nina 4). Na kila mtu mwingine atachukua tu rasilimali. Suluhisho moja la tatizo hili ni muundo wa async/wait. Wazo lake ni kwamba kazi haishiki thread ikiwa inahitaji kusubiri kitu kuendelea. Na inapofanya kitu, huanza tena utekelezaji wake (lakini sio lazima kwenye uzi huo huo!). Kwa upande wetu, tutasubiri uma.
SemaphoreSlim ina kwa hili WaitAsync() njia. Hapa kuna utekelezaji kwa kutumia muundo huu.
Mbinu na async / await inatafsiriwa katika mashine ya hali ya hila ambayo inarudisha ndani yake mara moja Task. Kupitia hiyo, unaweza kusubiri kukamilika kwa njia, kufuta, na kila kitu kingine ambacho unaweza kufanya na Task. Ndani ya njia, mashine ya serikali inadhibiti utekelezaji. Jambo la msingi ni kwamba ikiwa hakuna kuchelewa, basi utekelezaji ni synchronous, na ikiwa kuna, basi thread inatolewa. Kwa ufahamu bora wa hili, ni bora kuangalia mashine hii ya serikali. Unaweza kuunda minyororo kutoka kwa haya async / await mbinu.
Hebu tujaribu. Kazi ya wanafalsafa 100 kwenye mashine yenye cores 4 za kimantiki, sekunde 8. Suluhisho la hapo awali na Monitor liliendesha tu nyuzi 4 za kwanza na zingine hazikufanya kazi hata kidogo. Kila moja ya nyuzi hizi 4 haikufanya kitu kwa takriban 2ms. Na suluhisho la async / kungojea liliendesha 100 zote, na wastani wa kungojea kwa sekunde 6.8 kila moja. Kwa kweli, katika mifumo halisi, bila kufanya kazi kwa sekunde 6 haikubaliki na ni bora kutoshughulikia maombi mengi kama haya. Suluhisho na Monitor iligeuka kuwa sio hatari hata kidogo.
Hitimisho
Kama unaweza kuona kutoka kwa mifano hii ndogo, .NET inasaidia miundo mingi ya ulandanishi. Walakini, sio wazi kila wakati jinsi ya kuzitumia. Natumaini makala hii ilikuwa ya manufaa. Kwa sasa, hii ndiyo mwisho, lakini bado kuna mambo mengi ya kuvutia yaliyosalia, kwa mfano, makusanyo ya thread-salama, TPL Dataflow, Reactive programming, Software Transaction model, nk.