Šis straipsnis yra veidrodinis mašininio vertimo straipsnis, spauskite čia norėdami pereiti prie originalaus straipsnio.

Rodinys: 20809|Atsakyti: 1

[Šaltinis] ConcurrentDictionary vs. Žodynas + Užrakinimas - Dennis Gao

[Kopijuoti nuorodą]
Paskelbta 2016-09-13 13:33:04 | | |
Prieš .NET 4.0, jei mums reikėjo naudoti žodyno klasę kelių gijų aplinkoje, neturėjome kito pasirinkimo, kaip tik patys įdiegti gijų sinchronizavimą, kad gijos būtų saugios.

Daugelis kūrėjų tikrai įdiegė panašų saugų siūlų sprendimą, sukurdami visiškai naują saugaus sriegio žodyno tipą arba tiesiog įtraukdami žodyno objektą į klasę ir pridėdami užrakinimo mechanizmą prie visų metodų, kuriuos vadiname "Žodynas + užraktai".

Bet dabar turime ConcurrentDictionary. MSDN žodyno klasės dokumentacijos aprašyme teigiama, kad jei reikia naudoti saugų diegimą, naudokite ConcurrentDictionary.

Taigi, dabar, kai turime saugią žodyno klasę, mums nebereikia jos diegti patiems. Puiku, ar ne?

Problemos kilmė

Tiesą sakant, anksčiau naudojau "CocurrentDictionary" tik vieną kartą, kad patikrinčiau jo reagavimą. Kadangi jis gerai pasirodė testuose, iškart pakeičiau jį savo klase, atlikau keletą bandymų ir tada kažkas nutiko.

Taigi, kas nutiko? Ar nesakėte, kad siūlai saugūs?

Po daugiau bandymų radau problemos šaknį. Tačiau dėl tam tikrų priežasčių MSDN 4.0 versijoje nėra metodo GetOrAdd parašo, kuriam reikia perduoti atstovo tipo parametrą, aprašymo. Pažvelgęs į 4.5 versiją, radau šią pastabą:

Jei vienu metu skambinate GetOrAdd skirtingose gijose, addValueFactory gali būti iškviečiama kelis kartus, tačiau jos rakto / reikšmės pora gali būti neįtraukta į žodyną kiekvieno skambučio metu.
Tai problema, su kuria susidūriau. Kadangi tai anksčiau nebuvo aprašyta dokumentuose, turėjau atlikti daugiau bandymų, kad patvirtinčiau problemą. Žinoma, problema, su kuria susiduriu, yra susijusi su mano naudojimu, apskritai, kai kuriems duomenims talpykloje naudoju žodyno tipą:

Šie duomenys kuriami labai lėtai;
Šie duomenys gali būti sukurti tik vieną kartą, nes antrasis kūrimas sukels išimtį arba keli kūriniai gali sukelti išteklių nutekėjimą ir pan.;
Turėjau problemų su antrąja sąlyga. Jei abi gijos nustato, kad duomenų nėra, jie bus sukurti vieną kartą, tačiau bus sėkmingai išsaugotas tik vienas rezultatas. O kaip dėl kito?

Jei jūsų sukurtas procesas pateikia išimtį, galite naudoti pabandyti: laimikis (nepakankamai elegantiškas, bet išsprendžia problemą). Bet ką daryti, jei ištekliai yra sukurti, o ne perdirbti?

Galima sakyti, kad objektas yra sukurtas ir bus surinktas šiukšlėmis, jei jis jame nebebus nurodytas. Tačiau pagalvokite, kas nutiktų, jei susidarytų toliau aprašyta situacija:

