Lock
När flera trådar delar på data måste du skydda åtkomsten med ett lås. C# 13 introducerade System.Threading.Lock — en dedikerad lås-typ som ersätter det gamla mönstret med lock(object).
När du läst detta ska du kunna
- Förklara varför lås behövs vid flertrådad kod
- Använda
Lockmedlock-satsen - Använda
Lock.EnterScope()för mer kontroll - Förklara skillnaden mot det gamla
lock(object)-mönstret
Problemet — race condition
Utan lås kan två trådar skriva till samma variabel samtidigt och ge fel resultat:
int counter = 0;
// Två trådar ökar räknaren 1000 gånger varvar t1 = Task.Run(() => { for (int i = 0; i < 1000; i++) counter++; });var t2 = Task.Run(() => { for (int i = 0; i < 1000; i++) counter++; });
await Task.WhenAll(t1, t2);
Console.WriteLine(counter); // Borde vara 2000, men är ofta t.ex. 1847counter++ är inte atomär — läs, addera, skriv. Trådarna stör varandra.
Gamla sättet — lock(object)
private readonly object _lock = new object();
void IncrementSafely(){ lock (_lock) { counter++; }}Det fungerar, men object ger ingen typinformation — kompilatorn vet inte att det är ett lås.
Nya sättet — System.Threading.Lock
private readonly Lock _lock = new Lock();
void IncrementSafely(){ lock (_lock) // fungerar precis som förut — men typen är Lock, inte object { counter++; }}lock-satsen känner igen Lock-typen och använder en mer effektiv implementation under huven.
Praktiskt exempel
public class SafeCounter{ private readonly Lock _lock = new Lock(); private int _count;
public void Increment() { lock (_lock) _count++; }
public int Count => _count;}var counter = new SafeCounter();
var tasks = Enumerable.Range(0, 10) .Select(_ => Task.Run(() => { for (int i = 0; i < 1000; i++) counter.Increment(); }));
await Task.WhenAll(tasks);
Console.WriteLine(counter.Count); // alltid 10000EnterScope — explicit kontroll
Lock.EnterScope() ger ett IDisposable-scope som du använder med using. Det låter dig kontrollera exakt när låset tas och släpps:
private readonly Lock _lock = new Lock();
void UpdateData(List<int> data, int value){ using (_lock.EnterScope()) { data.Add(value); data.Sort(); } // Låset är släppt här — utanför using-blocket}EnterScope() är semantiskt identisk med lock (_lock) { } men ibland mer läsbar när låsblocket är långt.
Skillnad mot object
lock(object) | lock(Lock) | |
|---|---|---|
| Typinfo för kompilatorn | Nej | Ja |
| Prestanda | Bra | Bättre (optimerad implementering) |
EnterScope() | Nej | Ja |
| Bakåtkompatibel syntax | ✓ | ✓ |
Vad ett lås inte löser
- Deadlock: om tråd A håller lås 1 och väntar på lås 2, medan tråd B håller lås 2 och väntar på lås 1 — båda väntar för alltid
- Starvation: en tråd kan aldrig få låset om andra alltid hinner först
- Onödig serialisering: för stora lås-block tar bort fördelen med parallellism
Håll lås-block så korta som möjligt.
TL;DR
// Deklareraprivate readonly Lock _lock = new Lock();
// Använd — precis som lock(object) men med bättre typlock (_lock){ // skyddad kod här}
// Alternativt med usingusing (_lock.EnterScope()){ // skyddad kod här}Lock ersätter new object() som låstoken. Syntaxen är identisk — bytet är en enradsändring.