See artikkel on masintõlke peegelartikkel, palun klõpsake siia, et hüpata algse artikli juurde.

Vaade: 20809|Vastuse: 1

[Allikas] ConcurrentDictionary vs. Dictionary+Locking - Dennis Gao

[Kopeeri link]
Postitatud 13.09.2016 13:33:04 | | |
Enne .NET 4.0 versiooni, kui pidime kasutama Sõnastiku klassi mitmelõimelises keskkonnas, polnud meil muud valikut kui ise lõimede sünkroniseerimist rakendada, et lõimed oleksid turvalised.

Paljud arendajad on kindlasti rakendanud sarnast lõime-turvalist lahendust, kas luues täiesti uue lõimekindla sõnastiku tüübi või lihtsalt kapseldades sõnastiku objekti klassi ja lisades lukustusmehhanismi kõigile meetoditele, mida nimetame "Sõnastikuks + lukustusteks".

Aga nüüd on meil ConcurrentDictionary. MSDN-i sõnastiku klassi dokumentatsiooni lõime-ohutu kirjeldus ütleb, et kui vajad lõime-turvalist rakendust, kasuta ConcurrentDictionaryt.

Nüüd, kui meil on lõime-turvaline sõnastiku klass, ei pea me seda enam ise rakendama. Suurepärane, kas pole?

Probleemi päritolu

Tegelikult olen CocurrentDictionaryt kasutanud vaid korra oma testis, et testida selle reageerimisvõimet. Kuna see läks testides hästi, asendasin selle kohe oma klassiga, tegin mõned testid ja siis läks midagi valesti.

Mis siis valesti läks? Kas sa ei öelnud, et see on niidikindel?

Pärast täiendavaid katseid leidsin probleemi juure. Kuid mingil põhjusel ei sisalda MSDN versioon 4.0 GetOrAdd meetodi allkirja kirjeldust, mis nõuaks delegaadi tüüpi parameetri läbimist. Pärast versiooni 4.5 vaatamist leidsin selle märkuse:

Kui kutsud GetOrAddi samaaegselt erinevatel lõimedel, võib addValueFactory kutsuda mitu korda, kuid selle võtme/väärtuse paari ei pruugi iga kõne puhul sõnastiku juurde lisada.
See ongi probleem, millega ma kokku puutusin. Kuna dokumentatsioonis seda varem ei kirjeldatud, pidin probleemi kinnitamiseks rohkem teste tegema. Muidugi on probleem, millega kokku puutun, seotud minu kasutusega, üldiselt kasutan sõnastiku tüüpi andmete vahemällu salvestamiseks:

Need andmed loovad väga aeglaselt;
Seda andmeid saab luua vaid korra, sest teine loomine toob kaasa erandi või mitmed loomised võivad põhjustada ressursside lekkimist jne;
Mul oli probleem teise tingimusega. Kui mõlemad lõimed leiavad, et mingi andmetükk ei eksisteeri, luuakse see üks kord, kuid ainult üks tulemus salvestatakse edukalt. Aga see teine?

Kui loodud protsess annab erandi, võid kasutada try: Catch (pole piisavalt elegantne, aga lahendab probleemi). Aga mis siis, kui ressurss luuakse, mitte ei taaskasutata?

Võid öelda, et objekt on loodud ja koristatakse prügikasti, kui seda enam seal ei mainita. Kuid mõelge, mis juhtuks, kui allpool kirjeldatud olukord juhtuks:

Genereeri koodi dünaamiliselt Emitiga. Kasutasin seda lähenemist kaugjuhtimise raamistikus ja panin kõik rakendused assambleesse, mida ei saanud taaskasutada. Kui tüüp luuakse kaks korda, eksisteerib teine alati, isegi kui seda pole kunagi kasutatud.
Loo teema otse või kaudselt. Näiteks peame ehitama komponendi, mis kasutab asünkroonsete sõnumite töötlemiseks patenteeritud lõime ja sõltub nende vastuvõtmise järjekorrast. Kui komponent on instantsitud, luuakse lõim. Kui see komponendi eksemplar hävitatakse, lõpetatakse ka lõim. Aga kui me kustutame objekti viite pärast komponendi hävitamist, siis lõim mingil põhjusel ei lõpe ja hoiab viide objektile. Kui niit ei sure, ei taaskasutata ka objekti.
Tee P/Invoke operatsioon. Nõua, et vastu võetud käepideme suletud aegade arv oleks sama mis avangute arv.
Kindlasti on palju sarnaseid olukordi. Näiteks hoiab sõnastiku objekt ühendust kaugserveri teenusega, mida saab taotleda vaid korra, ja kui seda taotletakse teist korda, arvab teine teenus, et on tekkinud mingi viga ja logib selle logisse. (Ettevõttes, kus ma töötasin, oli selle seisundi eest mõned õiguslikud karistused.) )
Seega on lihtne näha, et Dictionary + Locks ei saa kiirustades asendada ConcurrentDictionaryga, isegi kui dokumentatsioon ütleb, et see on lõimede jaoks ohutu.