Dinamiškai generuokite kodą naudodami "Emit". Aš naudojau šį metodą "Remoting" sistemoje ir sudėjau visus įgyvendinimus į asamblėją, kurios negalima perdirbti. Jei tipas sukuriamas du kartus, antrasis visada egzistuos, net jei jis niekada nebuvo naudojamas.
Sukurkite temą tiesiogiai arba netiesiogiai. Pavyzdžiui, turime sukurti komponentą, kuris asinchroniniams pranešimams apdoroti naudoja patentuotą giją, ir remiasi jų gavimo tvarka. Kai komponentas sukuriamas, sukuriamas siūlas. Kai šis komponento egzempliorius sunaikinamas, gija taip pat nutraukiama. Bet jei sunaikinę komponentą ištrinsime nuorodą į objektą, tačiau gija dėl kokių nors priežasčių nesibaigia ir turi nuorodą į objektą. Tada, jei siūlas nemiršta, objektas taip pat nebus perdirbtas.
Atlikite P/Invoke operaciją. Reikalauti, kad gautos rankenos uždarymo laikas būtų toks pat kaip angų skaičius.
Tiesa, panašių situacijų yra daug. Pavyzdžiui, žodyno objektas palaikys ryšį su paslauga nuotoliniame serveryje, kurio galima paprašyti tik vieną kartą, o jei to bus prašoma antrą kartą, kita paslauga manys, kad įvyko kažkokia klaida, ir užregistruos ją žurnale. (Įmonėje, kurioje dirbau, už šią būklę buvo taikomos tam tikros teisinės bausmės.) )
Taigi, nesunku pastebėti, kad žodynas + spynos negali būti skubiai pakeistas ConcurrentDictionary, net jei dokumentacijoje sakoma, kad jis yra saugus.

Išanalizuokite problemą

Vis dar nesuprantate?

Tiesa, ši problema gali nekilti taikant žodyno + spynų metodą. Kadangi tai priklauso nuo konkretaus įgyvendinimo, pažvelkime į šį paprastą pavyzdį:


Aukščiau pateiktame kode prieš pradėdami užklausti rakto reikšmės, laikome žodyno užraktą. Jei nurodytos rakto ir reikšmės poros nėra, ji bus sukurta tiesiogiai. Tuo pačiu metu, kadangi jau laikome to žodyno užraktą, galime pridėti raktų ir reikšmių poras tiesiai į žodyną. Tada atleiskite žodyno užraktą ir grąžinkite rezultatą. Jei dvi gijos tuo pačiu metu pateikia užklausas dėl tos pačios rakto reikšmės, pirmoji gija, kuri gauna žodyno užraktą, užbaigs objekto kūrimą, o kita gija lauks, kol šis kūrimas bus baigtas, ir gaus sukurto rakto vertės rezultatą, gavus žodyno užraktą.

Tai gerai, ar ne?

Tai tikrai ne! Nemanau, kad tokio objekto kūrimas lygiagrečiai, kai galų gale naudojamas tik vienas, nesukuria mano aprašytos problemos.

Situacija ir problema, kurią bandau išplėtoti, ne visada gali būti atkuriama, paralelinėje aplinkoje galime tiesiog sukurti du objektus ir tada išmesti vieną. Taigi, kaip tiksliai palyginsime žodyną + spynas ir ConcurrentDictionary?

Atsakymas yra: tai priklauso nuo užrakto naudojimo strategijos ir žodyno naudojimo.

1 žaidimas: lygiagrečiai sukurkite tą patį objektą

Pirmiausia tarkime, kad objektą galima sukurti du kartus, tad kas atsitiks, jei dvi gijos sukuria šį objektą vienu metu?

Antra, kiek laiko praleidžiame panašiems kūriniams?

Galime tiesiog sukurti pavyzdį, kai objekto instanciavimas trunka 10 sekundžių. Kai pirmoji gija sukuria objektą po 5 sekundžių, antroji implementacija bando iškviesti GetOrAdd metodą, kad gautų objektą, o kadangi objektas vis dar neegzistuoja, jis taip pat pradeda kurti objektą.

