Šis raksts ir mašīntulkošanas spoguļraksts, lūdzu, noklikšķiniet šeit, lai pārietu uz oriģinālo rakstu.

Skats: 20809|Atbildi: 1

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

[Kopēt saiti]
Publicēts 13.09.2016 13:33:04 | | |
Pirms .NET 4.0, ja mums vajadzēja izmantot vārdnīcas klasi daudzpavedienu vidē, mums nebija citas izvēles, kā pašiem ieviest pavedienu sinhronizāciju, lai pavedieni būtu droši.

Daudzi izstrādātāji noteikti ir ieviesuši līdzīgu pavedienu drošu risinājumu, vai nu izveidojot pilnīgi jaunu pavedienu drošu vārdnīcas tipu, vai vienkārši iekapsulējot vārdnīcas objektu klasē un pievienojot bloķēšanas mehānismu visām metodēm, ko mēs saucam par "Dictionary + Locks".

Bet tagad mums ir ConcurrentDictionary. MSDN vārdnīcas klases dokumentācijas pavedienu drošā aprakstā norādīts, ka, ja nepieciešams izmantot pavedienu drošu ieviešanu, izmantojiet ConcurrentDictionary.

Tātad, tagad, kad mums ir pavedienu droša vārdnīcas klase, mums tā vairs nav jāievieš pašiem. Lieliski, vai ne?

Problēmas izcelsme

Patiesībā es esmu izmantojis CocurrentDictionary tikai vienu reizi, lai pārbaudītu tā reaģētspēju. Tā kā tas labi darbojās testos, es to uzreiz nomainīju ar savu klasi, veicu dažus testus, un tad kaut kas nogāja greizi.

Tātad, kas nogāja greizi? Vai jūs neteicāt, ka pavediens ir drošs?

Pēc papildu testēšanas es atradu problēmas sakni. Taču kāda iemesla dēļ MSDN versijā 4.0 nav iekļauts metodes GetOrAdd paraksta apraksts, kas prasa pārstāvja tipa parametra nodošanu. Apskatot versiju 4.5, es atradu šo piezīmi:

Ja jūs izsaucat GetOrAdd vienlaicīgi dažādos pavedienos, addValueFactory var tikt izsaukts vairākas reizes, bet tā atslēgas/vērtības pāris var netikt pievienots vārdnīcai katram zvanam.
Tā ir problēma, ar kuru es saskāros. Tā kā tas iepriekš nebija aprakstīts dokumentācijā, man bija jāveic vairāk testu, lai apstiprinātu problēmu. Protams, problēma, ar kuru es saskaros, ir saistīta ar manu lietojumu, kopumā es izmantoju vārdnīcas tipu, lai kešatmiņā saglabātu dažus datus:

Šie dati ir ļoti lēni veidojami;
Šos datus var izveidot tikai vienu reizi, jo otrais radījums radīs izņēmumu, vai vairāki radījumi var izraisīt resursu noplūdi utt.;
Man bija problēma ar otro nosacījumu. Ja abi pavedieni konstatē, ka datu daļa nepastāv, tas tiks izveidots vienu reizi, bet tikai viens rezultāts tiks veiksmīgi saglabāts. Kā ar otru?

Ja izveidotais process rada izņēmumu, varat izmantot izmēģināt: nozveja (nav pietiekami eleganta, bet tas atrisina problēmu). Bet ko darīt, ja resurss tiek radīts, nevis pārstrādāts?

Varētu teikt, ka objekts ir izveidots un tiks savākts atkritumi, ja tajā vairs nav atsauces. Tomēr apsveriet, kas notiktu, ja notiktu tālāk aprakstītā situācija:

