Denna artikel är en spegelartikel om maskinöversättning, klicka här för att hoppa till originalartikeln.

Utsikt: 20809|Svar: 1

[Källa] ConcurrentDictionary vs. Dictionary+Locking - Dennis Gao

[Kopiera länk]
Publicerad på 2016-09-13 13:33:04 | | |
Före .NET 4.0, om vi behövde använda Dictionary-klassen i en multitrådad miljö, hade vi inget annat val än att själva implementera trådsynkronisering för att hålla trådarna säkra.

Många utvecklare har definitivt implementerat en liknande trådsäker lösning, antingen genom att skapa en helt ny trådsäker ordbokstyp, eller helt enkelt kapsla in ett ordboksobjekt i en klass och lägga till en låsmekanism till alla metoder, som vi kallar "Dictionary + Locks".

Men nu har vi ConcurrentDictionary. Den trådsäkra beskrivningen av Dictionary-klassdokumentationen på MSDN säger att om du behöver använda en trådsäker implementation, använd ConcurrentDictionary.

Så, nu när vi har en trådsäker ordboksklass behöver vi inte längre implementera den själva. Toppen, eller hur?

Problemets ursprung

Faktum är att jag bara har använt CocurrentDictionary en gång tidigare, i mitt test för att testa dess respons. Eftersom det presterade bra på proven bytte jag genast ut det mot min klass, gjorde några tester, och sedan gick något fel.

Så, vad gick fel? Sa du inte trådsäker?

Efter mer testande hittade jag roten till problemet. Men av någon anledning innehåller MSDN version 4.0 inte en beskrivning av GetOrAdd-metodens signatur som kräver att man skickar en delegattypparameter. Efter att ha tittat på version 4.5 hittade jag denna notis:

Om du anropar GetOrAdd samtidigt i olika trådar kan addValueFactory anropas flera gånger, men dess nyckel/värde-par kanske inte läggs till i ordboken för varje anrop.
Det är problemet jag stötte på. Eftersom det inte tidigare beskrevs i dokumentationen var jag tvungen att göra fler tester för att bekräfta problemet. Självklart, problemet jag stöter på är relaterat till min användning, generellt använder jag ordbokstypen för att cacha viss data:

Denna data är mycket långsam att skapa;
Denna data kan bara skapas en gång, eftersom den andra skapelsen kommer att utlösa ett undantag, eller flera skapelser kan leda till resursläckage, etc.;
Jag hade problem med det andra tillståndet. Om båda trådarna upptäcker att en databit inte existerar kommer den att skapas en gång, men endast ett resultat kommer att sparas framgångsrikt. Vad sägs om den andra?

Om processen du skapar ger ett undantag kan du använda försök: Catch (inte tillräckligt elegant, men det löser problemet). Men vad händer om en resurs skapas och inte återvinns?

Du kan säga att ett objekt skapas och kommer att samlas in som skräp om det inte längre refereras i det. Men fundera på vad som skulle hända om den situation som beskrivs nedan inträffade:

Generera kod dynamiskt med Emit. Jag använde detta tillvägagångssätt i ett Remoting-ramverk och lade alla implementationer i en assembler som inte kunde återanvändas. Om en typ skapas två gånger kommer den andra alltid att existera, även om den aldrig har använts.
Skapa en tråd direkt eller indirekt. Till exempel behöver vi bygga en komponent som använder en proprietär tråd för att behandla asynkrona meddelanden och förlitar sig på i vilken ordning de tas emot. När komponenten instansieras skapas en tråd. När denna komponentinstans förstörs avslutas även tråden. Men om vi tar bort referensen till objektet efter att komponenten förstörts, så slutar tråden av någon anledning inte och behåller referensen till objektet. Om tråden inte dör kommer föremålet inte heller att återvinnas.
Utför en P/Invoke-operation. Kräv att antalet stängda tider för det mottagna handtaget måste vara detsamma som antalet öppningar.
Visserligen finns det många liknande situationer. Till exempel kommer ett ordboksobjekt att hålla en anslutning till en tjänst på en fjärrserver, vilket bara kan begäras en gång, och om det begärs en andra gång kommer den andra tjänsten att tro att något slags fel har inträffat och logga det i loggen. (På ett företag jag arbetade för fanns det vissa juridiska påföljder för detta tillstånd.) )
Så det är lätt att se att Dictionary + Locks inte kan ersättas snabbt med ConcurrentDictionary, även om dokumentationen säger att det är trådsäkert.

Analysera problemet

Förstår du fortfarande inte?

Det är sant att detta problem kanske inte uppstår under Dictionary + Locks-metoden. Eftersom detta beror på den specifika implementationen, låt oss titta på detta enkla exempel:


I koden ovan håller vi låset på ordboken innan vi börjar fråga nyckelvärdet. Om det angivna nyckel-värde-paret inte existerar, kommer det att skapas direkt. Samtidigt, eftersom vi redan har lås på den ordboken, kan vi lägga till nyckel-värde-par direkt i ordboken. Släpp sedan ordbokslåset och returnera resultatet. Om två trådar frågar samma nyckelvärde samtidigt, kommer den första tråden som får ordbokslåset att slutföra skapandet av objektet, och den andra tråden väntar på att denna skapelse är klar och får resultatet av det skapade nyckelvärdet efter att ha fått ordbokslåset.

Det är väl bra, eller hur?

Det är det verkligen inte! Jag tror inte att det skapar det problem jag beskrivit att skapa ett parallellt objekt där bara ett används i slutändan.

Situationen och problemet jag försöker utveckla är kanske inte alltid reproducerbart, i en parallell miljö kan vi helt enkelt skapa två objekt och sedan kassera ett. Så, hur exakt jämför vi Dictionary + Locks och ConcurrentDictionary?

Svaret är: det beror på låsets användningsstrategi och hur ordboken används.

Spel 1: Skapa samma objekt parallellt

Först, låt oss anta att ett objekt kan skapas två gånger, så vad händer om två trådar skapar detta objekt samtidigt?

För det andra, hur lång tid lägger vi på liknande skapelser?

Vi kan helt enkelt bygga ett exempel där det tar 10 sekunder att instansiera ett objekt. När den första tråden skapar objektet 5 sekunder senare försöker den andra implementationen anropa GetOrAdd-metoden för att hämta objektet, och eftersom objektet fortfarande inte existerar börjar den också skapa objektet.

I detta tillstånd har vi 2 CPU:er som arbetar parallellt i 5 sekunder, och när den första tråden är klar måste den andra tråden fortfarande fortsätta köras i 5 sekunder för att slutföra konstruktionen av objektet. När den andra tråden är klar med att bygga objektet upptäcker den att ett objekt redan existerar, och väljer att använda det befintliga objektet och kassera det nyskapade objektet direkt.

Om den andra tråden bara väntar och den andra CPU:n gör något annat arbete (kör andra trådar eller applikationer, sparar lite ström), kommer den att få det önskade objektet efter 5 sekunder istället för 10 sekunder.

Så, under dessa förhållanden, vinner Dictionary + Locks ett litet spel.

Spel 2: Besök olika objekt parallellt

Nej, situationen du nämnde stämmer inte alls!

Exempelt ovan är lite märkligt, men det beskriver problemet, det är bara det att denna användning är mer extrem. Så, tänk på vad som händer om den första tråden skapar ett objekt, och den andra tråden behöver komma åt ett annat nyckelvärdeobjekt, och det nyckelvärdesobjektet redan existerar?

I ConcurrentDictionary gör den låsfria designen läsningarna mycket snabba eftersom det inte finns något lås på läsningen. I fallet med Dictionary + Locks kommer läsoperationen att vara ömsesidigt uteslutande, även om det är en helt annan nyckel, vilket uppenbarligen kommer att sakta ner läsoperationen.

På så sätt drog ConcurrentDictionary tillbaka ett spel.

Notera: Här anser jag att du förstår flera begrepp såsom Hink/Nod/Inträde i ordboksklassen, annars rekommenderas det att läsa Ofir Makmals artikel "Understanding Generic Dictionary in-depth", som förklarar dessa begrepp väl.

Spelets tredje spel: läs mer och skriv singel

Vad händer om du använder Multiple Readers och Single Writer istället för ett fullständigt lås på ordboken i Dictionary + Locks?

Om en tråd skapar ett objekt och håller ett uppgraderingsbart lås tills objektet skapas, låset uppgraderas till ett skrivlås, då kan läsoperationen utföras parallellt.

Vi kan också lösa problemet genom att låta en läsoperation vara inaktiv i 10 sekunder. Men om det är betydligt fler läsningar än skrivningar kommer vi att upptäcka att ConcurrentDictionary fortfarande är snabb eftersom den implementerar låsfria läsningar.

Att använda ReaderWriterLockSlim för ordböcker gör läsningarna sämre, och det rekommenderas generellt att använda Full Lock för ordböcker istället för ReaderWriterLockSlim.

Så, under dessa förhållanden, vann ConcurrentDictionary ytterligare ett spel.

Notera: Jag har tagit upp YieldReaderWriterLock- och YieldReaderWriterLockSlim-klasserna i tidigare artiklar. Genom att använda detta läs-skriv-lås har hastigheten förbättrats avsevärt (nu utvecklat till SpinReaderWriterLockSlim) och möjliggör att flera läsningar kan utföras parallellt med liten eller ingen effekt. Även om jag fortfarande använder det här sättet skulle en låslös ConcurrentDictionary uppenbarligen vara snabbare.