Esant tokiai būklei, turime 2 procesorius, kurie lygiagrečiai dirba 5 sekundes, o kai pirmoji gija baigia veikti, antroji gija vis tiek turi veikti 5 sekundes, kad užbaigtų objekto statybą. Kai antroji gija baigia kurti objektą, ji nustato, kad objektas jau yra, ir pasirenka naudoti esamą objektą ir tiesiogiai atsisakyti naujai sukurto objekto.

Jei antroji gija tiesiog laukia, o antrasis procesorius atlieka kokį nors kitą darbą (paleidžia kitas gijas ar programas, taupo šiek tiek energijos), jis gaus norimą objektą po 5 sekundžių, o ne po 10 sekundžių.

Taigi, esant tokioms sąlygoms, žodynas + spynos laimi nedidelį žaidimą.

2 žaidimas: lygiagrečiai aplankykite skirtingus objektus

Ne, jūsų paminėta situacija visai netiesa!

Na, aukščiau pateiktas pavyzdys yra šiek tiek savotiškas, bet jis apibūdina problemą, tiesiog šis naudojimas yra ekstremalesnis. Taigi, apsvarstykite, kas atsitiks, jei pirmoji gija kuria objektą, o antroji gija turi pasiekti kitą rakto reikšmės objektą, o tas rakto reikšmės objektas jau egzistuoja?

"ConcurrentDictionary" dizainas be užrakto leidžia skaityti labai greitai, nes skaitymas nėra užrakintas. "Dictionary + Locks" atveju skaitymo operacija bus užrakinta tarpusavyje nesuderinamai, net jei tai yra visiškai kitas raktas, o tai akivaizdžiai sulėtins skaitymo operaciją.

Tokiu būdu ConcurrentDictionary atšaukė žaidimą.

Pastaba: Čia manau, kad žodyno klasėje suprantate keletą sąvokų, tokių kaip kibiras / mazgas / įrašas, jei ne, rekomenduojama perskaityti Ofir Makmal straipsnį "Bendrojo žodyno supratimas nuodugniai", kuriame gerai paaiškinamos šios sąvokos.

trečiasis žaidimo žaidimas: skaityti daugiau ir parašyti vieną

Kas atsitiks, jei žodyne + užraktuose vietoj visiško žodyno užrakto naudosite kelis skaitytuvus ir vieną rašytoją?

Jei gija kuria objektą ir laiko atnaujinamą užraktą, kol objektas bus sukurtas, užraktas atnaujinamas į rašymo užraktą, tada skaitymo operaciją galima atlikti lygiagrečiai.

Taip pat galime išspręsti problemą palikdami skaitymo operaciją neveikiančią 10 sekundžių. Bet jei skaitymų yra daug daugiau nei rašoma, pamatysime, kad "ConcurrentDictionary" vis dar yra greitas, nes įdiegia skaitymus be užrakto.

Naudojant "ReaderWriterLockSlim" žodynams, skaitymas pablogėja, todėl paprastai rekomenduojama naudoti "Full Lock for Dictionaries", o ne "ReaderWriterLockSlim".

Taigi, esant tokioms sąlygoms, ConcurrentDictionary laimėjo dar vieną žaidimą.

Pastaba: Ankstesniuose straipsniuose aptariau "YieldReaderWriterLock" ir "YieldReaderWriterLockSlim" klases. Naudojant šį skaitymo-rašymo užraktą, greitis buvo žymiai pagerintas (dabar išsivystė į SpinReaderWriterLockSlim) ir leidžia kelis skaitymus vykdyti lygiagrečiai su mažu poveikiu arba visai be jokio poveikio. Nors aš vis dar naudoju šį būdą, be užrakto ConcurrentDictionary būtų akivaizdžiai greitesnis.

4 žaidimas: pridėkite kelias rakto ir reikšmės poras

Susidūrimas dar nesibaigė.

Ką daryti, jei turime kelias pagrindines reikšmes, kurias reikia pridėti, ir visos jos nesusiduria ir yra priskirtos skirtingiems kibirams?