Ģenerējiet kodu dinamiski, izmantojot Emit. Es izmantoju šo pieeju Remoting sistēmā un ievietoju visas implementācijas montāžā, kuru nevarēja pārstrādāt. Ja tips tiek izveidots divas reizes, otrais vienmēr pastāvēs, pat ja tas nekad nav ticis izmantots.
Izveidojiet pavedienu tieši vai netieši. Piemēram, mums ir jāizveido komponents, kas izmanto patentētu pavedienu, lai apstrādātu asinhronus ziņojumus, un paļaujas uz to saņemšanas secību. Kad komponents ir instancēts, tiek izveidots pavediens. Kad šī komponenta instance tiek iznīcināta, arī pavediens tiek pārtraukts. Bet, ja mēs izdzēšam atsauci uz objektu pēc komponenta iznīcināšanas, bet pavediens kāda iemesla dēļ nebeidzas un satur atsauci uz objektu. Tad, ja pavediens nemirst, arī objekts netiks pārstrādāts.
Veiciet P/Invoke operāciju. Pieprasiet, lai saņemtā roktura aizvēršanas reižu skaits būtu tāds pats kā atvērumu skaits.
Protams, ir daudz līdzīgu situāciju. Piemēram, vārdnīcas objekts uzturēs savienojumu ar attālā servera pakalpojumu, ko var pieprasīt tikai vienu reizi, un, ja tas tiek pieprasīts otrreiz, otrs pakalpojums domās, ka ir radusies kāda veida kļūda, un reģistrēs to žurnālā. (Uzņēmumā, kurā es strādāju, par šo stāvokli bija daži juridiski sodi.) )
Tātad, ir viegli redzēt, ka vārdnīcu + slēdzenes nevar steigā aizstāt ar ConcurrentDictionary, pat ja dokumentācijā teikts, ka tas ir drošs pavedieniem.

Analizējiet problēmu

Joprojām nesaprotat?

Ir taisnība, ka šī problēma var nerasties saskaņā ar Dictionary + Locks pieeju. Tā kā tas ir atkarīgs no konkrētās ieviešanas, apskatīsim šo vienkāršo piemēru:


Iepriekš minētajā kodā mēs turam vārdnīcas slēdzeni, pirms sākam vaicāt atslēgas vērtību. Ja norādītais atslēgas un vērtības pāris nepastāv, tas tiks izveidots tieši. Tajā pašā laikā, tā kā mēs jau esam bloķējuši šo vārdnīcu, mēs varam pievienot atslēgas un vērtības pārus tieši vārdnīcai. Pēc tam atlaidiet vārdnīcas bloķēšanu un atgrieziet rezultātu. Ja divi pavedieni vienlaicīgi vaicā vienu un to pašu atslēgas vērtību, pirmais pavediens, kas saņem vārdnīcas bloķēšanu, pabeigs objekta izveidi, bet otrs pavediens gaidīs šīs izveides pabeigšanu un pēc vārdnīcas bloķēšanas iegūšanas iegūs izveidotās atslēgas vērtības rezultātu.

Tas ir labi, vai ne?

Tas tiešām nav! Es nedomāju, ka tāda objekta izveide paralēli, kur galu galā tiek izmantots tikai viens, nerada manis aprakstīto problēmu.

Situācija un problēma, ko es cenšos izvērst, ne vienmēr ir reproducējama, paralēlā vidē mēs varam vienkārši izveidot divus objektus un pēc tam atmest vienu. Tātad, kā tieši mēs salīdzinām vārdnīcu + slēdzenes un ConcurrentDictionary?

Atbilde ir: tas ir atkarīgs no slēdzenes lietošanas stratēģijas un vārdnīcas izmantošanas.

1. spēle: izveidojiet vienu un to pašu objektu paralēli

Pirmkārt, pieņemsim, ka objektu var izveidot divas reizes, tad kas notiek, ja divi pavedieni rada šo objektu vienlaikus?

Otrkārt, cik ilgu laiku mēs pavadām līdzīgiem darbiem?

Mēs varam vienkārši izveidot piemēru, kur objekta instancēšana aizņem 10 sekundes. Kad pirmais pavediens izveido objektu 5 sekundes vēlāk, otrā implementācija mēģina izsaukt GetOrAdd metodi, lai iegūtu objektu, un, tā kā objekts joprojām nepastāv, tas arī sāk veidot objektu.

