Denne artikel er en spejling af maskinoversættelse, klik venligst her for at springe til den oprindelige artikel.

Udsigt: 20809|Svar: 1

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

[Kopier link]
Opslået på 13/09/2016 13.33.04 | | |
Før .NET 4.0, hvis vi skulle bruge Dictionary-klassen i et multitrådet miljø, havde vi intet andet valg end selv at implementere trådsynkronisering for at holde trådene sikre.

Mange udviklere har bestemt implementeret en lignende trådsikker løsning, enten ved at skabe en helt ny trådsikker ordbogstype eller blot ved at indkapsle et ordbogsobjekt i en klasse og tilføje en låsemekanisme til alle metoder, som vi kalder "Dictionary + Locks".

Men nu har vi ConcurrentDictionary. Den trådsikre beskrivelse af Dictionary-klassedokumentationen på MSDN siger, at hvis du har brug for at bruge en trådsikker implementering, skal du bruge ConcurrentDictionary.

Så nu hvor vi har en trådsikker ordbogsklasse, behøver vi ikke længere implementere den selv. Fantastisk, ikke?

Problemets oprindelse

Faktisk har jeg kun brugt CocurrentDictionary én gang før, i min test for at teste dens responsivitet. Fordi den klarede sig godt i prøverne, erstattede jeg den straks med min klasse, lavede nogle tests, og så gik noget galt.

Så, hvad gik galt? Sagde du ikke trådsikkert?

Efter yderligere tests fandt jeg roden til problemet. Men af en eller anden grund indeholder MSDN version 4.0 ikke en beskrivelse af GetOrAdd-metodesignaturen, der kræver, at man sender en delegerettypeparameter. Efter at have kigget på version 4.5 fandt jeg denne note:

Hvis du kalder GetOrAdd samtidig på forskellige tråde, kan addValueFactory blive kaldt flere gange, men dets nøgle/værdi-par tilføjes måske ikke til ordbogen for hvert kald.
Det var det problem, jeg stødte på. Fordi det ikke tidligere var beskrevet i dokumentationen, måtte jeg lave flere tests for at bekræfte problemet. Selvfølgelig er problemet, jeg støder på, relateret til mit forbrug; generelt bruger jeg ordbogstypen til at cache nogle data:

Disse data er meget langsomme at skabe;
Disse data kan kun oprettes én gang, fordi den anden oprettelse vil give en undtagelse, eller flere oprettelser kan føre til ressourcelækage osv.;
Jeg havde et problem med den anden tilstand. Hvis begge tråde opdager, at et stykke data ikke eksisterer, vil det blive oprettet én gang, men kun ét resultat vil blive gemt med succes. Hvad med den anden?

Hvis den proces, du opretter, kaster en undtagelse, kan du bruge try: Catch (ikke elegant nok, men det løser problemet). Men hvad hvis en ressource bliver skabt og ikke genanvendt?

Du kunne sige, at et objekt er oprettet og vil blive affaldsafhentet, hvis det ikke længere refereres til i det. Overvej dog, hvad der ville ske, hvis den nedenfor beskrevne situation indtraf:

Generer kode dynamisk med Emit. Jeg brugte denne tilgang i en Remote-ramme og lagde alle implementeringer i en assembly, der ikke kunne genbruges. Hvis en type skabes to gange, vil den anden altid eksistere, selvom den aldrig er blevet brugt.
Opret en tråd direkte eller indirekte. For eksempel skal vi bygge en komponent, der bruger en proprietær tråd til at behandle asynkrone beskeder og er afhængig af rækkefølgen, de modtages i. Når komponenten instansieres, oprettes en tråd. Når denne komponentinstans ødelægges, afsluttes tråden også. Men hvis vi sletter referencen til objektet efter at have ødelagt komponenten, men tråden af en eller anden grund ikke slutter og beholder referencen til objektet. Hvis tråden ikke dør, vil objektet heller ikke blive genanvendt.
Udfør en P/Invoke-operation. Bekrev, at antallet af lukkede tidspunkter for det modtagne håndtag skal være det samme som antallet af åbninger.
Der er ganske vist mange lignende situationer. For eksempel vil et ordbogsobjekt holde en forbindelse til en tjeneste på en fjernserver, som kun kan anmodes om én gang, og hvis den anmodes en anden gang, vil den anden tjeneste tro, at der er sket en fejl og logge det i loggen. (I en virksomhed, jeg arbejdede for, var der nogle juridiske sanktioner for denne tilstand.) )
Så det er let at se, at Dictionary + Locks ikke kan erstattes hurtigt med ConcurrentDictionary, selvom dokumentationen siger, at det er trådsikkert.