Iš pradžių šis klausimas buvo įdomus, bet aš padariau testą, kuris ne visai tiko. Aš naudojau <int, int> tipo žodyną, o objekto statybos gamykla pateikdavo neigiamą rezultatą tiesiai kaip raktą.

Aš tikėjausi, kad ConcurrentDictionary bus greičiausias, bet jis pasirodė lėčiausias. Kita vertus, žodynas + užraktai veikia greičiau. Kodėl?

Taip yra todėl, kad "ConcurrentDictionary" paskirsto mazgus ir sudeda juos į skirtingus kibirus, kurie yra optimizuoti taip, kad atitiktų skaitymo operacijų dizainą be užraktų. Tačiau pridedant rakto vertės elementus, mazgo kūrimo procesas tampa brangus.

Net ir lygiagrečiomis sąlygomis mazgo užrakto paskirstymas vis tiek sunaudoja daugiau laiko nei naudojant pilną užraktą.

Taigi, žodynas + spynos laimi šį žaidimą.

žaisti penktąjį žaidimą: skaitymo operacijų dažnis yra didesnis

Atvirai kalbant, jei turėtume delegatą, kuris galėtų greitai sutvarkyti objektus, mums nereikėtų žodyno. Mes galime tiesiogiai paskambinti delegatui, kad gautų objektą, tiesa?

Tiesą sakant, atsakymas taip pat yra tas, kad tai priklauso nuo situacijos.

Įsivaizduokite, kad rakto tipas yra eilutė ir jame yra įvairių žiniatinklio serverio puslapių kelio žemėlapiai, o atitinkama reikšmė yra objekto tipas, kuriame yra dabartinių vartotojų, pasiekiančių puslapį, įrašas ir visų apsilankymų puslapyje skaičius nuo serverio paleidimo.

Sukurti tokį objektą yra beveik akimirksniu. Ir po to jums nereikia kurti naujo objekto, tiesiog pakeiskite jame išsaugotas reikšmes. Taigi galima leisti sukurti būdą du kartus, kol bus naudojamas tik vienas egzempliorius. Tačiau kadangi "ConcurrentDictionary" mazgų išteklius paskirsto lėčiau, naudojant žodyną + užraktus kūrimo laikas bus greitesnis.

Taigi, šis pavyzdys yra labai ypatingas, taip pat matome, kad žodynas + spynos veikia geriau esant tokiai sąlygai, užima mažiau laiko.

Nors mazgų paskirstymas ConcurrentDictionary yra lėtesnis, aš nebandžiau įdėti į jį 100 milijonų duomenų elementų, kad patikrinčiau laiką. Nes tai akivaizdžiai užima daug laiko.

Tačiau daugeliu atvejų, sukūrus duomenų elementą, jis visada skaitomas. Kaip keičiasi duomenų elemento turinys, yra kitas klausimas. Taigi nesvarbu, kiek milisekundžių dar reikia duomenų elementui sukurti, nes nuskaitymai yra greitesni (vos keliomis milisekundėmis greičiau), tačiau skaitymai vyksta dažniau.

Taigi, ConcurrentDictionary laimėjo žaidimą.

6 žaidimas: sukurkite objektus, kurie sunaudoja skirtingą laiką

Kas atsitiks, jei skirtingų duomenų elementų kūrimo laikas skiriasi?

Sukurkite kelis duomenų elementus, kurie sunaudoja skirtingą laiką, ir lygiagrečiai įtraukite juos į žodyną. Tai yra stipriausia "ConcurrentDictionary" pusė.

ConcurrentDictionary naudoja daugybę skirtingų užrakinimo mechanizmų, kad duomenų elementus būtų galima pridėti vienu metu, tačiau logika, pvz., nuspręsti, kurį užraktą naudoti, prašyti užrakto pakeisti kibiro dydį ir pan., nepadeda. Duomenų elementų įdėjimo į kibirą greitis yra greitas. Kas iš tikrųjų daro ConcurrentDictionary laimėti yra jo gebėjimas kurti objektus lygiagrečiai.

