Tento článek je zrcadlovým článkem o strojovém překladu, klikněte zde pro přechod na původní článek.

Pohled: 20809|Odpověď: 1

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

[Kopírovat odkaz]
Zveřejněno 13.09.2016 13:33:04 | | |
Před .NET 4.0, pokud jsme potřebovali použít třídu Slovník v vícevláknovém prostředí, neměli jsme jinou možnost než implementovat synchronizaci vláken sami, abychom vlákna ochránili.

Mnoho vývojářů jistě implementovalo podobné řešení bezpečné pro vlákna, buď vytvořením zcela nového typu slovníku bezpečným pro vlákna, nebo jednoduše zapouzdřením objektu slovníku ve třídě a přidáním mechanismu uzamykaní ke všem metodám, který nazýváme "Slovník + zámky".

Ale teď máme ConcurrentDictionary. Popis třídy Dictionary safe v dokumentaci Dictionary na MSDN uvádí, že pokud potřebujete použít implementaci bezpečnou pro vlákna, použijte ConcurrentDictionary.

Takže teď, když máme třídu slovníku bezpečnou pro vlákna, už ji nemusíme sami implementovat. Skvělé, že?

Původ problému

Ve skutečnosti jsem CocurrentDictionary použil jen jednou předtím, v testu k otestování jeho odezvy. Protože si v testech vedl dobře, hned jsem ho nahradil svou třídou, udělal jsem testy a pak se něco pokazilo.

Tak co se pokazilo? Neříkal jsi, že je to bezpečné pro nit?

Po dalším testování jsem našel kořen problému. Ale z nějakého důvodu verze MSDN 4.0 neobsahuje popis podpisu metody GetOrAdd, který vyžaduje předání parametru typu delegát. Po prohlédnutí verze 4.5 jsem našel tuto poznámku:

Pokud voláte GetOrAdd současně na různých vláknech, addValueFactory může být volán vícekrát, ale jeho pár klíč/hodnota nemusí být přidán do slovníku pro každé volání.
To je ten problém, na který jsem narazil. Protože to nebylo dříve popsáno v dokumentaci, musel jsem provést další testování, abych problém potvrdil. Samozřejmě, problém, na který narážím, souvisí s mým používáním, obecně používám typ slovníku k ukládání některých dat:

Tato data se vytvářejí velmi pomalu;
Tato data lze vytvořit pouze jednou, protože druhé vytvoření vyvolá výjimku, nebo více vytvořených dat může vést k úniku zdrojů atd.;
Měl jsem problém s druhou podmínkou. Pokud obě vlákna zjistí, že nějaký údaj neexistuje, bude vytvořen jednou, ale úspěšně se uloží pouze jeden výsledek. A co ten druhý?

Pokud proces, který vytvoříte, vyvolá výjimku, můžete použít try: Chytit (není dost elegantní, ale problém to řeší). Ale co když je zdroj vytvořen a nerecyklován?

Můžete říct, že objekt je vytvořen a bude vyhozen do garbage collect, pokud už v něm není odkazován. Nicméně zvažte, co by se stalo, kdyby nastala situace popsaná níže:

Generujte kód dynamicky pomocí Emitu. Tento přístup jsem použil v rámci pro vzdálené operace a všechny implementace jsem dal do assembleru, který nešlo recyklovat. Pokud je typ vytvořen dvakrát, druhý bude vždy existovat, i když nikdy nebyl použit.
Vytvořte vlákno přímo nebo nepřímo. Například musíme vytvořit komponentu, která používá proprietární vlákno ke zpracování asynchronních zpráv a spoléhá na pořadí, v jakém jsou přijaty. Když je komponenta instancována, vytvoří se vlákno. Když je tato instance komponenty zničena, vlákno je také ukončeno. Ale pokud smažeme odkaz na objekt po zničení komponenty, vlákno z nějakého důvodu nekončí a odkaz na objekt zůstává. Pokud pak nit neodumře, předmět nebude recyklován také.
Proveďte operaci P/Invoke. Požaduj, aby počet uzavřených časů pro přijatou rukojeť byl stejný jako počet otevření.
Jistě, existuje mnoho podobných situací. Například slovníkový objekt bude držet spojení se službou na vzdáleném serveru, které lze požádat pouze jednou, a pokud je požádán podruhé, druhá služba si myslí, že došlo k nějaké chybě, a zaznamená ji do logu. (Ve firmě, kde jsem pracoval, byly za tento stav nějaké právní sankce.) )
Je tedy jasné, že Dictionary + Locks nelze narychlo nahradit ConcurrentDictionary, i když dokumentace tvrdí, že je to bezpečné pro vlákna.