Analüüsi probleemi

Ikka ei saa aru?

Tõsi on, et see probleem ei pruugi tekkida Dictionary + Locks lähenemise korral. Kuna see sõltub konkreetsest rakendusest, vaatame seda lihtsat näidet:


Ülaltoodud koodis hoiame sõnastiku lukku enne, kui hakkame võtme väärtust pärima. Kui määratud võtme-väärtuse paari ei eksisteeri, luuakse see otse. Samas, kuna meil on sellel sõnastikul juba lukk, saame lisada võtme-väärtuse paarid otse sõnastikule. Seejärel vabasta sõnastiku lukk ja tagasta tulemus. Kui kaks lõime pärivad sama võtme väärtust samaaegselt, lõpetab esimene lõim, mis saab sõnastiku luku, objekti loomise lõpule ning teine lõim ootab selle loomise lõppu ja saab loodud võtme väärtuse tulemuse pärast sõnastiku luku saamist.

See on hea, eks?

Tegelikult ei ole! Ma ei arva, et paralleelselt objekti loomine, kus lõpuks kasutatakse ainult ühte, ei tekita seda probleemi, mida kirjeldasin.

Olukord ja probleem, mida ma püüan selgitada, ei pruugi alati olla korduvad, paralleelses keskkonnas saame lihtsalt luua kaks objekti ja siis ühe kõrvale jätta. Kuidas siis täpselt võrrelda Dictionary + Locksi ja ConcurrentDictionaryt?

Vastus on: see sõltub luku kasutamise strateegiast ja sellest, kuidas sõnaraamatut kasutatakse.

Mäng 1: Loo sama objekt paralleelselt

Esiteks, oletame, et objekti saab luua kaks korda, mis juhtub, kui kaks lõime loovad selle objekti samaaegselt?

Teiseks, kui kaua kulutame sarnastele loomingutele?

Võime lihtsalt luua näite, kus objekti instantsieerimine võtab 10 sekundit. Kui esimene lõim loob objekti 5 sekundi pärast, proovib teine rakendus kutsuda GetOrAdd meetodit, et objekti saada, ja kuna objekti ikka ei eksisteeri, hakkab ka see objekti looma.

Sellises olukorras töötavad kaks protsessorit paralleelselt 5 sekundit ning kui esimene lõim lõpetab, peab teine lõim ikkagi 5 sekundit töötama, et objekti ehitus lõpule viia. Kui teine lõim lõpetab objekti ehitamise, leiab ta, et objekt on juba olemas, ning valib olemasoleva objekti kasutamise ja äsja loodud objekti otse ära viskamise.

Kui teine lõim lihtsalt ootab ja teine protsessor teeb muud tööd (käivitab teisi lõime või rakendusi, säästab energiat), saab ta soovitud objekti 5 sekundi pärast, mitte 10 sekundiga.

Nii et nende tingimuste korral võidab Sõnastik + Lukud väikese mängu.

Mäng 2: Külasta erinevaid objekte paralleelselt

Ei, see olukord, mida sa ütlesid, ei vasta üldse tõele!

Noh, ülaltoodud näide on veidi kummaline, aga see kirjeldab probleemi, lihtsalt see kasutus on äärmuslikum. Nii et mõtle, mis juhtub, kui esimene lõim loob objekti ja teine lõim peab pääsema ligi teisele võtme-väärtuse objektile ning see võtme-väärtuse objekt on juba olemas?

ConcurrentDictionarys teeb lukuvaba disain lugemised väga kiireks, sest lugemisel pole lukku. Dictionary + Locks puhul on lugemistoiming lukustatud, isegi kui tegemist on täiesti erineva võtmega, mis ilmselgelt aeglustab lugemisoperatsiooni.