Analyser problemet

Forstår du det stadig ikke?

Det er rigtigt, at dette problem måske ikke opstår under Dictionary + Locks-tilgangen. Da dette afhænger af den specifikke implementering, lad os se på dette simple eksempel:


I ovenstående kode holder vi låsen på ordbogen, før vi begynder at forespørge nøgleværdien. Hvis det specificerede nøgle-værdi-par ikke eksisterer, vil det blive oprettet direkte. Samtidig, fordi vi allerede har lås på den ordbog, kan vi tilføje nøgle-værdi-par direkte til ordbogen. Derefter frigives ordbogslåsen og returneres resultatet. Hvis to tråde forespørger den samme nøgleværdi samtidig, vil den første tråd, der får ordbogslåsen, fuldføre oprettelsen af objektet, og den anden tråd vil vente på færdiggørelsen af denne oprettelse og få resultatet af den oprettede nøgleværdi efter at have fået ordbogslåsen.

Det er godt, ikke?

Det er det virkelig ikke! Jeg tror ikke, at det at skabe et objekt parallelt som dette, hvor kun ét bruges til sidst, ikke skaber det problem, jeg har beskrevet.

Den situation og det problem, jeg prøver at uddybe, er måske ikke altid reproducerbart; i et parallelt miljø kan vi simpelthen skabe to objekter og derefter kassere ét. Så hvordan sammenligner vi præcist Dictionary + Locks og ConcurrentDictionary?

Svaret er: det afhænger af strategien for låsbrugen og hvordan ordbogen bruges.

Spil 1: Skab det samme objekt parallelt

Lad os først antage, at et objekt kan oprettes to gange, så hvad sker der, hvis to tråde opretter dette objekt på samme tid?

For det andet, hvor lang tid bruger vi på lignende kreationer?

Vi kan simpelthen bygge et eksempel, hvor det tager 10 sekunder at instansiere et objekt. Når den første tråd opretter objektet 5 sekunder senere, prøver den anden implementering at kalde GetOrAdd-metoden for at hente objektet, og da objektet stadig ikke eksisterer, begynder den også at oprette objektet.

I denne tilstand har vi 2 CPU'er, der arbejder parallelt i 5 sekunder, og når den første tråd er færdig, skal den anden tråd stadig køre i 5 sekunder for at færdiggøre konstruktionen af objektet. Når den anden tråd er færdig med at bygge objektet, finder den, at et objekt allerede eksisterer, og vælger at bruge det eksisterende objekt og kassere det nyoprettede objekt direkte.

Hvis den anden tråd blot venter, og den anden CPU udfører andet arbejde (kører andre tråde eller applikationer, sparer noget strøm), vil den få det ønskede objekt efter 5 sekunder i stedet for 10 sekunder.

Så under disse betingelser vinder Dictionary + Locks et lille spil.

Spil 2: Besøg forskellige objekter parallelt

Nej, den situation du nævnte, er slet ikke sand!

Nå, ovenstående eksempel er lidt mærkeligt, men det beskriver problemet, det er bare, at denne brug er mere ekstrem. Så overvej, hvad der sker, hvis den første tråd opretter et objekt, og den anden tråd skal tilgå et andet nøgle-værdi-objekt, og det nøgle-værdi-objekt allerede eksisterer?