Analyzujte problém

Pořád tomu nerozumíš?

Je pravda, že tento problém nemusí vzniknout v rámci přístupu Slovník + Zámky. Protože to závisí na konkrétní implementaci, podívejme se na tento jednoduchý příklad:


V uvedeném kódu držíme zámek na slovníku před zahájením dotazování na hodnotu klíče. Pokud zadaný pár klíč-hodnota neexistuje, bude vytvořen přímo. Zároveň, protože už máme na tento slovník zámek, můžeme do slovníku přímo přidávat dvojice klíč-hodnota. Pak uvolněte slovníkový zámek a vraťte výsledek. Pokud dvě vlákna současně dotazují stejnou klíčovou hodnotu, první vlákno, které získá slovníkový zámek, dokončí vytvoření objektu a druhé vlákno počká na dokončení tohoto vytvoření a po získání slovníkového zámku získá výsledek vytvořené klíčové hodnoty.

To je dobře, že?

Opravdu není! Nemyslím si, že vytvoření objektu paralelně, kde se nakonec použije jen jeden, nevytvoří problém, který jsem popsal.

Situace a problém, které se snažím rozvést, nemusí být vždy reprodukovatelné, v paralelním prostředí můžeme jednoduše vytvořit dva objekty a jeden zahodit. Jak tedy přesně porovnáváme Dictionary + Locks a ConcurrentDictionary?

Odpověď zní: záleží na strategii používání zámků a na tom, jak se slovník používá.

Hra 1: Vytvořte stejný objekt paralelně

Nejprve předpokládejme, že objekt lze vytvořit dvakrát, co se stane, když tento objekt vytvoří dvě vlákna současně?

Za druhé, jak dlouho trávíme podobnými výtvory?

Můžeme jednoduše vytvořit příklad, kde instancování objektu trvá 10 sekund. Když první vlákno vytvoří objekt o 5 sekund později, druhá implementace se pokusí zavolat metodu GetOrAdd k získání objektu, a protože objekt stále neexistuje, začne ho také vytvářet.

V tomto stavu máme 2 CPU pracující paralelně po dobu 5 sekund, a když první vlákno dokončí práci, druhé vlákno musí pokračovat v běhu ještě 5 sekund, aby dokončilo konstrukci objektu. Když druhé vlákno dokončí stavbu objektu, zjistí, že objekt již existuje, a rozhodne se použít existující objekt a nově vytvořený objekt přímo zahodit.

Pokud druhé vlákno prostě počká a druhý CPU udělá jinou práci (spustí jiná vlákna nebo aplikace, ušetří energii), dostane požadovaný objekt za 5 sekund místo za 10 sekund.

Za těchto podmínek tedy Dictionary + Locks vyhrává malou hru.

Hra 2: Navštěvujte různé objekty paralelně

Ne, ta situace, kterou jste popsal, vůbec není pravda!

No, výše uvedený příklad je trochu zvláštní, ale popisuje problém, jen toto použití je extrémnější. Zvažte tedy, co se stane, když první vlákno vytváří objekt a druhé vlákno potřebuje přístup k jinému objektu klíč-hodnota, a ten objekt s klíčovou hodnotou už existuje?