Šajā stāvoklī mums ir 2 procesori, kas strādā paralēli 5 sekundes, un, kad pirmais pavediens beidz darboties, otrajam pavedienam joprojām jāturpina darboties 5 sekundes, lai pabeigtu objekta būvniecību. Kad otrais pavediens pabeidz objekta veidošanu, tas konstatē, ka objekts jau pastāv, un izvēlas izmantot esošo objektu un tieši atmest jaunizveidoto objektu.

Ja otrais pavediens vienkārši gaida un otrais CPU veic kādu citu darbu (palaižot citus pavedienus vai lietojumprogrammas, ietaupot enerģiju), tas iegūs vēlamo objektu pēc 5 sekundēm, nevis 10 sekundēm.

Tātad, šādos apstākļos vārdnīca + slēdzenes uzvar nelielu spēli.

2. spēle: paralēli apmeklējiet dažādus objektus

Nē, jūsu teiktā situācija nemaz nav taisnība!

Nu, iepriekš minētais piemērs ir mazliet savdabīgs, bet tas apraksta problēmu, tikai šis lietojums ir ekstrēmāks. Tātad, apsveriet, kas notiek, ja pirmais pavediens veido objektu, un otrajam pavedienam ir jāpiekļūst citam atslēgas-vērtības objektam, un šis atslēgas-vērtības objekts jau pastāv?

ConcurrentDictionary dizains bez bloķēšanas padara lasīšanu ļoti ātru, jo lasīšana nav bloķēta. Vārdnīcas + slēdzenes gadījumā lasīšanas operācija tiks bloķēta savstarpēji izslēdzoši, pat ja tā ir pilnīgi cita atslēga, kas acīmredzami palēninās lasīšanas darbību.

Tādā veidā ConcurrentDictionary atvilka spēli.

Piezīme: Šeit es uzskatu, ka jūs saprotat vairākus jēdzienus, piemēram, Bucket / Node / Entry vārdnīcas klasē, ja nē, ieteicams izlasīt Ofir Makmal rakstu "Izpratne par vispārējo vārdnīcu padziļināti", kas labi izskaidro šos jēdzienus.

Trešā spēle spēles: lasīt vairāk un rakstīt vienu

Kas notiek, ja vārdnīcā + slēdzenes vārdnīcas pilnīgas bloķēšanas vietā izmantojat vairākus lasītājus un vienu rakstītāju?

Ja pavediens rada objektu un tur jaunināmu bloķēšanu, līdz objekts tiek izveidots, slēdzene tiek jaunināta uz rakstīšanas slēdzeni, tad lasīšanas operāciju var veikt paralēli.

Mēs varam arī atrisināt problēmu, atstājot lasīšanas operāciju dīkstāvē 10 sekundes. Bet, ja ir daudz vairāk lasījumu nekā raksta, mēs atradīsim, ka ConcurrentDictionary joprojām ir ātrs, jo tas ievieš bezbloķēšanas režīma lasījumus.

Izmantojot ReaderWriterLockSlim vārdnīcām, lasīšana pasliktinās, un parasti ieteicams lietot pilnu bloķēšanu vārdnīcām, nevis ReaderWriterLockSlim.

Tātad, šādos apstākļos, ConcurrentDictionary uzvarēja vēl vienu spēli.

Piezīme: Iepriekšējos rakstos esmu aptvēris YieldReaderWriterLock un YieldReaderWriterLockSlim klases. Izmantojot šo lasīšanas-rakstīšanas slēdzeni, ātrums ir ievērojami uzlabots (tagad attīstījies par SpinReaderWriterLockSlim) un ļauj paralēli izpildīt vairākus lasījumus ar nelielu ietekmi vai bez tās. Kamēr es joprojām izmantoju šo veidu, ConcurrentDictionary bez bloķēšanas acīmredzot būtu ātrāka.

4. spēle: pievienojiet vairākus atslēgas un vērtības pārus

Izrāde vēl nav beigusies.