Nii tõmbas ConcurrentDictionary mängu tagasi.

Märkus: Siin arvan, et mõistad mitmeid mõisteid nagu Bucket/Node/Entry sõnaraamatu klassis, kui mitte, soovitatakse lugeda Ofir Makmali artiklit "Understanding Generic Dictionary in-depth", mis selgitab neid mõisteid hästi.

Mängu kolmas mäng: loe rohkem ja kirjuta üksik

Mis juhtub, kui kasutad Dictionary + Locks sõnastiku täieliku lukustuse asemel Multiple Readers ja Single Writerit?

Kui lõim loob objekti ja hoiab uuendatavat lukku kuni objekti loomiseni, uuendatakse lukk kirjutamislukuks, siis saab lugemistoimingu paralleelselt läbi viia.

Probleemi saab lahendada ka sellega, et jätame lugemisoperatsiooni 10 sekundiks tühikäigule. Kuid kui lugemisi on palju rohkem kui kirjutamisi, leiame, et ConcurrentDictionary on endiselt kiire, sest see rakendab lukuvaba režiimi lugemisi.

ReaderWriterLockSlim'i kasutamine sõnaraamatute jaoks teeb lugemise halvemaks ning üldiselt soovitatakse kasutada sõnaraamatute puhul Full Lock'i, mitte ReaderWriterLockSlim'i.

Nii et nende tingimuste korral võitis ConcurrentDictionary veel ühe mängu.

Märkus: Olen varasemates artiklites käsitlenud YieldReaderWriterLock ja YieldReaderWriterSlim kursusi. Selle lugemis-kirjutamise luku abil on kiirus märkimisväärselt paranenud (nüüd arenenud SpinReaderWriterLockSlim-iks) ja võimaldab mitme lugemise paralleelselt täita vähese või olematu mõjuga. Kuigi ma kasutan seda meetodit, oleks lukuvaba ConcurrentDictionary ilmselgelt kiirem.

Mäng 4: Lisa mitu võtme-väärtuse paari

Vastasseis pole veel läbi.

Mis siis, kui meil on mitu võtmeväärtust, mida lisada, ja kõik need ei põrku ning määratakse erinevatesse ämbritesse?

Alguses oli see küsimus uudishimulik, kuid tegin testi, mis ei sobinud päris hästi. Kasutasin sõnaraamatut tüübiga <int, int> ja objekti ehitustehas tagastas negatiivse tulemuse otse võtmena.

Ootasin, et ConcurrentDictionary on kõige kiirem, aga see osutus kõige aeglasemaks. Dictionary + Locks seevastu töötab kiiremini. Miks nii?

See on tingitud sellest, et ConcurrentDictionary eraldab sõlmed ja paigutab need erinevatesse ämbritesse, mis on optimeeritud lukuvaba disaini järgi lugemistoimingute jaoks. Kuid võtmeväärtuslike esemete lisamisel muutub sõlme loomise protsess kulukaks.

Isegi paralleelsetes tingimustes võtab Sõlme lukustuse eraldamine rohkem aega kui täislukk.

Nii et Dictionary + Locks võidab selle mängu.

Viienda mängu mängimine: lugemisoperatsioonide sagedus on kõrgem

Ausalt öeldes, kui meil oleks delegaat, kes suudaks kiiresti objekte instantsida, ei oleks meil sõnaraamatut vaja. Me võime otse delegaadile helistada, et ese kätte saada, eks?

Tegelikult sõltub see ka olukorrast.

Kujutame ette, et võtme tüüp on string ja sisaldab teekaarte erinevate veebiserveri lehtede jaoks ning vastav väärtus on objektitüüp, mis sisaldab praeguste kasutajate kirjet lehele ligipääsu ja kõigi külastuste arvu alates serveri käivitamisest.

Sellise objekti loomine toimub peaaegu hetkega. Ja pärast seda ei pea sa uut objekti looma, vaid lihtsalt muuta sinna salvestatud väärtusi. Seega on võimalik lubada viisi loomist kaks korda, kuni kasutatakse ainult ühte eksemplari. Kuid kuna ConcurrentDictionary jaotab sõlmede ressursse aeglasemalt, toob Dictionary + Locks kasutamine kaasa kiirema loomise aja.