V ConcurrentDictionary umožňuje bezzámkový design čtení velmi rychlé, protože na čtení není žádný zámek. V případě Dictionary + Locks bude operace čtení vzájemně vylučující, i když jde o zcela jiný klíč, což samozřejmě zpomalí operaci čtení.

Tímto způsobem ConcurrentDictionary stáhl hru zpět.

Poznámka: Zde považuji za to, že rozumíte několika konceptům jako Bucket/Node/Entry ve třídě slovníku, pokud ne, doporučuje se přečíst si článek Ofira Makmala "Understanding Generalic Dictionary in-depth", který tyto koncepty dobře vysvětluje.

Třetí hra hry: čtěte více a pište single

Co se stane, když použijete Více čteček a Single Writer místo úplného uzamčení slovníku ve Slovníku + zámku?

Pokud vlákno vytváří objekt a drží upgradovatelný zámek až do jeho vytvoření, zámek je upgradován na zápisový zámek, pak lze čtení provádět paralelně.

Problém můžeme také vyřešit tím, že necháme čtení nečinnou 10 sekund. Ale pokud je čtení než zápisů mnohem více, zjistíme, že ConcurrentDictionary je stále rychlý, protože implementuje čtení v režimu bez zámku.

Používání ReaderWriterLockSlim pro slovníky zhoršuje čtení, a obecně se doporučuje používat Full Lock pro slovníky místo ReaderWriterLockSlim.

Za těchto podmínek tedy ConcurrentDictionary vyhrál další hru.

Poznámka: V předchozích článcích jsem se věnoval třídám YieldReaderWriterLock a YieldReaderWriterSlim. Použitím tohoto zámku čtení a zápisu byla rychlost výrazně zlepšena (nyní se vyvinula v SpinReaderWriterLockSlim) a umožňuje provádět více čtení paralelně s minimálním nebo žádným dopadem. I když to stále používám, bezzámkový ConcurrentDictionary by byl samozřejmě rychlejší.

Hra 4: Přidání více párů klíč-hodnota

Souboj ještě neskončil.

Co když máme více klíčových hodnot k přičtení, ale všechny se nesrážejí a jsou přiřazeny do různých kategorií?

Zpočátku byla tato otázka zvědavá, ale udělal jsem test, který úplně neseděl. Použil jsem slovník typu <int, int> a konstrukční továrna objektu vracela záporný výsledek přímo jako klíč.

Čekal jsem, že ConcurrentDictionary bude nejrychlejší, ale nakonec byl nejpomalejší. Dictionary + Locks naopak funguje rychleji. Proč tomu tak je?

Je to proto, že ConcurrentDictionary přiděluje uzly a umisťuje je do různých bucketů, což je optimalizováno tak, aby vyhovovalo návrhu bez zámků pro čtecí operace. Při přidávání položek klíč-hodnota se však proces vytváření uzlu stává nákladným.

I za paralelních podmínek přidělování uzlového zámku stále zabere více času než použití plného zámku.

Takže Dictionary + Locks tuto hru vyhrává.

Hraní páté hry: Frekvence čtení je vyšší

Upřímně, kdybychom měli delegáta, který dokáže rychle instancovat objekty, nepotřebovali bychom slovník. Můžeme přímo zavolat delegáta, aby získal objekt, že?

Ve skutečnosti je odpověď také taková, že záleží na situaci.

Představte si, že typ klíče je string a obsahuje mapy cest pro různé stránky na webovém serveru, přičemž odpovídající hodnota je typ objektu, který obsahuje záznam aktuálních uživatelů přistupujících ke stránce a počet všech návštěv stránky od spuštění serveru.

Vytvoření takového objektu je téměř okamžité. A pak už nemusíte vytvářet nový objekt, stačí změnit uložené hodnoty. Je tedy možné umožnit vytvoření cesty dvakrát, dokud nebude použita pouze jedna instance. Nicméně, protože ConcurrentDictionary alokuje zdroje uzlů pomaleji, použití Dictionary+ Locks povede k rychlejšímu tvorbě.

