Stel dat er een klasse is die een publiek int-tellerveld bevat dat door meerdere threads toegankelijk is, en dit aantal zal alleen maar toenemen of dalen.
Bij het toevoegen van dit veld, welke van de volgende schema's moeten worden gebruikt, en waarom?
- lock(this.locker) this.counter++;
- Interlocked.Increment (ref this.counter);
- Verander de toegangsmodificator van counter naar publieke volatiel
Het slechtste (geen van hen werkt echt)
Verander de toegangsmodificator van counter naar publieke volatiel
Deze aanpak is eigenlijk helemaal niet veilig, en het punt van volatile is dat meerdere threads die op meerdere CPU's draaien data bufferen en uitgevoerde instructies herschikken.
Als het niet-vluchtig is, moet CPU B even wachten om de verhoogde waarde te zien wanneer CPU A met een waarde toeneemt, wat tot problemen kan leiden.
Als het vluchtig is, zorgt het ervoor dat beide CPU's dezelfde waarde tegelijk zien. Maar het vermijdt niet cross-cutting lees- en schrijfbewerkingen.
Waarde toevoegen aan een variabele vereist eigenlijk drie stappen
1. Lezen, 2. Voeg toe 3. Schrijf
Stel dat thread A de waarde van teller als 1 leest en niet klaar is om te verhogen, dan leest thread B ook de waarde van teller als 1, en beginnen beide threads incrementele en schrijfbewerkingen uit te voeren. De waarde van de laatste teller is 2. Dit klopt niet, beide threads hebben een verhogingsoperatie uitgevoerd, en het juiste resultaat zou 3 moeten zijn. Dus het labelen als vluchtig is simpelweg onveilig.
Het is beter
lock(this.locker) this.counter++;
Op deze manier is het veilig (vergeet niet om overal waar je bij deze counter wilt komen te vergrendelen, natuurlijk). Het voorkomt dat andere threads de vergrendelde code uitvoeren. En het voorkomt ook het hierboven genoemde multi-CPU instructiesequencing-probleem. Het probleem is dat lock traag is in prestaties, en als je lock op andere, niet-gerelateerde plekken gebruikt, kan het je andere threads blokkeren.
Beste
Interlocked.Increment (ref this.counter);
Dit is veilig en zeer efficiënt. Het voert lees-, verhoog- en schrijfbewerkingen uit in één atoom zonder onderbroken te worden in het midden. Omdat het andere code niet beïnvloedt, hoef je elders geen sloten te onthouden. En het is ook erg snel (zoals MSDN zegt, is het op de CPU's van vandaag vaak gewoon een instructie).
Maar ik weet niet helemaal zeker of het ook het instructievolgordeprobleem van de CPU kan oplossen, of dat het in combinatie met vluchtig en deze increment gebruikt moet worden.
Supplement: Welke problemen lost vluchtig goed op?
Aangezien vluchtig multithreading niet kan voorkomen, wat kan het dan doen? Een goed voorbeeld is dat je twee threads hebt: de ene schrijft altijd naar een variabele, stel dat deze variabele queneLength is, en de andere leest altijd data uit deze variabele. Als queueLenght niet vluchtig is, kan thread A 5 keer lezen, maar thread B kan vertraagde data zien, of zelfs data in de verkeerde volgorde. Een oplossing is het gebruik van lock, maar in dit geval kun je ook vluchtig zijn. Dit zorgt ervoor dat thread B altijd de nieuwste data ziet die door thread A is geschreven, maar deze logica werkt alleen als je het niet leest tijdens het schrijven, en als je het niet schrijft wanneer je het leest. Zodra meerdere threads lees-wijzigen-schrijfbewerking willen uitvoeren, moet je Interlocked of lock gebruiken.
|