Tačiau iš tikrųjų galime padaryti tą patį. Jei mums nerūpi, ar lygiagrečiai kuriame objektus, ar kai kurie iš jų buvo atmesti, galime pridėti užraktą, kad nustatytume, ar duomenų elementas jau egzistuoja, tada atleiskite užraktą, sukurkite duomenų elementą, paspauskite jį, kad gautumėte užraktą, dar kartą patikrinkite, ar duomenų elementas egzistuoja, o jei jo nėra, pridėkite duomenų elementą. Kodas gali atrodyti maždaug taip:

* Atkreipkite dėmesį, kad aš naudoju <int, int> tipo žodyną.

Aukščiau pateiktoje paprastoje struktūroje žodynas + užraktai veikia beveik taip pat gerai, kaip ir ConcurrentDictionary, kai kuriami ir pridedami duomenų elementai lygiagrečiomis sąlygomis. Tačiau yra ir ta pati problema, kai kai kurios reikšmės gali būti sugeneruotos, bet niekada nenaudojamos.

Išvada

Taigi, ar yra išvada?

Šiuo metu vis dar yra keletas:

Visos žodyno klasės yra labai greitos. Nors sukūriau milijonus duomenų, tai vis tiek greita. Paprastai sukuriame tik nedidelį skaičių duomenų elementų, o tarp skaitymų yra tam tikri laiko intervalai, todėl paprastai nepastebime duomenų elementų skaitymo laiko sąnaudų.
Jei to paties objekto negalima sukurti du kartus, nenaudokite ConcurrentDictionary.
Jei tikrai nerimaujate dėl našumo, žodynas + spynos vis tiek gali būti geras sprendimas. Svarbus veiksnys yra pridėtų ir pašalintų duomenų elementų skaičius. Bet jei yra daug skaitymo operacijų, ji yra lėtesnė nei ConcurrentDictionary.
Nors aš jo nepristatiau, iš tikrųjų yra daugiau laisvės naudoti žodyno + spynų schemą. Pavyzdžiui, galite užrakinti vieną kartą, pridėti kelis duomenų elementus, ištrinti kelis duomenų elementus arba kelis kartus pateikti užklausą ir pan., tada atleisti užraktą.
Apskritai venkite naudoti "ReaderWriterLockSlim", jei skaitoma daug daugiau nei rašoma. Žodyno tipai jau yra daug greitesni nei skaitymo užraktas skaitymo-rašymo užrakte. Žinoma, tai priklauso ir nuo laiko, sugaišto kuriant objektą spynoje.
Taigi, manau, kad pateikti pavyzdžiai yra šiek tiek ekstremalūs, tačiau jie rodo, kad ConcurrentDictionary naudojimas ne visada yra geriausias sprendimas.

Pajuskite skirtumą

Aš parašiau šį straipsnį ketindamas ieškoti geresnio sprendimo.

Aš jau bandau giliau suprasti, kaip veikia konkreti žodyno klasė (dabar jaučiu, kad esu labai aiškus).

Be abejo, "Bucket" ir "Node" "ConcurrentDictionary" yra labai paprasti. Aš padariau kažką panašaus, kai bandžiau sukurti žodyno klasę. Įprasta žodyno pamoka gali atrodyti paprastesnė, bet iš tikrųjų ji yra sudėtingesnė.

ConcurrentDictionary kiekvienas mazgas yra visa klasė. Žodyno klasėje mazgas įgyvendinamas naudojant reikšmės tipą, o visi mazgai laikomi didžiuliame masyve, o kibiras naudojamas masyvo indeksavimui. Jis taip pat naudojamas vietoj paprastos mazgo nuorodos į kitą mazgą (juk kaip struktūros tipo mazgas, jame negali būti struktūros tipo mazgo nario).