I ConcurrentDictionary gør det låsefri design læsningerne meget hurtige, fordi der ikke er nogen lås på læsningen. I tilfælde af Dictionary + Locks vil læseoperationen være låst gensidigt udelukkende, selvom det er en helt anden nøgle, hvilket naturligvis vil sænke læseoperationen.

På denne måde trak ConcurrentDictionary et spil tilbage.

Bemærk: Her mener jeg, at du forstår flere begreber såsom Spand/Node/Indgang i ordbogsklassen; hvis ikke, anbefales det at læse Ofir Makmals artikel "Understanding Generic Dictionary in-depth", som forklarer disse begreber godt.

Det tredje spil i spillet: læs mere og skriv enkeltstående

Hvad sker der, hvis du bruger Multiple Readers og Single Writer i stedet for en fuld lås på ordbogen i Dictionary + Locks?

Hvis en tråd opretter et objekt og holder en opgraderbar lås, indtil objektet oprettes, opgraderes låsen til en skrivelås, hvorefter læseoperationen kan udføres parallelt.

Vi kan også løse problemet ved at lade en læseoperation være inaktiv i 10 sekunder. Men hvis der er langt flere læsninger end skrivninger, vil vi opdage, at ConcurrentDictionary stadig er hurtig, fordi den implementerer læseløsninger uden lås.

Brug af ReaderWriterLockSlim til ordbøger gør læsningen værre, og det anbefales generelt at bruge Full Lock til ordbøger i stedet for ReaderWriterLockSlim.

Så under disse betingelser vandt ConcurrentDictionary endnu et spil.

Bemærk: Jeg har dækket YieldReaderWriterLock- og YieldReaderReaderLockSlim-klasserne i tidligere artikler. Ved at bruge denne læse-skrive-lås er hastigheden blevet forbedret betydeligt (nu udviklet til SpinReaderWriterLockSlim) og tillader flere læsninger parallelt med minimal eller ingen effekt. Selvom jeg stadig bruger denne metode, ville en låsløs ConcurrentDictionary selvfølgelig være hurtigere.

Spil 4: Tilføj flere nøgle-værdi par

Opgøret er ikke slut endnu.

Hvad hvis vi har flere nøgleværdier at tilføje, og alle ikke kolliderer og tildeles forskellige buckets?

I starten var dette spørgsmål nysgerrigt, men jeg lavede en test, der ikke helt passede. Jeg brugte en ordbog af typen <int, int> og objektets konstruktionsfabrik ville returnere et negativt resultat direkte som nøgle.

Jeg forventede, at ConcurrentDictionary ville være den hurtigste, men det viste sig at være den langsomste. Dictionary + Locks, derimod, præsterer hurtigere. Hvorfor er det sådan?

Dette skyldes, at ConcurrentDictionary allokerer noder og placerer dem i forskellige buckets, hvilket er optimeret til at opfylde låsefri design for læseoperationer. Men når man tilføjer nøgleværdi-elementer, bliver processen med at oprette en node dyr.

Selv under parallelle forhold kræver allokering af en nodelås stadig mere tid end brug af en fuld lås.

Så Dictionary + Locks vinder dette spil.

At spille det femte spil: Hyppigheden af læseoperationer er højere

Ærligt talt, hvis vi havde en delegeret, der hurtigt kunne instansiere objekter, ville vi ikke have brug for en ordbog. Vi kan direkte ringe til delegaten for at hente objektet, ikke?

Faktisk er svaret også, at det afhænger af situationen.

Forestil dig, at nøgletypen er streng og indeholder stikort for forskellige sider på webserveren, og den tilsvarende værdi er en objekttype, der indeholder registreringen af de nuværende brugere, der tilgår siden, samt antallet af besøg på siden siden serveren startede.

At skabe et objekt som dette sker næsten øjeblikkeligt. Og derefter behøver du ikke oprette et nyt objekt, bare ændre de værdier, der er gemt i det. Så det er muligt at tillade oprettelse af en vej to gange, indtil kun én instans bruges. Men fordi ConcurrentDictionary allokerer noderessourcer langsommere, vil brugen af Dictionary + Locks resultere i hurtigere oprettelsestider.