See näide on väga eriline, näeme ka, et Dictionary + Locks toimib selles olukorras paremini, võttes vähem aega.

Kuigi sõlmede jaotus ConcurrentDictionarys on aeglasem, ei proovinud ma sinna panna 100 miljonit andmeelementi, et aega testida. Sest see võtab ilmselgelt palju aega.

Kuid enamasti, kui andmeüksus on loodud, loetakse see alati. Kuidas andmeelemendi sisu muutub, on teine teema. Seega ei ole tähtis, kui palju millisekundiid andmeelemendi loomine veel võtab, sest lugemised on kiiremad (vaid paar millisekundit kiirem), kuid lugemised toimuvad sagedamini.

Nii et ConcurrentDictionary võitis mängu.

Mäng 6: Loo objekte, mis tarbivad erinevaid aegu

Mis juhtub, kui erinevate andmeelementide loomise aeg varieerub?

Loo mitu andmeelementi, mis tarbivad erinevaid aegu, ja lisa need paralleelselt sõnaraamatusse. See on ConcurrentDictionary tugevaim külg.

ConcurrentDictionary kasutab mitmeid erinevaid lukustusmehhanisme, et võimaldada andmeelementide samaaegset lisamist, kuid loogika nagu otsustamine, millist lukku kasutada, luku palumine ämbri suuruse muutmiseks jne, ei aita. Andmeelementide mahutamise kiirus ämbrisse on masinakiirusel. Mis teeb ConcurrentDictionary'i tõeliseks võiduks, on selle võime luua objekte paralleelselt.

Kuid tegelikult saame sama teha. Kui me ei hooli, kas loome objekte paralleelselt või kui osa neist on ära visatud, saame lisada luku, et tuvastada, kas andmeüksus juba eksisteerib, siis vabastada lukustus, luua andmeüksus, vajutada seda, et saada lukk, kontrollida uuesti, kas andmeüksus eksisteerib, ja kui ei ole, lisada andmeelement. Kood võib välja näha umbes selline:

* Pane tähele, et kasutan sõnastikku tüübiga <int, int>.

Ülaltoodud lihtsas struktuuris toimib Dictionary + Locks peaaegu sama hästi kui ConcurrentDictionary, kui luuakse ja lisatakse andmeelemente paralleelsetes tingimustes. Kuid on ka sama probleem, kus mõned väärtused võivad genereerida, kuid neid ei kasutata kunagi.

Järeldus

Kas on järeldus?

Praegu on veel mõned:

Kõik sõnastikutunnid on väga kiired. Kuigi olen loonud miljoneid andmeid, on see siiski kiire. Tavaliselt loome vaid väikese arvu andmeelemente ja lugemiste vahel on teatud ajavahemikke, nii et me tavaliselt ei märka andmeelementide lugemise ajakoormust.
Kui sama objekti ei saa kaks korda luua, ära kasuta ConcurrentDictionaryt.
Kui sind tõesti huvitab jõudlus, võib Dictionary + Locks siiski olla hea lahendus. Oluline tegur on lisatud ja eemaldatud andmeelementide arv. Kuid kui lugemisoperatsioone on palju, on see aeglasem kui ConcurrentDictionary.
Kuigi ma seda ei tutvustanud, on tegelikult rohkem vabadust kasutada Sõnastiku + Lukkude skeemi. Näiteks saad lukustada korra, lisada mitu andmeelementi, kustutada mitu andmeelementi või mitu korda päringut teha jne, ja siis lukustust vabastada.
Üldiselt väldi ReaderWriterLockSlim'i kasutamist, kui seal on palju rohkem lugemisi kui kirjutamisi. Sõnastiku tüübid on juba palju kiiremad kui lugemisluku saamine lugemis-kirjutamise lukustusse. Muidugi sõltub see ka sellest, kui palju aega kulub objekti loomiseks lukus.
Seega arvan, et toodud näited on veidi äärmuslikud, kuid näitavad, et ConcurrentDictionary kasutamine ei ole alati parim lahendus.

Tunne erinevust

Kirjutasin selle artikli eesmärgiga leida parem lahendus.

Ma püüan juba sügavamalt mõista, kuidas konkreetne sõnastiku kursus töötab (nüüd tunnen, et olen väga selge).