Ko darīt, ja mums ir jāpievieno vairākas atslēgas vērtības, un tās visas nesaduras un tiek piešķirtas dažādos spaiņos?

Sākumā šis jautājums bija ziņkārīgs, bet es veicu testu, kas ne visai derēja. Es izmantoju <int, int> tipa vārdnīcu, un objekta būvniecības rūpnīca atgrieza negatīvu rezultātu tieši kā atslēgu.

Es gaidīju, ka ConcurrentDictionary būs ātrākais, bet tas izrādījās lēnākais. No otras puses, vārdnīca + slēdzenes darbojas ātrāk. Kāpēc?

Tas ir tāpēc, ka ConcurrentDictionary piešķir mezglus un ievieto tos dažādos spaiņos, kas ir optimizēti, lai atbilstu lasīšanas operāciju dizainam bez bloķēšanas. Tomēr, pievienojot atslēgas vērtības vienumus, mezgla izveides process kļūst dārgs.

Pat paralēlos apstākļos mezgla bloķēšanas piešķiršana joprojām patērē vairāk laika nekā pilnas bloķēšanas izmantošana.

Tātad, vārdnīca + slēdzenes uzvar šo spēli.

Piektās spēles spēlēšana: lasīšanas operāciju biežums ir lielāks

Atklāti sakot, ja mums būtu delegāts, kas varētu ātri instancēt objektus, mums nebūtu vajadzīga vārdnīca. Mēs varam tieši piezvanīt delegātam, lai iegūtu objektu, vai ne?

Patiesībā atbilde ir arī tāda, ka tas ir atkarīgs no situācijas.

Iedomājieties, ka atslēgas tips ir virkne un satur ceļu kartes dažādām tīmekļa servera lapām, un atbilstošā vērtība ir objekta tips, kas satur ierakstu par pašreizējiem lietotājiem, kas piekļūst lapai, un visu lapas apmeklējumu skaitu kopš servera startēšanas.

Šāda objekta izveide ir gandrīz tūlītēja. Un pēc tam jums nav nepieciešams izveidot jaunu objektu, vienkārši mainiet tajā saglabātās vērtības. Tātad ir iespējams atļaut izveidot ceļu divas reizes, līdz tiek izmantots tikai viens gadījums. Tomēr, tā kā ConcurrentDictionary mezglu resursus piešķir lēnāk, izmantojot Dictionary + Locks, izveides laiks būs ātrāks.

Tātad, ar šo piemēru ir ļoti īpašs, mēs arī redzam, ka vārdnīca + slēdzenes šajā stāvoklī darbojas labāk, aizņemot mazāk laika.

Lai gan mezglu sadalījums ConcurrentDictionary ir lēnāks, es nemēģināju tajā ievietot 100 miljonus datu vienumu, lai pārbaudītu laiku. Jo tas acīmredzot aizņem daudz laika.

Bet vairumā gadījumu, kad datu vienums ir izveidots, tas vienmēr tiek lasīts. Kā mainās datu vienuma saturs, ir cits jautājums. Tāpēc nav svarīgi, cik milisekundes vairāk nepieciešams, lai izveidotu datu vienumu, jo lasīšana ir ātrāka (tikai dažas milisekundes ātrāka), bet lasīšana notiek biežāk.

Tātad, ConcurrentDictionary uzvarēja spēli.

6. spēle: izveidojiet objektus, kas patērē dažādus laikus

Kas notiek, ja dažādu datu vienumu izveidei nepieciešamais laiks atšķiras?

Izveidojiet vairākus datu vienumus, kas patērē dažādus laikus, un pievienojiet tos vārdnīcai paralēli. Tas ir ConcurrentDictionary spēcīgākais punkts.

ConcurrentDictionary izmanto vairākus dažādus bloķēšanas mehānismus, lai ļautu vienlaicīgi pievienot datu vienumus, bet loģika, piemēram, izlemt, kuru slēdzeni izmantot, pieprasīt slēdzeni, lai mainītu spaiņa lielumu utt., nepalīdz. Ātrums, kādā datu vienumi tiek ievietoti spainī, ir ātrs. Tas, kas patiešām padara ConcurrentDictionary uzvarētāju, ir tā spēja izveidot objektus paralēli.