Så med dette eksempel er det meget specielt, vi ser også, at Dictionary + Locks klarer sig bedre under denne betingelse og tager mindre tid.

Selvom nodeallokeringen i ConcurrentDictionary er langsommere, forsøgte jeg ikke at indtaste 100 millioner dataelementer for at teste tiden. For det tager selvfølgelig meget tid.

Men i de fleste tilfælde, når et dataelement er oprettet, bliver det altid læst. Hvordan indholdet af dataelementet ændrer sig, er en anden sag. Så det er ligegyldigt, hvor mange millisekunder det tager at oprette et dataelement, fordi læsninger er hurtigere (kun et par millisekunder hurtigere), men læsningerne sker oftere.

Så ConcurrentDictionary vandt spillet.

Spil 6: Skab objekter, der bruger forskellige tider

Hvad sker der, hvis den tid, det tager at oprette forskellige dataelementer, varierer?

Opret flere dataelementer, der bruger forskellige tider, og tilføj dem parallelt til ordbogen. Dette er det stærkeste punkt ved ConcurrentDictionary.

ConcurrentDictionary bruger en række forskellige låsemekanismer for at tillade at tilføje dataelementer samtidigt, men logik som at beslutte, hvilken lås man skal bruge, anmode om en lås for at ændre størrelsen på spanden osv. hjælper ikke. Den hastighed, hvormed dataelementer lægges i en spand, er maskinhurtig. Det, der virkelig gør ConcurrentDictionary til en succes, er dets evne til at skabe objekter parallelt.

Men vi kan faktisk gøre det samme. Hvis vi er ligeglade med, om vi opretter objekter parallelt, eller om nogle af dem er blevet kasseret, kan vi tilføje en lås for at opdage, om dataelementet allerede eksisterer, så frigive låsen, oprette dataelementet, trykke på det for at få låsen, tjekke igen om dataelementet eksisterer, og hvis ikke, tilføje dataelementet. Koden kan se nogenlunde sådan ud:

* Bemærk, at jeg bruger en ordbog over typen <int, int>.

I den simple struktur ovenfor præsterer Dictionary + Locks næsten lige så godt som ConcurrentDictionary, når man opretter og tilføjer dataelementer under parallelle betingelser. Men der er også det samme problem, hvor nogle værdier kan blive genereret, men aldrig brugt.

konklusion

Så, er der en konklusion?

Lige nu er der stadig nogle:

Alle ordbogsklasser er meget hurtige. Selvom jeg har skabt millioner af data, er det stadig hurtigt. Normalt opretter vi kun et lille antal dataelementer, og der er nogle tidsintervaller mellem læsningerne, så vi bemærker generelt ikke tidsoverhead ved at læse dataelementer.
Hvis det samme objekt ikke kan oprettes to gange, skal ConcurrentDictionary ikke bruges.
Hvis du virkelig er bekymret for ydeevnen, kan Dictionary + Locks stadig være en god løsning. En vigtig faktor er antallet af dataelementer, der tilføjes og fjernes. Men hvis der er mange læseoperationer, er det langsommere end ConcurrentDictionary.
Selvom jeg ikke introducerede det, er der faktisk mere frihed til at bruge Dictionary + Locks-ordningen. For eksempel kan du låse én gang, tilføje flere dataelementer, slette flere dataelementer eller forespørge flere gange osv., og så frigive låsen.
Generelt bør du undgå at bruge ReaderWriterLockSlim, hvis der er langt flere læsninger end skriver. Ordbogstyper er allerede meget hurtigere end at få en læselås i en læse-skriv lås. Selvfølgelig afhænger dette også af den tid, det tager at oprette et objekt i en lås.
Så jeg synes, de givne eksempler er lidt ekstreme, men de viser, at brugen af ConcurrentDictionary ikke altid er den bedste løsning.

Mærk forskellen

Jeg skrev denne artikel med intentionen om at finde en bedre løsning.