Pridedant ir pašalinant žodyną, žodyno klasė negali tiesiog sukurti naujo mazgo, ji turi patikrinti, ar yra indeksas, žymintis panaikintą mazgą, ir tada jį pakartotinai naudoti. Arba "Count" naudojamas norint gauti naujo mazgo vietą masyve. Tiesą sakant, kai masyvas pilnas, žodyno klasė priverčia keisti dydį.

"ConcurrentDictionary" mazgas gali būti laikomas nauju objektu. Mazgo pašalinimas yra tiesiog jo nuorodos pašalinimas. Pridėjus naują mazgą, galima tiesiog sukurti naują mazgo egzempliorių. Dydžio keitimas yra tik siekiant išvengti konfliktų, tačiau tai nėra privaloma.

Taigi, jei žodyno klasė tikslingai naudoja sudėtingesnius algoritmus, kaip ConcurrentDictionary užtikrins, kad ji geriau veiktų kelių gijų aplinkoje?

Tiesa ta, kad visų mazgų sudėjimas į vieną masyvą yra greičiausias būdas paskirstyti ir skaityti, net jei mums reikia kito masyvo, kad galėtume sekti, kur rasti tuos duomenų elementus. Taigi atrodo, kad turint tą patį kibirų skaičių bus naudojama daugiau atminties, tačiau naujų duomenų elementų nereikia perskirstyti, nereikia naujų objektų sinchronizavimo ir naujų šiukšlių surinkimo neatsiranda. Nes viskas jau yra savo vietose.

Tačiau mazgo turinio pakeitimas nėra atominė operacija, o tai yra vienas iš veiksnių, dėl kurių jo gija yra nesaugi. Kadangi mazgai yra visi objektai, iš pradžių sukuriamas mazgas, o tada atnaujinama atskira nuoroda, nurodanti į jį (atominė operacija čia). Taigi, skaitymo gija gali skaityti žodyno turinį be užrakto, o skaitymas turi būti viena iš senų ir naujų reikšmių, ir nėra galimybės perskaityti nepilną reikšmę.

Taigi, tiesa ta, kad jei jums nereikia užrakto, žodyno klasė skaito greičiau, nes būtent užraktas sulėtina skaitymą.

Šis straipsnis yra išverstas iš Paulo Zemek straipsnio "Žodynas + užrakinimas prieš ConcurrentDictionary" CodeProject, ir kai kurie teiginiai pasikeis dėl supratimo.







Ankstesnis:IoC efektyvus Autofac
Kitą:"Alibaba" 4 žmonės buvo atleisti už tai, kad naudojo JS scenarijus ir skubėjo pirkti mėnulio pyragus
 Savininkas| Paskelbta 2016-09-13 13:33:15 |
ConcurrentDictionary palaiko naujus ir atnaujintus naujinimus
http://www.itsvse.com/thread-2955-1-1.html
(Šaltinis: Code Agriculture Network)
Atsakomybės apribojimas:
Visa programinė įranga, programavimo medžiaga ar straipsniai, kuriuos skelbia Code Farmer Network, yra skirti tik mokymosi ir mokslinių tyrimų tikslams; Aukščiau nurodytas turinys negali būti naudojamas komerciniais ar neteisėtais tikslais, priešingu atveju vartotojai prisiima visas pasekmes. Šioje svetainėje pateikiama informacija gaunama iš interneto, o ginčai dėl autorių teisių neturi nieko bendra su šia svetaine. Turite visiškai ištrinti aukščiau pateiktą turinį iš savo kompiuterio per 24 valandas nuo atsisiuntimo. Jei jums patinka programa, palaikykite autentišką programinę įrangą, įsigykite registraciją ir gaukite geresnes autentiškas paslaugas. Jei yra kokių nors pažeidimų, susisiekite su mumis el. paštu.

Mail To:help@itsvse.com