Takže u tohoto velmi speciálního příkladu vidíme, že Dictionary+ Locks funguje lépe za této podmínky, zabere to méně času.

Ačkoliv je alokace uzlů v ConcurrentDictionary pomalejší, nesnažil jsem se do něj vložit 100 milionů datových položek, abych otestoval čas. Protože to samozřejmě zabere hodně času.

Ale ve většině případů, jakmile je datová položka vytvořena, je vždy čtena. Jak se obsah datové položky mění, je jiná věc. Takže nezáleží na tom, kolik milisekund více trvá vytvoření datového prvku, protože čtení jsou rychlejší (jen o pár milisekund rychlejší), ale čtení probíhají častěji.

Takže ConcurrentDictionary vyhrál hru.

Hra 6: Vytvářejte objekty, které spotřebovávají různé časy

Co se stane, když se čas potřebný k vytvoření různých datových položek liší?

Vytvořte více datových položek, které spotřebovávají různě, a přidávejte je do slovníku paralelně. To je nejsilnější stránka ConcurrentDictionary.

ConcurrentDictionary používá řadu různých mechanismů uzamykaní, které umožňují současně přidávat datové položky, ale logika jako rozhodování, který zámek použít, žádost o změnu velikosti kbelíku apod., nepomáhá. Rychlost, jakou jsou datové položky vloženy do kbelíku, je strojově rychlá. To, co ConcurrentDictionary skutečně dělá vítězným, je jeho schopnost vytvářet objekty paralelně.

Nicméně můžeme udělat totéž. Pokud nám nevadí, jestli vytváříme objekty paralelně, nebo jestli některé z nich byly zahozeny, můžeme přidat zámek, který detekuje, zda datový prvek již existuje, pak zámek uvolnit, vytvořit datový prvek, stisknout ho pro získání zámku, znovu zkontrolovat, zda datový prvek existuje, a pokud ne, přidat datový prvek. Kód by mohl vypadat asi takto:

* Všimněte si, že používám slovník typu <int, int>.

V jednoduché struktuře výše funguje Dictionary + Locks téměř stejně dobře jako ConcurrentDictionary při vytváření a přidávání datových položek za paralelních podmínek. Ale je tu i stejný problém, kdy některé hodnoty mohou být generovány, ale nikdy nepoužity.

závěr

Takže, existuje nějaký závěr?

V tuto chvíli jsou stále některé:

Všechny kurzy slovníku jsou velmi rychlé. I když jsem vytvořil miliony dat, je to stále rychlé. Obvykle vytváříme jen malý počet datových položek a mezi čteními jsou určité časové intervaly, takže si obvykle nevšímáme časové režie při čtení datových položek.
Pokud nelze vytvořit stejný objekt dvakrát, nepoužívejte ConcurrentDictionary.
Pokud vám opravdu záleží na výkonu, Dictionary + Locks může být stále dobré řešení. Důležitým faktorem je počet přidaných a odebraných datových položek. Pokud je však čtení mnoho operací, je to pomalejší než ConcurrentDictionary.
I když jsem to nezavedl, ve skutečnosti je větší volnost používat schéma Slovník + Zámky. Například můžete jednou zamknout, přidat více datových položek, smazat více datových položek nebo se několikrát dotazovat atd., a pak zámek uvolnit.
Obecně se vyhněte používání ReaderWriterLockSlim, pokud je přečtených mnohem více než napsaných. Typy slovníků jsou už mnohem rychlejší než získání zámku čtení v systému čtení a zápisu. Samozřejmě to závisí i na čase potřebném k vytvoření objektu v zámku.
Myslím, že uvedené příklady jsou trochu extrémní, ale ukazují, že použití ConcurrentDictionary není vždy nejlepší řešení.

Pocítíte rozdíl

Tento článek jsem napsal s úmyslem najít lepší řešení.

Už teď se snažím lépe pochopit, jak konkrétní kurz slovníku funguje (teď mám pocit, že jsem velmi jasný).