Spel 4: Lägg till flera nyckelvärdepar

Uppgörelsen är inte över än.

Vad händer om vi har flera nyckelvärden att lägga till, och alla inte kolliderar utan tilldelas olika kategorier?

Till en början var den här frågan nyfiken, men jag gjorde ett test som inte riktigt passade. Jag använde en ordbok av typen <int, int> och objektets konstruktionsfabrik gav ett negativt resultat direkt som nyckel.

Jag förväntade mig att ConcurrentDictionary skulle vara snabbast, men det visade sig vara det långsammaste. Dictionary + Locks, å andra sidan, presterar snabbare. Varför är det så?

Detta beror på att ConcurrentDictionary allokerar noder och placerar dem i olika hinkar, vilket är optimerat för att uppfylla den låsfria designen för läsoperationer. Men när man lägger till nyckelvärdesobjekt blir processen att skapa en nod dyr.

Även under parallella förhållanden tar det mer tid att allokera ett Node-lås än att använda ett fullständigt lås.

Så, Dictionary + Locks vinner det här spelet.

Att spela det femte spelet: Frekvensen av läsoperationer är högre

Ärligt talat, om vi hade en delegat som snabbt kunde instansiera objekt, skulle vi inte behöva en ordbok. Vi kan direkt ringa delegaten för att hämta objektet, eller hur?

Faktum är att svaret också är att det beror på situationen.

Föreställ dig att nyckeltypen är sträng och innehåller vägkartor för olika sidor på webbservern, och motsvarande värde är en objekttyp som innehåller posten för de nuvarande användarna som använder sidan och antalet besök på sidan sedan servern startade.

Att skapa ett sådant här objekt sker nästan omedelbart. Och efter det behöver du inte skapa ett nytt objekt, bara ändra de värden som sparats i det. Så det är möjligt att tillåta skapandet av ett sätt två gånger tills endast en instans används. Eftersom ConcurrentDictionary dock allokerar nodresurser långsammare, kommer användning av Dictionary + Locks att resultera i snabbare skapandetider.

Så, med detta exempel är mycket speciellt, ser vi också att Dictionary + Locks fungerar bättre under dessa förhållanden och tar mindre tid.

Även om nodallokeringen i ConcurrentDictionary är långsammare, försökte jag inte lägga in 100 miljoner dataobjekt för att testa tiden. För det tar uppenbarligen mycket tid.

Men i de flesta fall, när en databit väl har skapats, läses den alltid. Hur innehållet i dataobjektet förändras är en annan sak. Så det spelar ingen roll hur många millisekunder det tar att skapa ett dataobjekt, eftersom läsningarna är snabbare (bara några millisekunder snabbare), men läsningarna sker oftare.

Så, ConcurrentDictionary vann spelet.

Spel 6: Skapa objekt som tar upp olika tider

Vad händer om tiden det tar att skapa olika dataobjekt varierar?

Skapa flera dataobjekt som tar olika tider och lägg till dem parallellt i ordboken. Detta är den starkaste punkten i ConcurrentDictionary.

ConcurrentDictionary använder flera olika låsmekanismer för att tillåta att dataobjekt läggs till samtidigt, men logik som att bestämma vilket lås som ska användas, begära att ett lås ändrar hinkens storlek, etc., hjälper inte. Hastigheten med vilken dataobjekt läggs i en hink är maskinsnabb. Det som verkligen gör ConcurrentDictionary framgångsrikt är dess förmåga att skapa objekt parallellt.

Men vi kan faktiskt göra samma sak. Om vi inte bryr oss om vi skapar objekt parallellt, eller om några av dem har kasserats, kan vi lägga till ett lås för att upptäcka om dataobjektet redan finns, sedan släppa låset, skapa dataobjektet, trycka på det för att få låset, kontrollera igen om dataobjektet finns, och om det inte gör det, lägga till dataobjektet. Koden kan se ut ungefär så här:

* Observera att jag använder en ordbok av typen <int, int>.

I den enkla strukturen ovan fungerar Dictionary + Locks nästan lika bra som ConcurrentDictionary när man skapar och lägger till dataobjekt under parallella förhållanden. Men det finns också samma problem, där vissa värden kan genereras men aldrig användas.

slutsats

Så, finns det en slutsats?

Just nu finns det fortfarande några:

Alla ordboksklasser är väldigt snabba. Även om jag har skapat miljontals data går det fortfarande snabbt. Normalt skapar vi bara ett litet antal dataobjekt, och det finns vissa tidsintervall mellan läsningarna, så vi märker generellt inte tidsbelastningen för att läsa dataobjekt.
Om samma objekt inte kan skapas två gånger, använd inte ConcurrentDictionary.
Om du verkligen är orolig för prestandan kan Dictionary + Locks fortfarande vara en bra lösning. En viktig faktor är antalet dataobjekt som lagts till och tas bort. Men om det finns många läsoperationer är det långsammare än ConcurrentDictionary.
Även om jag inte introducerade det, finns det faktiskt mer frihet att använda Dictionary + Locks-schemat. Till exempel kan du låsa en gång, lägga till flera dataobjekt, ta bort flera dataobjekt, eller fråga flera gånger, osv., och sedan släppa låset.
Generellt bör du undvika att använda ReaderWriterLockSlim om det finns fler lässaker än skriver. Ordbokstyper är redan mycket snabbare än att få ett läslås i ett läs-skriv-lås. Naturligtvis beror detta också på den tid som krävs för att skapa ett objekt i ett lås.
Så jag tycker att exemplen som ges är lite extrema, men de visar att ConcurrentDictionary inte alltid är den bästa lösningen.

Känn skillnaden

Jag skrev den här artikeln med avsikten att söka en bättre lösning.

Jag försöker redan få en djupare förståelse för hur en specifik ordboksklass fungerar (nu känner jag att jag är väldigt tydlig).

Man kan hävda att Bucket och Node i ConcurrentDictionary är mycket enkla. Jag gjorde något liknande när jag försökte skapa en ordboksklass. Den vanliga ordboksklassen kan verka enklare, men i själva verket är den mer komplex.

I ConcurrentDictionary är varje nod en komplett klass. I Dictionary-klassen implementeras Node med en värdetyp, och alla noder hålls i en stor array, medan Bucket används för att indexera i arrayen. Den används också istället för en Nodes enkla referens till dess nästa Nod (trots allt, som en Nod av en strukturtyp kan den inte innehålla en nodmedlem av en strukturtyp).

När man lägger till och tar bort en ordbok kan Dictionary-klassen inte bara skapa en ny nod, den måste kontrollera om det finns ett index som markerar en nod som har tagits bort, och sedan återanvända den. Eller så används "Count" för att få positionen för den nya Noden i arrayen. Faktum är att när arrayen är full, tvingar Dictionary-klassen fram en storleksförändring.

För ConcurrentDictionary kan en nod betraktas som ett nytt objekt. Att ta bort en nod är helt enkelt att ta bort dess referens. Att lägga till en ny Node kan helt enkelt skapa en ny Node-instans. Att ändra storleken är bara för att undvika konflikter, men det är inte obligatoriskt.

Så, om Dictionary-klassen medvetet använder mer komplexa algoritmer för att hantera den, hur ska ConcurrentDictionary då säkerställa att den presterar bättre i en multitrådad miljö?

Sanningen är: att lägga alla noder i en array är det snabbaste sättet att allokera och läsa, även om vi behöver en annan array för att hålla koll på var vi hittar dessa dataobjekt. Så det verkar som att samma antal hinkar kommer att använda mer minne, men de nya dataobjekten behöver inte omfördelas, inga nya objektsynkroniseringar behövs och ny skräpsamling sker inte. För allt är redan på plats.

Att ersätta innehåll i en nod är dock inte en atomär operation, vilket är en av faktorerna som gör dess tråd osäker. Eftersom noder alla är objekt skapas initialt en nod, och sedan uppdateras en separat referens för att peka på den (atomär operation här). Så lästråden kan läsa ordboksinnehållet utan lås, och läsningen måste vara ett av de gamla och nya värdena, och det finns ingen risk att läsa ett ofullständigt värde.

Så, sanningen är: om du inte behöver ett lås är Dictionary-klassen snabbare vid läsningar, eftersom det är låset som saktar ner läsningen.

Denna artikel är översatt från Paulo Zemeks artikel "Dictionary + Locking versus ConcurrentDictionary" på CodeProject, och vissa påståenden kommer att ändras av förståelseskäl.







Föregående:IoC-effektiv Autofac
Nästa:Alibaba 4-personer blev avskedade för att de använde JS-skript för att skynda sig att köpa månkakor
 Hyresvärd| Publicerad på 2016-09-13 13:33:15 |
ConcurrentDictionary stödjer nya och uppdaterade uppdateringar
http://www.itsvse.com/thread-2955-1-1.html
(Källa: Code Agriculture Network)
Friskrivning:
All programvara, programmeringsmaterial eller artiklar som publiceras av Code Farmer Network är endast för lärande- och forskningsändamål; Ovanstående innehåll får inte användas för kommersiella eller olagliga ändamål, annars kommer användarna att bära alla konsekvenser. Informationen på denna sida kommer från internet, och upphovsrättstvister har inget med denna sida att göra. Du måste helt radera ovanstående innehåll från din dator inom 24 timmar efter nedladdning. Om du gillar programmet, vänligen stöd äkta programvara, köp registrering och få bättre äkta tjänster. Om det finns något intrång, vänligen kontakta oss via e-post.

Mail To:help@itsvse.com