Tomēr mēs patiesībā varam darīt to pašu. Ja mums ir vienalga, vai mēs veidojam objektus paralēli vai ja daži no tiem ir atmesti, mēs varam pievienot slēdzeni, lai noteiktu, vai datu vienums jau pastāv, pēc tam atlaidiet slēdzeni, izveidojiet datu vienumu, nospiediet to, lai iegūtu bloķēšanu, vēlreiz pārbaudiet, vai datu vienums pastāv, un, ja tā nav, pievienojiet datu vienumu. Kods varētu izskatīties apmēram šādi:

* Ņemiet vērā, ka es izmantoju <int, int> tipa vārdnīcu.

Iepriekš minētajā vienkāršajā struktūrā vārdnīca + slēdzenes darbojas gandrīz tikpat labi kā ConcurrentDictionary, veidojot un pievienojot datu vienumus paralēlos apstākļos. Bet ir arī tāda pati problēma, kur dažas vērtības var tikt ģenerētas, bet nekad netiek izmantotas.

Secinājums

Tātad, vai ir secinājums?

Šajā brīdī joprojām ir daži:

Visas vārdnīcas klases ir ļoti ātras. Lai gan esmu izveidojis miljoniem datu, tas joprojām ir ātrs. Parasti mēs izveidojam tikai nelielu skaitu datu vienumu, un starp lasījumiem ir daži laika intervāli, tāpēc mēs parasti nepamanām datu vienumu lasīšanas laika pieskaitāmās izmaksas.
Ja vienu un to pašu objektu nevar izveidot divas reizes, neizmantojiet ConcurrentDictionary.
Ja jūs patiešām uztrauc veiktspēja, vārdnīca + slēdzenes joprojām varētu būt labs risinājums. Svarīgs faktors ir pievienoto un noņemto datu vienumu skaits. Bet, ja ir daudz lasīšanas operāciju, tas ir lēnāks nekā ConcurrentDictionary.
Lai gan es to neieviesu, patiesībā ir lielāka brīvība izmantot Dictionary + Locks shēmu. Piemēram, varat bloķēt vienu reizi, pievienot vairākus datu vienumus, dzēst vairākus datu vienumus vai vaicāt vairākas reizes utt., un pēc tam atbrīvot bloķēšanu.
Kopumā izvairieties no ReaderWriterLockSlim izmantošanas, ja ir daudz vairāk lasījumu nekā rakstītu. Vārdnīcu tipi jau ir daudz ātrāki nekā lasīšanas bloķēšana lasīšanas-rakstīšanas slēdzenē. Protams, tas ir atkarīgs arī no laika, kas patērēts, lai izveidotu objektu slēdzenē.
Tātad, es domāju, ka sniegtie piemēri ir nedaudz ekstrēmi, bet tie parāda, ka ConcurrentDictionary izmantošana ne vienmēr ir labākais risinājums.

Sajūtiet atšķirību

Es rakstīju šo rakstu ar nolūku meklēt labāku risinājumu.

Es jau cenšos iegūt dziļāku izpratni par to, kā darbojas konkrēta vārdnīcas klase (tagad man šķiet, ka esmu ļoti skaidrs).

Iespējams, spainis un mezgls ConcurrentDictionary ir ļoti vienkārši. Es darīju kaut ko līdzīgu, kad mēģināju izveidot vārdnīcas klasi. Parastā vārdnīcas klase var šķist vienkāršāka, bet patiesībā tā ir sarežģītāka.

ConcurrentDictionary katrs mezgls ir pilna klase. Vārdnīcas klasē Node tiek ieviests, izmantojot vērtību tipu, un visi mezgli tiek turēti milzīgā masīvā, bet Bucket tiek izmantots, lai indeksētu masīvu. To izmanto arī mezgla vienkāršās atsauces uz nākamo mezglu vietā (galu galā, kā struktūras tipa mezgls, tas nevar saturēt struktūras tipa mezgla locekli).

