Aanbevolen procedures voor beheerde threading
Multithreading vereist zorgvuldig programmeren. Voor de meeste taken kunt u de complexiteit verminderen door aanvragen in de wachtrij te plaatsen voor uitvoering door threadpoolthreads. In dit onderwerp worden moeilijkere situaties behandeld, zoals het coördineren van het werk van meerdere threads of het afhandelen van threads die blokkeren.
Notitie
Vanaf .NET Framework 4 bieden de taakparallelbibliotheek en PLINQ API's die een deel van de complexiteit en risico's van programmeren met meerdere threads verminderen. Zie Parallel programmeren in .NET voor meer informatie.
Impasses en raceomstandigheden
Met multithreading worden problemen met doorvoer en reactiesnelheid opgelost, maar hierdoor worden nieuwe problemen geïntroduceerd: impasses en racevoorwaarden.
Impasses
Er treedt een impasse op wanneer elk van twee threads probeert een resource te vergrendelen die de andere al heeft vergrendeld. Geen van beide threads kan verdere vooruitgang boeken.
Veel methoden van de beheerde threadingklassen bieden time-outs waarmee u impasses kunt detecteren. Met de volgende code wordt bijvoorbeeld geprobeerd een vergrendeling op een object met de naam lockObject
te verkrijgen. Als de vergrendeling niet wordt verkregen in 300 milliseconden, Monitor.TryEnter retourneert false
.
If Monitor.TryEnter(lockObject, 300) Then
Try
' Place code protected by the Monitor here.
Finally
Monitor.Exit(lockObject)
End Try
Else
' Code to execute if the attempt times out.
End If
if (Monitor.TryEnter(lockObject, 300)) {
try {
// Place code protected by the Monitor here.
}
finally {
Monitor.Exit(lockObject);
}
}
else {
// Code to execute if the attempt times out.
}
Racevoorwaarden
Een racevoorwaarde is een bug die optreedt wanneer het resultaat van een programma afhankelijk is van welke van twee of meer threads eerst een bepaald codeblok bereikt. Het uitvoeren van het programma produceert verschillende resultaten en het resultaat van een bepaalde uitvoering kan niet worden voorspeld.
Een eenvoudig voorbeeld van een racevoorwaarde is het verhogen van een veld. Stel dat een klasse een statisch privéveld (gedeeld in Visual Basic) heeft dat steeds wordt verhoogd wanneer een exemplaar van de klasse wordt gemaakt, met behulp van code zoals objCt++;
(C#) of objCt += 1
(Visual Basic). Deze bewerking vereist het laden van de waarde in objCt
een register, het verhogen van de waarde en het opslaan ervan in objCt
.
In een multithreaded-toepassing kan een thread die de waarde heeft geladen en verhoogd, worden verpest door een andere thread die alle drie de stappen uitvoert; wanneer de eerste thread de uitvoering hervat en de waarde ervan opslaat, wordt deze overschreven objCt
zonder rekening te houden met het feit dat de waarde in de tussentijd is gewijzigd.
Deze specifieke rasvoorwaarde wordt gemakkelijk vermeden door methoden van de Interlocked klasse te gebruiken, zoals Interlocked.Increment. Zie Syncizing Data for Multithreading (Gegevens synchroniseren voor multithreading) voor meer informatie over andere technieken voor het synchroniseren van gegevens tussen meerdere threads.
Racevoorwaarden kunnen ook optreden wanneer u de activiteiten van meerdere threads synchroniseert. Wanneer u een regel code schrijft, moet u rekening houden met wat er kan gebeuren als een thread werd afgekeurd voordat de regel werd uitgevoerd (of vóór een van de afzonderlijke computerinstructies waaruit de regel bestaat) en een andere thread deze overhaakt.
Statische leden en statische constructors
Een klasse wordt pas geïnitialiseerd als de klasseconstructor (static
constructor in C#, Shared Sub New
in Visual Basic) is uitgevoerd. Om te voorkomen dat code wordt uitgevoerd op een type dat niet is geïnitialiseerd, blokkeert de algemene taalruntime alle aanroepen van andere threads naar static
leden van de klasse (Shared
leden in Visual Basic) totdat de klasseconstructor is uitgevoerd.
Als een klasseconstructor bijvoorbeeld een nieuwe thread start en de threadprocedure een static
lid van de klasse aanroept, wordt de nieuwe thread geblokkeerd totdat de klasseconstructor is voltooid.
Dit is van toepassing op elk type dat een static
constructor kan hebben.
Het aantal processors
Of er nu meerdere processors of slechts één processor beschikbaar zijn op een systeem, kan invloed hebben op de multithreaded-architectuur. Zie Aantal processors voor meer informatie.
Gebruik de Environment.ProcessorCount eigenschap om het aantal processors te bepalen dat tijdens de runtime beschikbaar is.
Algemene aanbevelingen
Houd rekening met de volgende richtlijnen bij het gebruik van meerdere threads:
Gebruik dit niet Thread.Abort om andere threads te beëindigen. Het aanroepen van
Abort
een andere thread is vergelijkbaar met het genereren van een uitzondering op die thread, zonder te weten welk punt die thread heeft bereikt in de verwerking.Gebruik de activiteiten van meerdere threads niet en Thread.Resume synchroniseer deze nietThread.Suspend. MutexManualResetEventGebruik , , en AutoResetEventMonitor.
Beheer de uitvoering van werkthreads niet vanuit uw hoofdprogramma (bijvoorbeeld met behulp van gebeurtenissen). Ontwerp in plaats daarvan uw programma zodat werkthreads verantwoordelijk zijn voor het wachten totdat het werk beschikbaar is, het uitvoeren ervan en het melden van andere onderdelen van uw programma wanneer u klaar bent. Als uw werkrolthreads niet worden geblokkeerd, kunt u threads voor threadgroepen gebruiken. Monitor.PulseAll is handig in situaties waarin werkthreads worden geblokkeerd.
Gebruik geen typen als vergrendelingsobjecten. Dat wil gezegd: vermijd code, zoals
lock(typeof(X))
in C# ofSyncLock(GetType(X))
in Visual Basic, of het gebruik van Monitor.Enter met Type objecten. Voor een bepaald type is er slechts één exemplaar van System.Type per toepassingsdomein. Als het type waarop u een vergrendeling aanneemt openbaar is, kan andere code dan uw eigen vergrendelingen aannemen, wat leidt tot impasses. Zie Aanbevolen procedures voor betrouwbaarheid voor aanvullende problemen.Wees voorzichtig bij het vergrendelen van exemplaren, bijvoorbeeld
lock(this)
in C# ofSyncLock(Me)
in Visual Basic. Als andere code in uw toepassing, buiten het type, een vergrendeling op het object neemt, kunnen impasses optreden.Zorg ervoor dat een thread die een monitor heeft ingevoerd, altijd die monitor verlaat, zelfs als er een uitzondering optreedt terwijl de thread zich in de monitor bevindt. De C# -vergrendelingsinstructie en de Visual Basic SyncLock-instructie bieden dit gedrag automatisch, waarbij een definitief blok wordt gebruikt om ervoor te zorgen dat deze Monitor.Exit wordt aangeroepen. Als u niet zeker weet dat Exit wordt aangeroepen, kunt u overwegen om uw ontwerp te wijzigen voor het gebruik van Mutex. Er wordt automatisch een mutex vrijgegeven wanneer de thread die momenteel eigenaar is van de thread, wordt beëindigd.
Gebruik meerdere threads voor taken waarvoor verschillende resources nodig zijn en vermijd het toewijzen van meerdere threads aan één resource. Elke taak met I/O profiteert bijvoorbeeld van een eigen thread, omdat deze thread wordt geblokkeerd tijdens I/O-bewerkingen en dus andere threads kan uitvoeren. Gebruikersinvoer is een andere resource die profiteert van een toegewezen thread. Op een computer met één processor bestaat een taak met intensieve berekeningen samen met gebruikersinvoer en met taken die betrekking hebben op I/O, maar meerdere rekenintensieve taken hebben te maken met elkaar.
Overweeg methoden van de Interlocked klasse te gebruiken voor eenvoudige statuswijzigingen in plaats van de
lock
instructie (SyncLock
in Visual Basic). Delock
instructie is een goed algemeen hulpprogramma, maar de Interlocked klasse biedt betere prestaties voor updates die atomisch moeten zijn. Intern wordt één vergrendelingsvoorvoegsel uitgevoerd als er geen conflicten zijn. Kijk in codebeoordelingen naar code zoals in de volgende voorbeelden. In het eerste voorbeeld wordt een statusvariabele verhoogd:SyncLock lockObject myField += 1 End SyncLock
lock(lockObject) { myField++; }
U kunt de prestaties als volgt verbeteren met behulp van de Increment methode in plaats van de
lock
instructie:System.Threading.Interlocked.Increment(myField)
System.Threading.Interlocked.Increment(myField);
Notitie
Gebruik de Add methode voor atomische stappen die groter zijn dan 1.
In het tweede voorbeeld wordt een variabele van het verwijzingstype alleen bijgewerkt als het een null-verwijzing (
Nothing
in Visual Basic) is.If x Is Nothing Then SyncLock lockObject If x Is Nothing Then x = y End If End SyncLock End If
if (x == null) { lock (lockObject) { x ??= y; } }
De prestaties kunnen als volgt worden verbeterd met behulp van de CompareExchange methode:
System.Threading.Interlocked.CompareExchange(x, y, Nothing)
System.Threading.Interlocked.CompareExchange(ref x, y, null);
Notitie
De overbelasting van de CompareExchange<T>(T, T, T) methode biedt een typeveilig alternatief voor referentietypen.
Aanbevelingen voor klassebibliotheken
Houd rekening met de volgende richtlijnen bij het ontwerpen van klassebibliotheken voor multithreading:
Vermijd indien mogelijk synchronisatie. Dit geldt met name voor intensief gebruikte code. Een algoritme kan bijvoorbeeld worden aangepast om een racevoorwaarde te tolereren in plaats van het te elimineren. Onnodige synchronisatie vermindert de prestaties en creëert de mogelijkheid van impasses en racevoorwaarden.
Stel statische gegevens (
Shared
in Visual Basic) standaard veilig.Maak exemplaargegevensthread niet standaard veilig. Het toevoegen van vergrendelingen voor het maken van thread-veilige code vermindert de prestaties, verhoogt conflicten over de vergrendeling en creëert de mogelijkheid dat impasses optreden. In algemene toepassingsmodellen voert slechts één thread tegelijk gebruikerscode uit, waardoor de noodzaak van threadveiligheid wordt geminimaliseerd. Daarom zijn de .NET-klassebibliotheken standaard niet threadveilig.
Vermijd het bieden van statische methoden die de statische status wijzigen. In veelvoorkomende serverscenario's wordt de statische status gedeeld tussen aanvragen, wat betekent dat meerdere threads die code tegelijkertijd kunnen uitvoeren. Hiermee opent u de mogelijkheid van threadingfouten. Overweeg een ontwerppatroon te gebruiken waarmee gegevens worden ingekapseld in exemplaren die niet worden gedeeld tussen aanvragen. Als statische gegevens worden gesynchroniseerd, kunnen aanroepen tussen statische methoden die de status wijzigen, leiden tot impasses of redundante synchronisatie, wat de prestaties nadelig beïnvloedt.