Asynkrone datastrukturer
Når du skriver async
-kode i C#, vil du ofte have brug for at dele data mellem flere metoder eller baggrundsopgaver. Men de fleste datastrukturer i .NET – som List<T>
og Dictionary<TKey,TValue>
– er ikke trådsikre. Hvis flere tasks forsøger at tilgå eller ændre dem samtidigt, kan det føre til fejl eller uforudsigelig adfærd.
List<string> beskeder = new List<string>();
async Task TilføjAsync(string tekst)
{
for (int i = 0; i < 100; i++)
{
beskeder.Add(tekst + i);
await Task.Delay(1);
}
}
var task1 = TilføjAsync("Hej");
var task2 = TilføjAsync("Verden");
await Task.WhenAll(task1, task2);
Console.WriteLine(beskeder.Count);
Hvis du starter flere instanser af TilføjAsync
samtidigt, risikerer du en InvalidOperationException
eller datakorruption. Det skyldes, at List<T>
ikke er trådsikker – og at await
giver mulighed for, at to tasks kører samtidigt.
Du kan beskytte samlingen med SemaphoreSlim
, men det kræver omhyggelig håndtering.
Brug af SemaphoreSlim
omkring samlinger
Hvis du vil bruge klassiske samlinger i async-kode, kan du sikre dem med en SemaphoreSlim
:
SemaphoreSlim sem = new SemaphoreSlim(1, 1);
List<string> beskeder = new List<string>();
async Task TilføjAsync(string tekst)
{
await sem.WaitAsync();
try
{
for (int i = 0; i < 100; i++)
{
beskeder.Add(tekst + i);
await Task.Delay(1);
}
}
finally
{
sem.Release();
}
}
var task1 = TilføjAsync("Hej");
var task2 = TilføjAsync("Verden");
await Task.WhenAll(task1, task2);
Console.WriteLine(beskeder.Count);
Dette virker fint, men kan hurtigt blive en flaskehals, hvis mange tasks kæmper om adgang til samme ressource.
Trådsikre samlinger i .NET
.NET tilbyder en række trådsikre samlinger i System.Collections.Concurrent
. De kan bruges uden lock
eller SemaphoreSlim
:
ConcurrentBag<T>
– uordnet samling (velegnet til “log alt”-scenarier)ConcurrentQueue<T>
– first-in-first-outConcurrentStack<T>
– last-in-first-outConcurrentDictionary<TKey,TValue>
– map med sikre opslag og opdateringer
Eksempel med ConcurrentBag<T>
:
using System.Collections.Concurrent;
// bemærk - rækkefølgen er ikke garanteret
ConcurrentBag<string> beskeder = new ConcurrentBag<string>();
async Task TilføjAsync(string tekst)
{
for (int i = 0; i < 100; i++)
{
beskeder.Add(tekst + i);
await Task.Delay(1);
}
}
var task1 = TilføjAsync("Hej");
var task2 = TilføjAsync("Verden");
await Task.WhenAll(task1, task2);
Console.WriteLine(beskeder.Count);
Hvorfor findes der ikke en ConcurrentList<T>
?
Du undrer dig måske over, at .NET ikke tilbyder en ConcurrentList<T>
– men det er der en god grund til. De fleste brugsscenarier for en List<T>
kræver en eller anden form for ordnet adgang (f.eks. via index), og det er svært at gøre effektivt og trådsikkert samtidigt. Derfor findes der ingen officiel ConcurrentList<T>
i .NET.
I stedet bør du:
- Bruge
ConcurrentBag<T>
hvis rækkefølge er ligegyldig. - Bruge
ConcurrentQueue<T>
hvis du har brug for først-ind-først-ud (FIFO). - Beskytte en almindelig
List<T>
medSemaphoreSlim
, hvis du har behov for ordnet adgang eller index-baseret læsning.
Immutable datastrukturer
En anden tilgang er at undgå delt tilstand helt. Med de immutable datastrukturer i System.Collections.Immutable
arbejder du med kopier i stedet for ændringer:
using System.Collections.Immutable;
ImmutableList<string> navne = ImmutableList<string>.Empty;
navne = navne.Add("Alice");
navne = navne.Add("Bob");
Denne tilgang kræver, at du altid tildeler resultatet af en ændring til en ny variabel – eller den samme. Det er trådsikkert, fordi ingen kan ændre det gamle objekt.
Hvad skal du vælge?
Situation | Anbefalet løsning |
---|---|
Du har eksisterende List<T> og vil beskytte den | SemaphoreSlim |
Du har høj samtidighed uden await | Concurrent* samlinger |
Du vil undgå delt tilstand helt | Immutable* samlinger |