Pievienojot un noņemot vārdnīcu, vārdnīcas klase nevar vienkārši izveidot jaunu mezglu, tai ir jāpārbauda, vai ir indekss, kas atzīmē izdzēsto mezglu, un pēc tam to atkārtoti izmantot. Vai arī "Count" tiek izmantots, lai iegūtu jaunā mezgla pozīciju masīvā. Faktiski, kad masīvs ir pilns, vārdnīcas klase piespiež mainīt izmēru.

ConcurrentDictionary mezglu var uzskatīt par jaunu objektu. Mezgla noņemšana nozīmē vienkārši tā atsauces noņemšanu. Pievienojot jaunu mezglu, var vienkārši izveidot jaunu mezgla instanci. Lieluma maiņa ir tikai tāpēc, lai izvairītos no konfliktiem, bet tā nav obligāta.

Tātad, ja vārdnīcas klase mērķtiecīgi izmanto sarežģītākus algoritmus, lai to apstrādātu, kā ConcurrentDictionary nodrošinās, ka tā darbojas labāk vairāku pavedienu vidē?

Patiesība ir tāda: visu mezglu ievietošana vienā masīvā ir ātrākais veids, kā piešķirt un lasīt, pat ja mums ir nepieciešams cits masīvs, lai izsekotu, kur atrast šos datu elementus. Tātad izskatās, ka tāds pats spaiņu skaits aizņems vairāk atmiņas, bet jaunie datu vienumi nav jāpārdala, nav nepieciešama jauna objektu sinhronizācija un jauna atkritumu savākšana nenotiek. Jo viss jau ir savā vietā.

Tomēr satura aizstāšana mezglā nav atomu darbība, kas ir viens no faktoriem, kas padara tā pavedienu nedrošu. Tā kā mezgli ir visi objekti, sākotnēji tiek izveidots mezgls, un pēc tam tiek atjaunināta atsevišķa atsauce, lai norādītu uz to (atomu darbība šeit). Tātad, lasītais pavediens var lasīt vārdnīcas saturu bez slēdzenes, un lasījumam jābūt vienai no vecajām un jaunajām vērtībām, un nav iespēju lasīt nepilnīgu vērtību.

Tātad, patiesība ir tāda: ja jums nav nepieciešama slēdzene, vārdnīcas klase ir ātrāka lasīšanā, jo tā ir slēdzene, kas palēnina lasīšanu.

Šis raksts ir tulkots no Paulo Zemek raksta "Dictionary + Locking versus ConcurrentDictionary" vietnē CodeProject, un daži apgalvojumi mainīsies izpratnes dēļ.







Iepriekšējo:IoC efektīvs Autofac
Nākamo:Alibaba 4 cilvēki tika atlaisti par JS skriptu izmantošanu, lai steidzās iegādāties mēness kūkas
 Saimnieks| Publicēts 13.09.2016 13:33:15 |
ConcurrentDictionary atbalsta jaunus un atjauninātus atjauninājumus
http://www.itsvse.com/thread-2955-1-1.html
(Avots: Code Agriculture Network)
Atruna:
Visa programmatūra, programmēšanas materiāli vai raksti, ko publicē Code Farmer Network, ir paredzēti tikai mācību un pētniecības mērķiem; Iepriekš minēto saturu nedrīkst izmantot komerciāliem vai nelikumīgiem mērķiem, pretējā gadījumā lietotājiem ir jāuzņemas visas sekas. Informācija šajā vietnē nāk no interneta, un autortiesību strīdiem nav nekāda sakara ar šo vietni. Iepriekš minētais saturs ir pilnībā jāizdzēš no datora 24 stundu laikā pēc lejupielādes. Ja jums patīk programma, lūdzu, atbalstiet oriģinālu programmatūru, iegādājieties reģistrāciju un iegūstiet labākus oriģinālus pakalpojumus. Ja ir kādi pārkāpumi, lūdzu, sazinieties ar mums pa e-pastu.

Mail To:help@itsvse.com