Jeg prøver allerede at få en dybere forståelse af, hvordan et bestemt ordbogsfag fungerer (nu føler jeg, at jeg er meget klar).

Man kan argumentere for, at Bucket og Node i ConcurrentDictionary er meget simple. Jeg gjorde noget lignende, da jeg prøvede at lave et ordbogskursus. Den almindelige ordbogsklasse kan virke simplere, men i virkeligheden er den mere kompleks.

I ConcurrentDictionary er hver Node en komplet klasse. I Dictionary-klassen er Node implementeret med en værditype, og alle noder opbevares i et stort array, mens Bucket bruges til at indeksere i arrayet. Den bruges også i stedet for en Nodes simple reference til dens næste Node (trods alt kan den som Node af en struct-type ikke indeholde et Node-medlem af en struct-type).

Når en ordbog tilføjes og fjernes, kan Dictionary-klassen ikke blot oprette en ny node; den skal kontrollere, om der er et indeks, der markerer en slettet node, og derefter genbruge den. Eller "Count" bruges til at få positionen af den nye Node i arrayet. Faktisk, når arrayet er fuldt, tvinger Dictionary-klassen en størrelsesændring.

For ConcurrentDictionary kan en Node betragtes som et nyt objekt. At fjerne en Node er simpelthen at fjerne dens reference. At tilføje en ny Node kan blot oprette en ny Node-instans. At ændre størrelsen er kun for at undgå konflikter, men det er ikke obligatorisk.

Så hvis Dictionary-klassen bevidst bruger mere komplekse algoritmer til at håndtere det, hvordan vil ConcurrentDictionary så sikre, at den præsterer bedre i et multitrådet miljø?

Sandheden er: at samle alle noderne i ét array er den hurtigste måde at allokere og læse på, selvom vi har brug for et andet array til at holde styr på, hvor vi kan finde dataelementerne. Så det ser ud til, at det at have samme antal buckets vil bruge mere hukommelse, men de nye dataelementer behøver ikke at blive omfordelt, der er ikke behov for nye objektsynkroniseringer, og ny garbage collection sker ikke. For alt er allerede på plads.

Dog er udskiftning af indhold i en Node ikke en atomar operation, hvilket er en af de faktorer, der gør dens tråd usikker. Da noder alle er objekter, oprettes en node oprindeligt, og derefter opdateres en separat reference til at pege på den (atomar operation her). Så læsetråden kan læse ordbogens indhold uden lås, og læseværdien skal være en af de gamle og nye værdier, og der er ingen chance for at læse en ufuldstændig værdi.

Så sandheden er: hvis du ikke har brug for en lås, er ordbogsklassen hurtigere på læsninger, fordi det er låsen, der sænker læsningen.

Denne artikel er oversat fra Paulo Zemeks artikel "Dictionary + Locking versus ConcurrentDictionary" på CodeProject, og nogle udsagn vil ændre sig af forståelsesmæssige årsager.







Tidligere:IoC-effektiv Autofac
Næste:Alibaba 4-personer blev fyret for at bruge JS-scripts til at skynde sig at købe månekager
 Udlejer| Opslået på 13/09/2016 13.33.15 |
ConcurrentDictionary understøtter nye og opdaterede opdateringer
http://www.itsvse.com/thread-2955-1-1.html
(Kilde: Code Agriculture Network)
Ansvarsfraskrivelse:
Al software, programmeringsmaterialer eller artikler udgivet af Code Farmer Network er kun til lærings- og forskningsformål; Ovenstående indhold må ikke bruges til kommercielle eller ulovlige formål, ellers skal brugerne bære alle konsekvenser. Oplysningerne på dette site kommer fra internettet, og ophavsretstvister har intet med dette site at gøre. Du skal slette ovenstående indhold fuldstændigt fra din computer inden for 24 timer efter download. Hvis du kan lide programmet, så understøt venligst ægte software, køb registrering og få bedre ægte tjenester. Hvis der er nogen overtrædelse, bedes du kontakte os via e-mail.

Mail To:help@itsvse.com