Võib väita, et Bucket ja Node ConcurrentDictionarys on väga lihtsad. Tegin midagi sarnast, kui proovisin luua sõnastikuklassi. Tavaline sõnastiku klass võib tunduda lihtsam, kuid tegelikult on see keerukam.

ConcurrentDictionarys on iga sõlm täielik klass. Sõnastiku klassis on Node rakendatud väärtustüübi abil ning kõik sõlmed hoitakse suures massiivis, samal ajal kui Bucket kasutatakse massiivi indekseerimiseks. Seda kasutatakse ka sõlme lihtsa viite asemel järgmisele sõlmele (lõppude lõpuks, kuna see on struktuuritüüpi sõlm, ei saa see sisaldada struktuuri tüüpi sõlmeliiget).

Sõnastiku lisamisel ja eemaldamisel ei saa Sõnastiku klass lihtsalt uut sõlme luua, vaid peab kontrollima, kas kustutatud sõlme tähistab indeks, ning seejärel seda uuesti kasutama. Või kasutatakse "Count" uue sõlme asukoha määramiseks massiivis. Tegelikult, kui massiivi on täis, sunnib sõnastiku klass suurust muutma.

ConcurrentDictionary puhul võib sõlme käsitleda kui uut objekti. Sõlme eemaldamine tähendab lihtsalt selle viite eemaldamist. Uue Node'i lisamine võib lihtsalt luua uue Node'i instantsi. Suuruse muutmine on ainult konfliktide vältimiseks, kuid see ei ole kohustuslik.

Kui Sõnastiku klass kasutab teadlikult keerukamaid algoritme selle käsitlemiseks, kuidas tagab ConcurrentDictionary, et see toimib paremini mitmelõimelises keskkonnas?

Tõde on see, et kõigi sõlmede paigutamine ühte massiivi on kiireim viis jaotamiseks ja lugemiseks, isegi kui vajame teist massiivi, et jälgida, kust need andmeelemendid leida. Tundub, et sama arvu ämbrite kasutamine kulutab rohkem mälu, kuid uusi andmeelemente ei pea ümber jaotama, uusi objektisünkroonimisi pole vaja ja uut prügi koristust ei toimu. Sest kõik on juba paigas.

Kuid sisu asendamine sõlmes ei ole aatomiline operatsioon, mis on üks teguritest, mis muudab selle lõime ebaturvaliseks. Kuna sõlmed on kõik objektid, luuakse alguses sõlm ja seejärel uuendatakse eraldi viide, mis sellele osutab (aatomioperatsioon siin). Seega saab lugemisniit lugeda sõnastiku sisu ilma lukustuseta ning lugemine peab olema üks vanadest ja uutest väärtustest, ning puuduliku väärtuse lugemise oht ei ole.

Tõde on see, et kui sul pole lukku vaja, on sõnastiku klass lugemiste osas kiirem, sest just lukk aeglustab lugemist.

See artikkel on tõlgitud Paulo Zemeki artiklist "Dictionary + Locking versus ConcurrentDictionary" CodeProjectis ning mõned väited võivad arusaamise tõttu muutuda.







Eelmine:IoC efektiivne Autofac
Järgmine:Alibaba 4 inimest vallandati, sest nad kasutasid JS-skripte, et kiirustada kuukookide ostmiseks
 Üürileandja| Postitatud 13.09.2016 13:33:15 |
ConcurrentDictionary toetab uusi ja uuendatud uuendusi
http://www.itsvse.com/thread-2955-1-1.html
(Allikas: Code Agriculture Network)
Disclaimer:
Kõik Code Farmer Networki poolt avaldatud tarkvara, programmeerimismaterjalid või artiklid on mõeldud ainult õppimiseks ja uurimistööks; Ülaltoodud sisu ei tohi kasutada ärilistel ega ebaseaduslikel eesmärkidel, vastasel juhul kannavad kasutajad kõik tagajärjed. Selle saidi info pärineb internetist ning autoriõiguste vaidlused ei ole selle saidiga seotud. Ülaltoodud sisu tuleb oma arvutist täielikult kustutada 24 tunni jooksul pärast allalaadimist. Kui sulle programm meeldib, palun toeta originaaltarkvara, osta registreerimist ja saa paremaid ehtsaid teenuseid. Kui esineb rikkumist, palun võtke meiega ühendust e-posti teel.

Mail To:help@itsvse.com