Dá se říci, že Bucket a Node v ConcurrentDictionary jsou velmi jednoduché. Udělal jsem něco podobného, když jsem se snažil vytvořit kurz slovníku. Běžná třída Slovník se může zdát jednodušší, ale ve skutečnosti je složitější.

V ConcurrentDictionary je každý uzel kompletní třídou. Ve třídě Slovník je Node implementován pomocí typu hodnoty a všechny uzly jsou uloženy v obrovském poli, zatímco Bucket se používá k indexování v poli. Používá se také místo jednoduché reference uzlu na jeho další uzel (koneckonců, jako uzel typu struktury nemůže obsahovat člena uzlu typu struktury).

Při přidávání a odebírání slovníku nemůže třída Dictionary jednoduše vytvořit nový uzel, musí zkontrolovat, zda existuje index označující smazaný uzel, a poté jej znovu použít. Nebo se "Count" používá k získání pozice nového uzlu v poli. Ve skutečnosti, když je pole plné, třída Dictionary vynutí změnu velikosti.

Pro ConcurrentDictionary lze uzel chápat jako nový objekt. Odstranění uzlu znamená jednoduše odstranění jeho odkazu. Přidání nového uzlu může jednoduše vytvořit novou instanci uzlu. Změna velikosti je pouze proto, aby se předešlo konfliktům, ale není povinná.

Pokud tedy třída Dictionary záměrně používá složitější algoritmy k jeho zpracování, jak zajistí ConcurrentDictionary, že bude fungovat lépe v vícevláknovém prostředí?

Pravda je: umístit všechny uzly do jednoho pole je nejrychlejší způsob, jak alokovat a číst, i když potřebujeme další pole, abychom sledovali, kde data najít. Takže to vypadá, že stejný počet bucketů spotřebuje více paměti, ale nové datové položky nemusí být přesouvány, nejsou potřeba synchronizace nových objektů a nové garbage collection nevzniká. Protože všechno už je připravené.

Nahrazení obsahu v Node však není atomickou operací, což je jeden z faktorů, proč je jeho vlákno nebezpečné. Protože uzly jsou všechny objekty, nejprve se vytvoří uzel a poté se aktualizuje samostatná reference, která na něj ukazuje (zde atomová operace). Vlákno čtení tedy může číst obsah slovníku bez zámku a čtení musí být jedna ze starých a nových hodnot, přičemž není možné přečíst neúplnou hodnotu.

Pravda je taková: pokud nepotřebujete zámek, třída Slovník je rychlejší při čtení, protože právě zámek zpomaluje čtení.

Tento článek je přeložen z článku Paula Zemeka "Dictionary + Locking versus ConcurrentDictionary" na CodeProject a některá tvrzení se změní z důvodu pochopení.







Předchozí:Efektivní Autofac pro IoC
Další:Lidé z Alibaby 4 byli propuštěni za to, že používali JS skripty k rychlému nákupu měsíčních koláčků
 Pronajímatel| Zveřejněno 13.09.2016 13:33:15 |
ConcurrentDictionary podporuje nové i aktualizované aktualizace
http://www.itsvse.com/thread-2955-1-1.html
(Zdroj: Code Agriculture Network)
Zřeknutí se:
Veškerý software, programovací materiály nebo články publikované organizací Code Farmer Network slouží pouze k učení a výzkumu; Výše uvedený obsah nesmí být používán pro komerční ani nelegální účely, jinak nesou všechny důsledky uživatelé. Informace na tomto webu pocházejí z internetu a spory o autorská práva s tímto webem nesouvisí. Musíte výše uvedený obsah ze svého počítače zcela smazat do 24 hodin od stažení. Pokud se vám program líbí, podporujte prosím originální software, kupte si registraci a získejte lepší skutečné služby. Pokud dojde k jakémukoli porušení, kontaktujte nás prosím e-mailem.

Mail To:help@itsvse.com