Threads, Parallelism and Synchronization
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Visual Basic 17.13 on .NET 10 (SDK 10.0.402) and was checked on 2026-10-19. Every example marked
"built and run" is a complete console program that starts with Option Strict On, was built with warnings treated as errors
(except where the text says otherwise) and was run in the .NET 10 SDK container (Linux); the output shown is real output. The
root namespace of the scratch projects was App. Concurrency output can depend on timing, so every program here prints only
facts that do not (counts, sorted lists, booleans such as "lost updates"); where a number could vary it is not shown as a
claim. The Windows Forms example is labelled "built, not run": it was compiled against the net10.0-windows targeting pack and
nothing more is claimed.
A thread is an operating-system unit of execution; a program with several can do several things at once on a multi-core machine, or
wait for I/O without freezing the user interface. The price is that data shared between threads can be read and written in
overlapping ways that no single-threaded reading of the source predicts. This page covers the primitives (Thread, the thread
pool, SyncLock, Monitor, Interlocked), the library layers above them (concurrent collections, Parallel, PLINQ) and the
Visual Basic specifics. Everything below the language is shared .NET platform material, covered from the C# side in
Threads and Synchronization (C#); for waiting without blocking a thread,
use Async and Await, which should be your first choice for I/O-bound work.
Choosing a tool
| Problem | Use | Notes |
|---|---|---|
Wait for I/O (network, files, timers) without blocking |
|
no thread is held while waiting |
Run one CPU-bound job off the current thread |
|
pool thread; keep the UI thread free |
Run many independent CPU-bound iterations |
|
only when each iteration is substantial |
A long-lived dedicated worker |
|
rare; the pool is unsuited to threads that block for a long time |
Protect shared state |
|
prefer not sharing over locking |
Hand work from producers to consumers |
|
bounded capacity gives back-pressure |
Throttle concurrency |
|
|
Legacy UI background work |
|
replaced by |
Threads
A Thread is created with a delegate: ThreadStart (no parameters) or ParameterizedThreadStart (one Object). In Visual Basic you
write New Thread(AddressOf Method) or New Thread(Sub() ...), and the compiler picks the delegate from the shape of the method. A thread
does not run until Start(). Join() waits for it to finish. IsBackground decides whether the process may exit while the thread still runs.
Option Strict On
Imports System.Threading
Module Program
Private ReadOnly Started As New ManualResetEventSlim(False)
Private ReadOnly Release As New ManualResetEventSlim(False)
' ParameterizedThreadStart takes one Object; ThreadStart takes nothing.
Sub ImportBatch(state As Object)
Dim batch As String = CStr(state)
Console.WriteLine("worker " & Thread.CurrentThread.Name & " imports " & batch &
" (background: " & Thread.CurrentThread.IsBackground.ToString() & ")")
End Sub
Sub Wait()
Started.Set()
Release.Wait()
End Sub
Sub Main()
Dim t1 As New Thread(AddressOf ImportBatch) With {.Name = "importer-1"}
Console.WriteLine("before Start: " & t1.ThreadState.ToString())
t1.Start("batch A")
t1.Join()
Console.WriteLine("after Join: " & t1.ThreadState.ToString())
Dim result As Integer
Dim t2 As New Thread(Sub() result = Enumerable.Range(1, 100).Sum()) With {.Name = "summer"}
t2.Start()
t2.Join()
Console.WriteLine("t2 stored " & result.ToString() & " in a captured local")
Dim t3 As New Thread(AddressOf Wait) With {.IsBackground = True, .Name = "waiter"}
t3.Start()
Started.Wait()
Thread.Sleep(50)
Console.WriteLine("while blocked: " & t3.ThreadState.ToString())
Release.Set()
t3.Join()
Console.WriteLine("finished: " & t3.ThreadState.ToString())
Console.WriteLine("main thread id " & Environment.CurrentManagedThreadId.ToString() & ", t1 id differs: " & (t1.ManagedThreadId <> Environment.CurrentManagedThreadId).ToString())
End Sub
End Module
before Start: Unstarted
worker importer-1 imports batch A (background: False)
after Join: Stopped
t2 stored 5050 in a captured local
while blocked: Background, WaitSleepJoin
finished: Stopped
main thread id 1, t1 id differs: True
Source: Managed threading basics
-
ThreadStateis a set of flags:UnstartedbeforeStart,Stoppedafter the method returns, andWaitSleepJoinwhile the thread is blocked on a lock, aSleep, aJoinor a wait handle.Backgroundis one of the flags. It is a snapshot: read it for diagnostics, never to make decisions, because it can change before you act on it. -
A captured local (
result) is how a thread hands a value back.Join()makes the write visible to the caller. For anything more elaborate return the value from aTask(Of T)instead. -
AddressOf ImportBatchbinds toParameterizedThreadStartbecauseImportBatchtakes oneObject. A method with some other parameter type matches neither delegate, and the error is an overload-resolution failure on theThreadconstructor (see the error section). UnderOption Strict Ona lambda or anObjectparameter is the way out. -
A foreground thread (the default) keeps the process alive after
Mainreturns; a background thread does not. -
An unhandled exception in a thread ends the process (the
AppDomain.UnhandledExceptionevent fires first, but cannot stop it). Exceptions in `Task`s are different: they are stored in the task. -
Thread priority and affinity exist (
Priority,ProcessorAffinityon the process) but rarely help; the scheduler already does a better job.
Thread.Abort, Suspend and Resume are gone
Older material kills threads with Thread.Abort and pauses them with Suspend and Resume. On .NET 5 and later they are not supported: Abort compiles with a warning and
throws at run time. The build prints the warning (an error when warnings are errors), the program prints the exception:
Option Strict On
Imports System.Threading
Module Program
Sub Main()
Dim worker As New Thread(Sub() Thread.Sleep(Timeout.Infinite))
worker.IsBackground = True
worker.Start()
Try
worker.Abort()
Catch ex As PlatformNotSupportedException
Console.WriteLine(ex.GetType().Name & ": " & ex.Message)
End Try
End Sub
End Module
PlatformNotSupportedException: Thread abort is not supported on this platform.
Source: Destroying threads
The replacement is cooperative cancellation: pass a CancellationToken, check it in loops, and let the thread end by itself, or signal a
ManualResetEventSlim. Foreground threads need cooperative exit anyway.
The thread pool and Task.Run
Creating a thread costs roughly a megabyte of stack and an operating-system object, so short jobs run on the thread pool: a set of
reusable background threads that grows and shrinks on demand. ThreadPool.QueueUserWorkItem, Task.Run and the continuations of Await all
use it. Task.Run is the modern entry point: it returns a Task that carries the result and any exception.
Option Strict On
Imports System.Threading
Module Program
Function Describe() As String
Dim t As Thread = Thread.CurrentThread
Return If(t.IsThreadPoolThread, "pool", "dedicated") & ", background = " & t.IsBackground.ToString()
End Function
Sub Main()
Dim done As New CountdownEvent(3)
Dim lines As New Collections.Concurrent.ConcurrentBag(Of String)
ThreadPool.QueueUserWorkItem(Sub(state)
lines.Add("QueueUserWorkItem: " & Describe())
done.Signal()
End Sub)
Task.Run(Sub()
lines.Add("Task.Run: " & Describe())
done.Signal()
End Sub)
Task.Factory.StartNew(Sub()
lines.Add("LongRunning: " & Describe())
done.Signal()
End Sub, TaskCreationOptions.LongRunning)
done.Wait()
For Each line As String In lines.OrderBy(Function(l) l)
Console.WriteLine(line)
Next
Console.WriteLine("main thread: " & Describe())
End Sub
End Module
LongRunning: dedicated, background = True
QueueUserWorkItem: pool, background = True
Task.Run: pool, background = True
main thread: dedicated, background = False
Source: The managed thread pool
-
Pool threads are background threads, so a program that only has pool work running exits when
Mainreturns. -
TaskCreationOptions.LongRunningasks for a dedicated thread, which is right for a job that blocks for minutes (a consumer loop, a listener). Do not park pool threads on blocking calls: the pool injects new threads slowly, and a pool full of blocked threads (thread-pool starvation) makes everyAwaitcontinuation wait. -
Thread.Sleepblocks a thread; in async code useAwait Task.Delay. -
The pool is shared by the whole process. Libraries should not call
ThreadPool.SetMinThreadsas a fix for their own blocking.
Shared state and data races
A data race occurs when two threads access the same memory and at least one writes, without synchronisation. _counter += 1 looks like one step but is
three: read, add, write. Two threads can both read 5 and both write 6.
Option Strict On
Imports System.Threading
Module Program
Private Const Workers As Integer = 4
Private Const PerWorker As Integer = 500000
Private _unsafe As Integer
Private _locked As Integer
Private _atomic As Integer
Private ReadOnly Gate As New Object()
Sub RunWorkers(body As Action)
Dim threads As New List(Of Thread)
For i As Integer = 1 To Workers
Dim t As New Thread(Sub() body())
threads.Add(t)
t.Start()
Next
For Each t As Thread In threads
t.Join()
Next
End Sub
Sub Main()
Dim expected As Integer = Workers * PerWorker
RunWorkers(Sub()
For i As Integer = 1 To PerWorker
_unsafe += 1 ' read, add, write: three steps
Next
End Sub)
RunWorkers(Sub()
For i As Integer = 1 To PerWorker
SyncLock Gate
_locked += 1
End SyncLock
Next
End Sub)
RunWorkers(Sub()
For i As Integer = 1 To PerWorker
Interlocked.Increment(_atomic)
Next
End Sub)
Console.WriteLine("expected: " & expected.ToString())
Console.WriteLine("unsynchronised: lost updates = " & (_unsafe < expected).ToString())
Console.WriteLine("SyncLock: " & _locked.ToString())
Console.WriteLine("Interlocked: " & _atomic.ToString())
End Sub
End Module
expected: 2000000
unsynchronised: lost updates = True
SyncLock: 2000000
Interlocked: 2000000
Source: Managed threading best practices
The unsynchronised total is almost always short of 2,000,000; the program reports only the fact that updates were lost, because the
exact figure differs on every run. SyncLock and Interlocked both give the exact answer; Interlocked is cheaper for a single
variable, SyncLock is needed when several variables must change together. The deeper fix, in most programs, is to share less: let each worker
accumulate into its own variable and combine the results at the end (see Parallel.For with localInit below).
SyncLock and what it locks on
SyncLock expression … End SyncLock acquires the monitor of an object, runs the block, and releases the monitor in a Finally, even when the block throws. It is
Monitor.Enter and Monitor.Exit with the Try/Finally written for you.
Option Strict On
Imports System.Threading
Module Program
Private ReadOnly Gate As New Object()
Sub Main()
' SyncLock is Monitor.Enter / Monitor.Exit inside Try/Finally.
SyncLock Gate
Console.WriteLine("inside SyncLock: Monitor.IsEntered = " & Monitor.IsEntered(Gate).ToString())
SyncLock Gate ' the same thread may enter again
Console.WriteLine("re-entered, still held")
End SyncLock
End SyncLock
Console.WriteLine("outside: Monitor.IsEntered = " & Monitor.IsEntered(Gate).ToString())
' The lock is the object's identity, not the variable: two literals are the same object.
Dim first As String = "shelf"
Dim second As String = "sh" & "elf"
SyncLock first
Console.WriteLine("literal 'shelf' locked, a second literal is held too: " & Monitor.IsEntered(second).ToString())
End SyncLock
' A lock released by an exception.
Try
SyncLock Gate
Throw New InvalidOperationException("boom")
End SyncLock
Catch ex As InvalidOperationException
Console.WriteLine("after the exception, still held: " & Monitor.IsEntered(Gate).ToString())
End Try
' Nothing as the operand.
Dim nothingLock As Object = Nothing
Try
SyncLock nothingLock
End SyncLock
Catch ex As ArgumentNullException
Console.WriteLine("SyncLock Nothing: " & ex.GetType().Name)
End Try
' Monitor.TryEnter with a time-out, called while another thread holds the lock.
Dim holding As New ManualResetEventSlim(False)
Dim leave As New ManualResetEventSlim(False)
Dim holder As New Thread(Sub()
SyncLock Gate
holding.Set()
leave.Wait()
End SyncLock
End Sub)
holder.Start()
holding.Wait()
Console.WriteLine("TryEnter while another thread holds it: " & Monitor.TryEnter(Gate, 100).ToString())
leave.Set()
holder.Join()
Console.WriteLine("TryEnter once it is free: " & Monitor.TryEnter(Gate, 100).ToString())
Monitor.Exit(Gate)
End Sub
End Module
inside SyncLock: Monitor.IsEntered = True
re-entered, still held
outside: Monitor.IsEntered = False
literal 'shelf' locked, a second literal is held too: True
after the exception, still held: False
SyncLock Nothing: ArgumentNullException
TryEnter while another thread holds it: False
TryEnter once it is free: True
Source: SyncLock Statement
What each part of the output shows:
-
The monitor is re-entrant. The same thread may enter again; it must exit as many times as it entered. A different thread waits.
-
The lock is the object, not the variable.
firstandsecondare different variables holding the same internedString, so locking one holds the other. This is why you must never lock on a string, aTypeobject (GetType(Foo)), orMeof a public class: unrelated code can lock the same object and create contention or a deadlock you cannot see in your own source. Lock on aPrivate ReadOnlyobject created for the purpose, asGateis. -
An exception releases the lock. After the exception the monitor was free again.
-
SyncLock NothingthrowsArgumentNullExceptionat run time. -
A value type cannot be locked (BC30582, below): boxing would create a new object each time and nothing would be protected.
-
Monitor.TryEnter(object, milliseconds)returnsFalseinstead of waiting forever. Every successfulTryEntermust be paired withMonitor.Exit. -
Awaitis not allowed insideSyncLock(BC36943); the monitor belongs to a thread and anAwaitmay resume on another. UseSemaphoreSlim.WaitAsync.
For waiting on a condition (the classic bounded buffer), Monitor.Wait(obj) releases the lock and sleeps until another thread calls Monitor.Pulse(obj) or PulseAll. Prefer
BlockingCollection(Of T) or Channel(Of T), which package the pattern correctly.
The Lock type
System.Threading.Lock, a dedicated lock type with a faster implementation; C# 13 makes lock recognise it. Visual Basic17.13 does not use it from SyncLock, and says so:
Option Strict On
Imports System.Threading
Module Program
Sub Main()
Dim gate As New Lock()
SyncLock gate
Console.WriteLine("held")
End SyncLock
End Sub
End Module
Program.vb(8,18): error BC37329: A value of type 'System.Threading.Lock' is not supported in SyncLock. Consider manually calling 'Enter' and 'Exit' methods in a Try/Finally block instead.
The compiler refuses SyncLock on a Lock (BC37329); a Lock converted to Object would silently fall back to a slower monitor lock, and that is the warning BC42508. The supported way to
use a Lock is the explicit Enter/Exit pair in Try/Finally. The scoped form that C# uses, Using gate.EnterScope(), is also unavailable,
because Lock.Scope is a ref struct and Visual Basic marks types with embedded references as unusable:
Option Strict On
Imports System.Threading
Module Program
Sub Main()
Dim gate As New Lock()
Using scope As Lock.Scope = gate.EnterScope()
Console.WriteLine("held")
End Using
End Sub
End Module
Program.vb(8,24): error BC30668: 'Lock.Scope' is obsolete: 'Types with embedded references are not supported in this version of your compiler.'.
Program.vb(8,15): error BC36010: 'Using' operand of type 'Lock.Scope' must implement 'System.IDisposable'.
Program.vb(8,15): warning BC42351: Local variable 'scope' is read-only and its type is a structure. Invoking its members or passing it ByRef does not change its content and might lead to unexpected results. Consider declaring this variable outside of the 'Using' block.
Option Strict On
Imports System.Threading
Module Program
Sub Main()
Dim gate As New Lock()
gate.Enter()
Try
Console.WriteLine("Lock.Enter: IsHeldByCurrentThread = " & gate.IsHeldByCurrentThread.ToString())
Finally
gate.Exit()
End Try
If gate.TryEnter(TimeSpan.FromMilliseconds(10)) Then
Try
Console.WriteLine("Lock.TryEnter succeeded, held = " & gate.IsHeldByCurrentThread.ToString())
Finally
gate.Exit()
End Try
End If
Console.WriteLine("after Exit: IsHeldByCurrentThread = " & gate.IsHeldByCurrentThread.ToString())
End Sub
End Module
Lock.Enter: IsHeldByCurrentThread = True
Lock.TryEnter succeeded, held = True
after Exit: IsHeldByCurrentThread = False
Source: Lock
Use Lock only if you have measured a benefit. For most Visual Basic code a private Object and SyncLock is correct and clear.
Interlocked and Volatile
Interlocked provides atomic read-modify-write operations on a single variable: Increment, Decrement, Add, Exchange, CompareExchange. They take the variable ByRef, so
they work on fields and on locals captured by lambdas, but not on properties. CompareExchange(location, newValue, comparand) stores newValue only if location still equals
comparand, and always returns the previous value; looping on it gives lock-free "update if still current" logic.
Volatile.Read and Volatile.Write are the Visual Basic substitute for C#'s volatile keyword. Visual Basic has no Volatile modifier:
declaring Private Volatile Stopped As Boolean is a syntax error (below). Without a barrier the compiler or the processor may keep a flag in a register, so a worker thread could
loop forever on a stale False. Volatile.Write publishes the new value and Volatile.Read observes it.
Option Strict On
Imports System.Threading
Module Program
Private _stop As Boolean
Private _count As Integer
Private _best As Integer
Private _total As Long
Sub UpdateBest(candidate As Integer)
Dim seen As Integer
Do
seen = Volatile.Read(_best)
If candidate <= seen Then Return
Loop While Interlocked.CompareExchange(_best, candidate, seen) <> seen
End Sub
Sub Main()
' Volatile.Read/Write: there is no Volatile keyword in Visual Basic.
Dim spinner As New Thread(Sub()
Dim spins As Long = 0
Do Until Volatile.Read(_stop)
spins += 1
Loop
Console.WriteLine("spinner saw the flag and stopped")
End Sub)
spinner.Start()
Thread.Sleep(50)
Volatile.Write(_stop, True)
spinner.Join()
Dim threads As New List(Of Thread)
For worker As Integer = 1 To 4
Dim id As Integer = worker
Dim t As New Thread(Sub()
For i As Integer = 1 To 1000
Interlocked.Increment(_count)
Interlocked.Add(_total, i)
UpdateBest(id * 1000 + i)
Next
End Sub)
threads.Add(t)
t.Start()
Next
For Each t As Thread In threads
t.Join()
Next
Console.WriteLine("count = " & _count.ToString() & ", total = " & _total.ToString() & ", best = " & _best.ToString())
Dim old As Integer = Interlocked.Exchange(_count, 0)
Console.WriteLine("Exchange returned " & old.ToString() & ", now " & _count.ToString())
Console.WriteLine("CompareExchange swaps only on a match: " & Interlocked.CompareExchange(_count, 5, 1).ToString() & " then " & Interlocked.CompareExchange(_count, 5, 0).ToString() & ", now " & _count.ToString())
End Sub
End Module
spinner saw the flag and stopped
count = 4000, total = 2002000, best = 5000
Exchange returned 4000, now 0
CompareExchange swaps only on a match: 0 then 0, now 5
Source: Volatile
The last line shows the contract of CompareExchange: the first call compares against 1 but the value is 0, so nothing is stored and the old value (0) is returned; the second compares against 0, so it
stores 5 and again returns the old value, 0. Check the return value, not the variable, to learn whether the swap happened.
Concurrent collections and producer/consumer pipelines
The ordinary collections (List(Of T), Dictionary(Of K, V)) are not safe for concurrent writes; a corrupted Dictionary can loop forever. The
System.Collections.Concurrent namespace provides thread-safe versions: ConcurrentDictionary, ConcurrentQueue, ConcurrentStack, ConcurrentBag and BlockingCollection.
Option Strict On
Imports System.Collections.Concurrent
Imports System.Threading
Module Program
Sub Main()
Dim loansPerMember As New ConcurrentDictionary(Of String, Integer)
Dim members As String() = {"ana", "ben", "ana", "carla", "ben", "ana", "carla", "ana"}
Parallel.ForEach(members, Sub(member)
loansPerMember.AddOrUpdate(member, 1, Function(key, current) current + 1)
End Sub)
For Each pair As KeyValuePair(Of String, Integer) In loansPerMember.OrderBy(Function(p) p.Key)
Console.WriteLine(pair.Key & " = " & pair.Value.ToString())
Next
Dim titles As String = loansPerMember.GetOrAdd("dmitri", Function(key) key.Length).ToString()
Console.WriteLine("GetOrAdd created dmitri = " & titles)
Dim added As Boolean = loansPerMember.TryAdd("ana", 100)
Console.WriteLine("TryAdd on an existing key: " & added.ToString())
' A bounded producer/consumer pipeline.
Dim queue As New BlockingCollection(Of String)(boundedCapacity:=2)
Dim consumed As New List(Of String)
Dim consumer As Task = Task.Run(Sub()
For Each isbn As String In queue.GetConsumingEnumerable()
consumed.Add(isbn)
Next
End Sub)
For i As Integer = 1 To 6
queue.Add("978000000" & i.ToString("D4")) ' blocks when 2 items are waiting
Next
queue.CompleteAdding()
consumer.Wait()
Console.WriteLine("consumed in order: " & String.Join(" ", consumed))
Dim q As New ConcurrentQueue(Of Integer)
Parallel.For(0, 100, Sub(i) q.Enqueue(i))
Dim stack As New ConcurrentStack(Of Integer)
stack.PushRange({1, 2, 3})
Dim top As Integer
Console.WriteLine("queue count " & q.Count.ToString() & ", stack pop " & If(stack.TryPop(top), top.ToString(), "none"))
' Unlike Dictionary(Of K, V), changing a concurrent dictionary while enumerating it is allowed.
For Each pair As KeyValuePair(Of String, Integer) In loansPerMember
If Not pair.Key.EndsWith("-copy", StringComparison.Ordinal) Then
loansPerMember.TryAdd(pair.Key & "-copy", pair.Value)
End If
Next
Console.WriteLine("modified while enumerating, no exception; keys now " & loansPerMember.Count.ToString())
End Sub
End Module
ana = 4
ben = 2
carla = 2
GetOrAdd created dmitri = 6
TryAdd on an existing key: False
consumed in order: 9780000000001 9780000000002 9780000000003 9780000000004 9780000000005 9780000000006
queue count 100, stack pop 3
modified while enumerating, no exception; keys now 8
Source: Thread-safe collections
-
AddOrUpdateandGetOrAddare atomic with respect to the dictionary, but the delegate you pass may run more than once (and its result be discarded) if threads race; keep it free of side effects, or storeLazy(Of T)values when creation is expensive. -
BlockingCollection(boundedCapacity:=2)makesAddwait when two items are queued, so a fast producer cannot overrun a slow consumer.CompleteAdding()ends the consumer’sGetConsumingEnumerable()loop. -
Enumerating a concurrent collection while it changes does not throw (unlike
Dictionary), but the sequence you see is not a stable snapshot. TheCountof aConcurrentQueueorConcurrentBagis exact only when no one is writing. -
Channel(Of T)(namespaceSystem.Threading.Channels) is the asynchronous producer/consumer queue; its use from Visual Basic, including reading withWaitToReadAsyncor by drivingReadAllAsync()by hand, is on the async page.
See Collections for the single-threaded collections.
Parallel loops
Parallel.For, Parallel.ForEach and Parallel.Invoke split work across pool threads and return when all of it is done. They suit independent, CPU-bound iterations
that each do enough work to repay the scheduling.
Option Strict On
Imports System.Collections.Concurrent
Imports System.Threading
Module Program
Sub Main()
Dim squares As New ConcurrentBag(Of Integer)
Parallel.For(1, 6, Sub(i) squares.Add(i * i))
Console.WriteLine("Parallel.For squares: " & String.Join(" ", squares.OrderBy(Function(x) x)))
Dim options As New ParallelOptions With {.MaxDegreeOfParallelism = 2}
Dim peak As Integer = 0
Dim active As Integer = 0
Parallel.ForEach(Enumerable.Range(1, 8), options, Sub(i)
Dim now As Integer = Interlocked.Increment(active)
Dim seen As Integer
Do
seen = Volatile.Read(peak)
Loop While now > seen AndAlso Interlocked.CompareExchange(peak, now, seen) <> seen
Thread.Sleep(20)
Interlocked.Decrement(active)
End Sub)
Console.WriteLine("MaxDegreeOfParallelism = 2 respected: " & (peak <= 2).ToString())
' Thread-local partial sums: one Interlocked.Add per worker instead of one per element.
Dim total As Long = 0
Parallel.For(Of Long)(1, 100001, Function() 0L,
Function(i, state, local) local + i,
Sub(local) Interlocked.Add(total, local))
Console.WriteLine("sum of 1..100000 with localInit/localFinally: " & total.ToString())
' Break stops scheduling new iterations beyond the index; Stop ends as soon as possible.
Dim result As ParallelLoopResult = Parallel.For(0, 1000, Sub(i, state)
If i = 10 Then state.Break()
End Sub)
Console.WriteLine("IsCompleted = " & result.IsCompleted.ToString() & ", LowestBreakIteration = " & result.LowestBreakIteration.ToString())
' Exceptions are collected into an AggregateException.
Try
Parallel.For(0, 4, Sub(i)
If i Mod 2 = 1 Then Throw New InvalidOperationException("item " & i.ToString())
End Sub)
Catch ex As AggregateException
Dim messages As IEnumerable(Of String) = ex.InnerExceptions.Select(Function(e) e.Message).OrderBy(Function(m) m)
Console.WriteLine("AggregateException with " & ex.InnerExceptions.Count.ToString() & " inner exceptions: " & String.Join(", ", messages))
End Try
Dim order As New ConcurrentBag(Of String)
Parallel.Invoke(Sub() order.Add("loans"), Sub() order.Add("members"), Sub() order.Add("books"))
Console.WriteLine("Parallel.Invoke ran " & order.Count.ToString() & " actions")
End Sub
End Module
Parallel.For squares: 1 4 9 16 25
MaxDegreeOfParallelism = 2 respected: True
sum of 1..100000 with localInit/localFinally: 5000050000
IsCompleted = False, LowestBreakIteration = 10
AggregateException with 2 inner exceptions: item 1, item 3
Parallel.Invoke ran 3 actions
-
The loop body is a lambda:
Sub(i) ...forAction(Of Integer),Sub(i, state) ...when you needParallelLoopState. A lambda cannot leave a loop withExit For(BC30096, below); usestate.Break()orstate.Stop(). -
ParallelOptions.MaxDegreeOfParallelismcaps the concurrency (here 2), andCancellationTokenmakes the loop cancellable. -
Parallel.For(Of Long)(from, to, localInit, body, localFinally)gives each worker its own accumulator:localis private to the worker, so there is no contention until the singleInterlocked.Addat the end. The example states the type argument (Of Long) explicitly. -
Break()lets iterations below the break index finish and stops new ones above it;Stop()asks everything to stop at once.LowestBreakIterationreports the index. -
Exceptions from iterations are collected in an
AggregateException, and after the first one the loop stops scheduling new iterations (the exact set of messages can vary; here both odd items ran). -
The iterations run in no particular order. Writing
results(i)into a pre-sized array is safe because each index is touched once; appending to aListis not. -
Parallel.ForEachAsync(.NET 6) takes an asynchronous body and a degree of parallelism, for I/O-bound loops.
PLINQ
PLINQ is LINQ to Objects run in parallel. Add AsParallel() to the source; the Visual Basic query syntax (From ... Where ... Select) works on a ParallelQuery, as do Aggregate ... Into Sum()
and the method forms. Results come back in any order unless you add AsOrdered().
Option Strict On
Imports System.Collections.Concurrent
Imports System.Threading
Module Program
Sub Main()
Dim numbers As IEnumerable(Of Integer) = Enumerable.Range(1, 20)
Dim ordered As List(Of Integer) = (From n In numbers.AsParallel().AsOrdered()
Where n Mod 2 = 0
Select n * n).ToList()
Console.WriteLine("AsOrdered query: " & String.Join(" ", ordered))
Dim unordered As List(Of Integer) = numbers.AsParallel().Where(Function(n) n Mod 2 = 0).Select(Function(n) n * n).ToList()
Console.WriteLine("unordered query has the same elements: " & unordered.OrderBy(Function(x) x).SequenceEqual(ordered).ToString())
Dim sumOfAll As Integer = numbers.AsParallel().WithDegreeOfParallelism(2).Sum()
Console.WriteLine("Sum = " & sumOfAll.ToString())
Dim aggregated As Integer = Aggregate n In numbers.AsParallel() Into Sum(n)
Console.WriteLine("Aggregate ... Into Sum = " & aggregated.ToString())
Dim bag As New ConcurrentBag(Of Integer)
numbers.AsParallel().Where(Function(n) n > 15).ForAll(Sub(n) bag.Add(n))
Console.WriteLine("ForAll collected " & bag.Count.ToString() & " items")
Try
numbers.AsParallel().Select(Function(n) 10 \ (n - 5)).ToList()
Catch ex As AggregateException
Console.WriteLine("PLINQ exception: " & ex.InnerException.GetType().Name)
End Try
Using cts As New CancellationTokenSource()
cts.Cancel()
Try
numbers.AsParallel().WithCancellation(cts.Token).Select(Function(n) n).ToList()
Catch ex As OperationCanceledException
Console.WriteLine("WithCancellation: " & ex.GetType().Name)
End Try
End Using
End Sub
End Module
AsOrdered query: 4 16 36 64 100 144 196 256 324 400
unordered query has the same elements: True
Sum = 210
Aggregate ... Into Sum = 210
ForAll collected 5 items
PLINQ exception: DivideByZeroException
WithCancellation: OperationCanceledException
Source: Introduction to PLINQ
Notes: AsOrdered restores source order at some cost. ForAll runs a side effect on each result in parallel without merging them first (the target, a ConcurrentBag, must be thread-safe). Exceptions arrive as
an AggregateException. PLINQ and Parallel are slower than the sequential version for small or cheap work: measure before using them. See
LINQ Query Expressions for the query syntax.
Deadlocks
A deadlock is a cycle of waiting: thread 1 holds lock A and wants B while thread 2 holds B and wants A. Neither can proceed and the program hangs without an error. The programs below
would hang with plain SyncLock; Monitor.TryEnter with a timeout lets the example report it instead.
Option Strict On
Imports System.Threading
Module Program
Private ReadOnly Catalog As New Object()
Private ReadOnly Loans As New Object()
' Two threads take the same two locks in opposite order. A Barrier makes sure each holds its first lock
' before asking for the second, and Monitor.TryEnter turns the permanent hang into a report.
Function Try2(first As Object, second As Object, barrier As Barrier) As String
SyncLock first
barrier.SignalAndWait()
If Monitor.TryEnter(second, 200) Then
Monitor.Exit(second)
Return "ok"
End If
Return "gave up waiting"
End SyncLock
End Function
Sub Main()
Dim barrier As New Barrier(2)
Dim a As String = ""
Dim b As String = ""
Dim t1 As New Thread(Sub() a = Try2(Catalog, Loans, barrier))
Dim t2 As New Thread(Sub() b = Try2(Loans, Catalog, barrier))
t1.Start() : t2.Start()
t1.Join() : t2.Join()
Console.WriteLine("opposite order: thread 1 " & a & ", thread 2 " & b)
' The fix: every thread takes the locks in the same global order.
Dim barrier2 As New Barrier(1)
Dim c As String = ""
Dim d As String = ""
Dim t3 As New Thread(Sub() c = Try2(Catalog, Loans, barrier2))
t3.Start() : t3.Join()
Dim t4 As New Thread(Sub() d = Try2(Catalog, Loans, barrier2))
t4.Start() : t4.Join()
Console.WriteLine("same order: thread 3 " & c & ", thread 4 " & d)
End Sub
End Module
opposite order: thread 1 ok, thread 2 gave up waiting
same order: thread 3 ok, thread 4 ok
Source: Managed threading best practices
How to avoid them:
-
Always take multiple locks in the same global order (document it).
-
Hold locks for as short a time as possible, and never call unknown code (an event handler, a virtual method, a callback) while holding one.
-
Prefer one lock per invariant, or no lock: immutable data,
Interlocked, a single-consumer queue. -
Use
Monitor.TryEnterwith a timeout where you can recover; log and give up instead of hanging. -
For the async version of the same bug (blocking on a task from a thread with a synchronization context) see Async and Await.
-
A thread dump from the debugger (Parallel Stacks and Threads windows) or
dotnet-stackshows which thread waits for which lock.
Immutability and lazy initialisation
Data that never changes needs no lock. System.Collections.Immutable collections create a new collection on each change and leave the old one intact, so a reader holding a reference
always sees a complete, consistent snapshot. ImmutableInterlocked.Update replaces the shared reference atomically, retrying the transformation if another thread changed it first.
Option Strict On
Imports System.Collections.Immutable
Imports System.Threading
Module Program
Private _loans As ImmutableList(Of String) = ImmutableList(Of String).Empty
Private _published As String() = {"v1"}
Sub Main()
' Readers always see a complete snapshot; writers replace it atomically.
Dim writers As New List(Of Thread)
For w As Integer = 1 To 4
Dim id As Integer = w
Dim t As New Thread(Sub()
For i As Integer = 1 To 250
Dim entry As String = "w" & id.ToString() & "-" & i.ToString()
ImmutableInterlocked.Update(_loans, Function(current) current.Add(entry))
Next
End Sub)
writers.Add(t)
t.Start()
Next
For Each t As Thread In writers
t.Join()
Next
Console.WriteLine("ImmutableInterlocked.Update: " & _loans.Count.ToString() & " entries")
Dim before As ImmutableList(Of String) = _loans
Dim after As ImmutableList(Of String) = before.Add("extra")
Console.WriteLine("the old snapshot is unchanged: " & before.Count.ToString() & " versus " & after.Count.ToString())
' Publishing a new reference safely: build it fully, then Volatile.Write.
Dim fresh As String() = {"v1", "v2"}
Volatile.Write(_published, fresh)
Console.WriteLine("published: " & String.Join(",", Volatile.Read(_published)))
' Lazy initialisation that is safe on first use.
Dim index As New Lazy(Of Dictionary(Of String, Integer))(Function()
Console.WriteLine("building the index once")
Return New Dictionary(Of String, Integer) From {{"a", 1}}
End Function, LazyThreadSafetyMode.ExecutionAndPublication)
Dim readers As Task() = Enumerable.Range(0, 4).Select(Function(i) Task.Run(Function() index.Value.Count)).ToArray()
Task.WaitAll(readers)
Console.WriteLine("index count = " & index.Value.Count.ToString())
End Sub
End Module
ImmutableInterlocked.Update: 1000 entries
the old snapshot is unchanged: 1000 versus 1001
published: v1,v2
building the index once
index count = 1
Source: ImmutableInterlocked
-
The transformation function passed to
Updatemay run more than once. It must be a pure function of its argument, and must not capture the loop variable of aFor(BC42324); the example copiesentryinto a local first. -
Publish a fully constructed object by writing the reference with
Volatile.Write; readers useVolatile.Read. -
Lazy(Of T)withLazyThreadSafetyMode.ExecutionAndPublication(the default) runs the factory exactly once even if four threads ask at the same time, as the single "building the index once" line shows.
Thread-local and async-local state
-
<ThreadStatic>on aSharedfield gives each thread its own copy. The initialiser (= 10) runs only once, for the thread that triggers the type’s static initialisation; other threads see the default (0). Initialise lazily in the accessor, or useThreadLocal(Of T). -
ThreadLocal(Of T)takes a factory that runs once per thread, and (withtrackAllValues:=True) lets you read every thread’s value viaValues. -
AsyncLocal(Of T)flows with the logical execution context: it survivesAwait, and is copied toTask.Runand to new threads. Changes made in a child are not seen by the parent.
Option Strict On
Imports System.Threading
Module Program
<ThreadStatic>
Private _perThread As Integer = 10 ' the initialiser runs only for the thread that first loads the type
Private ReadOnly Local As New ThreadLocal(Of Integer)(Function() 10, trackAllValues:=True)
Private ReadOnly Ambient As New AsyncLocal(Of String)
Sub Bump()
_perThread += 1
Local.Value += 1
End Sub
Async Function ChildAsync() As Task
Console.WriteLine("child sees Ambient = " & Ambient.Value)
Ambient.Value = "changed in the child"
Await Task.Delay(1)
End Function
Sub Main()
Console.WriteLine("main thread first sees ThreadStatic = " & _perThread.ToString())
Dim t As New Thread(AddressOf Bump)
t.Start()
t.Join()
Bump()
Console.WriteLine("main thread: ThreadStatic = " & _perThread.ToString() & ", ThreadLocal = " & Local.Value.ToString())
Console.WriteLine("ThreadLocal values across threads: " & String.Join(",", Local.Values.OrderBy(Function(v) v)))
Dim worker As New Thread(Sub() Console.WriteLine("worker thread: ThreadStatic starts at " & _perThread.ToString() & " (initialiser did not run)"))
worker.Start()
worker.Join()
Ambient.Value = "request 42"
ChildAsync().GetAwaiter().GetResult()
Console.WriteLine("parent still sees Ambient = " & Ambient.Value)
Dim viaThread As String = ""
Dim t2 As New Thread(Sub() viaThread = Ambient.Value)
t2.Start()
t2.Join()
Console.WriteLine("a new Thread also receives the ExecutionContext: " & viaThread)
End Sub
End Module
main thread first sees ThreadStatic = 10
main thread: ThreadStatic = 11, ThreadLocal = 11
ThreadLocal values across threads: 11,11
worker thread: ThreadStatic starts at 0 (initialiser did not run)
child sees Ambient = request 42
parent still sees Ambient = request 42
a new Thread also receives the ExecutionContext: request 42
Source: Thread-local storage
Notice that the worker thread saw 0 for the ThreadStatic field: its = 10 initialiser had already run, on the main thread, before the worker started. The main thread’s own value
went from 10 to 11 in Bump. ThreadLocal ran its factory once per thread (both threads show 11). AsyncLocal carried "request 42" into the async child and into a new Thread, and the child’s
assignment did not leak back.
Other synchronization primitives
SemaphoreSlim limits concurrency, ReaderWriterLockSlim allows many readers or one writer, Barrier lets a fixed group of workers advance phase by phase, CountdownEvent waits for N signals,
ManualResetEventSlim and AutoResetEvent signal between threads, Mutex locks across processes. All are ordinary classes used the same way from Visual Basic, with Try/Finally around
every acquire/release pair.
Option Strict On
Imports System.Threading
Module Program
Sub Main()
' ReaderWriterLockSlim: many readers or one writer.
Dim rw As New ReaderWriterLockSlim()
Dim index As New Dictionary(Of String, Integer)
Dim done As New CountdownEvent(5)
For w As Integer = 1 To 2
Dim id As Integer = w
ThreadPool.QueueUserWorkItem(Sub(state)
rw.EnterWriteLock()
Try
index("writer" & id.ToString()) = id
Finally
rw.ExitWriteLock()
End Try
done.Signal()
End Sub)
Next
Dim seen As Integer = 0
For r As Integer = 1 To 3
ThreadPool.QueueUserWorkItem(Sub(state)
rw.EnterReadLock()
Try
Interlocked.Add(seen, index.Count)
Finally
rw.ExitReadLock()
End Try
done.Signal()
End Sub)
Next
done.Wait()
Console.WriteLine("ReaderWriterLockSlim: " & index.Count.ToString() & " entries written, readers finished: " & done.IsSet.ToString())
' Barrier: three workers finish phase 1 before any starts phase 2.
Dim log As New Collections.Concurrent.ConcurrentQueue(Of String)
Using barrier As New Barrier(3, Sub(b) log.Enqueue("--- phase " & b.CurrentPhaseNumber.ToString() & " complete ---"))
Dim workers As Task() = Enumerable.Range(1, 3).Select(Function(n) Task.Run(Sub()
log.Enqueue("phase 0 by worker")
barrier.SignalAndWait()
log.Enqueue("phase 1 by worker")
barrier.SignalAndWait()
End Sub)).ToArray()
Task.WaitAll(workers)
End Using
Console.WriteLine(String.Join(" | ", log.ToArray().Select(Function(l) l.Replace(" by worker", "")).GroupBy(Function(l) l).Select(Function(g) g.Key & " x" & g.Count().ToString())))
' SemaphoreSlim as a counting gate.
Using gate As New SemaphoreSlim(2, 2)
gate.Wait()
gate.Wait()
Console.WriteLine("third Wait(0) while both slots are taken: " & gate.Wait(0).ToString())
gate.Release(2)
Console.WriteLine("CurrentCount after releasing: " & gate.CurrentCount.ToString())
End Using
End Sub
End Module
ReaderWriterLockSlim: 2 entries written, readers finished: True
phase 0 x3 | --- phase 0 complete --- x1 | phase 1 x3 | --- phase 1 complete --- x1
third Wait(0) while both slots are taken: False
CurrentCount after releasing: 2
Pick the simplest primitive that fits: SyncLock or Interlocked for mutual exclusion, SemaphoreSlim for counting, a BlockingCollection for hand-off. ReaderWriterLockSlim pays off only
for read-heavy data with long read sections; a plain SyncLock is often faster.
Timers
System.Threading.Timer calls a callback on a pool thread at an interval (callbacks can overlap if one runs long; keep them short and thread-safe). PeriodicTimer is the modern async form: a
loop awaits the next tick, so ticks never overlap and cancellation is a token.
Option Strict On
Imports System.Threading
Module Program
Async Function TicksAsync() As Task
Dim ticks As Integer = 0
Using timer As New PeriodicTimer(TimeSpan.FromMilliseconds(30))
Using cts As New CancellationTokenSource(TimeSpan.FromMilliseconds(130))
Try
Do While Await timer.WaitForNextTickAsync(cts.Token)
ticks += 1
Loop
Catch ex As OperationCanceledException
Console.WriteLine("PeriodicTimer stopped by the token after " & ticks.ToString() & " ticks (3 or 4 expected: " & (ticks >= 3 AndAlso ticks <= 4).ToString() & ")")
End Try
End Using
End Using
End Function
Sub Main()
Dim fired As Integer = 0
Dim second As New ManualResetEventSlim(False)
Using timer As New Timer(Sub(state)
If Interlocked.Increment(fired) = 2 Then second.Set()
End Sub, Nothing, 20, 20)
second.Wait()
Console.WriteLine("System.Threading.Timer fired on a pool thread, at least twice: " & (fired >= 2).ToString())
End Using
TicksAsync().GetAwaiter().GetResult()
End Sub
End Module
System.Threading.Timer fired on a pool thread, at least twice: True
PeriodicTimer stopped by the token after 4 ticks (3 or 4 expected: True)
Source: PeriodicTimer
(System.Timers.Timer and the Windows Forms and WPF timers also exist; the Windows Forms timer ticks on the UI thread, the others on pool threads.)
BackgroundWorker versus Async/Await
BackgroundWorker is the pre-Async way to run work off the UI thread: DoWork runs on a pool thread, ProgressChanged and RunWorkerCompleted are raised on the thread that
called RunWorkerAsync (through its SynchronizationContext). It still works on .NET, and Windows Forms code from the Framework era is full of it. New code replaces it with Await Task.Run(...) and
IProgress(Of T):
Option Strict On
Imports System.ComponentModel
Imports System.Threading
Module Program
Sub OldWay()
Dim worker As New BackgroundWorker With {.WorkerReportsProgress = True, .WorkerSupportsCancellation = True}
Dim reports As New List(Of String)
Dim finished As New ManualResetEventSlim(False)
AddHandler worker.DoWork, Sub(sender As Object, e As DoWorkEventArgs)
For i As Integer = 1 To 4
Thread.Sleep(20)
DirectCast(sender, BackgroundWorker).ReportProgress(i * 25)
Next
e.Result = "imported 4 batches"
End Sub
AddHandler worker.ProgressChanged, Sub(sender As Object, e As ProgressChangedEventArgs)
SyncLock reports
reports.Add(e.ProgressPercentage.ToString())
End SyncLock
End Sub
AddHandler worker.RunWorkerCompleted, Sub(sender As Object, e As RunWorkerCompletedEventArgs)
Console.WriteLine("BackgroundWorker completed: " & CStr(e.Result) & ", cancelled = " & e.Cancelled.ToString())
finished.Set()
End Sub
worker.RunWorkerAsync()
finished.Wait()
Thread.Sleep(100)
SyncLock reports
Console.WriteLine("progress reports (sorted): " & String.Join(" ", reports.OrderBy(Function(r) CInt(r))))
End SyncLock
End Sub
Async Function NewWay() As Task
Dim reports As New List(Of Integer)
Dim progress As New Progress(Of Integer)(Sub(p)
SyncLock reports
reports.Add(p)
End SyncLock
End Sub)
Dim result As String = Await Task.Run(Function()
For i As Integer = 1 To 4
Thread.Sleep(20)
DirectCast(progress, IProgress(Of Integer)).Report(i * 25)
Next
Return "imported 4 batches"
End Function)
Await Task.Delay(100)
Console.WriteLine("Task.Run + IProgress completed: " & result)
SyncLock reports
Console.WriteLine("progress reports (sorted): " & String.Join(" ", reports.OrderBy(Function(r) r)))
End SyncLock
End Function
Sub Main()
OldWay()
NewWay().GetAwaiter().GetResult()
End Sub
End Module
BackgroundWorker completed: imported 4 batches, cancelled = False
progress reports (sorted): 25 50 75 100
Task.Run + IProgress completed: imported 4 batches
progress reports (sorted): 25 50 75 100
Source: BackgroundWorker
In a console program there is no context, so the events run on pool threads in no fixed order (the output is sorted for that reason); in a Windows Forms form they run on the UI thread, in order. The
async form is shorter, composes with Try/Catch (exceptions arrive at the Await instead of in e.Error) and supports cancellation with a token instead of CancelAsync plus polling.
Option Strict On
Imports System.ComponentModel
Imports System.Windows.Forms
Public Class ImportForm
Inherits Form
Private ReadOnly _status As New Label() With {.Dock = DockStyle.Top}
Private ReadOnly _start As New Button() With {.Text = "Import", .Dock = DockStyle.Bottom}
Private WithEvents Worker As New BackgroundWorker() With {.WorkerReportsProgress = True}
Public Sub New()
Controls.Add(_status)
Controls.Add(_start)
AddHandler _start.Click, Sub(sender, e) Worker.RunWorkerAsync()
End Sub
' DoWork runs on a pool thread: it must not touch controls.
Private Sub Worker_DoWork(sender As Object, e As DoWorkEventArgs) Handles Worker.DoWork
For i As Integer = 1 To 4
Threading.Thread.Sleep(100)
Worker.ReportProgress(i * 25)
Next
End Sub
' ProgressChanged and RunWorkerCompleted are raised on the UI thread.
Private Sub Worker_ProgressChanged(sender As Object, e As ProgressChangedEventArgs) Handles Worker.ProgressChanged
_status.Text = e.ProgressPercentage.ToString() & "%"
End Sub
Private Sub Worker_RunWorkerCompleted(sender As Object, e As RunWorkerCompletedEventArgs) Handles Worker.RunWorkerCompleted
_status.Text = "done"
End Sub
' From any other thread: marshal to the UI thread explicitly.
Public Sub SetStatusFromAnyThread(text As String)
If InvokeRequired Then
Invoke(Sub() _status.Text = text)
Else
_status.Text = text
End If
End Sub
End Class
Module Program
<STAThread>
Sub Main()
Application.EnableVisualStyles()
Application.Run(New ImportForm())
End Sub
End Module
This compiles on net10.0-windows with UseWindowsForms; it was not run. The Handles clauses on WithEvents Worker wire the three events (see
Events and Handlers); InvokeRequired/Invoke is the way to touch a control from any other thread when you are not using Await. In WPF the
equivalent is Dispatcher.Invoke or Dispatcher.InvokeAsync. A control must only be touched on the thread that created it, and
Control.CheckForIllegalCrossThreadCalls = False hides the symptom without removing the bug.
Errors and what they mean
Option Strict On
Imports System.Threading
Module Program
Private Counter As Integer
Sub Work(batch As Integer)
End Sub
Sub Main()
SyncLock Counter ' a value type: boxed each time, so it would never lock
End SyncLock
Dim t As New Thread(AddressOf Work) ' Work(Integer) matches neither thread delegate
End Sub
End Module
Program.vb(12,18): error BC30582: 'SyncLock' operand cannot be of type 'Integer' because 'Integer' is not a reference type.
Program.vb(15,22): error BC30518: Overload resolution failed because no accessible 'New' can be called with these arguments:
Option Strict On
Module Program
Private Volatile Stopped As Boolean ' there is no Volatile keyword
Sub Main()
End Sub
End Module
Program.vb(4,22): error BC30205: End of statement expected.
Option Strict On
Imports System.Threading
Module Program
Sub Main()
Dim x As Integer = 0
Parallel.For(0, 10, Sub(i)
x += 1
Exit For
End Sub)
End Sub
End Module
Program.vb(10,33): error BC30096: 'Exit For' can only appear inside a 'For' statement.
| Code | Message (shortened) | Typical cause and fix |
|---|---|---|
BC30582 |
'SyncLock' operand cannot be of type 'Integer' because 'Integer' is not a reference type. |
Locking a value-type field. Lock a private |
BC30518 |
Overload resolution failed because no accessible 'New' can be called with these arguments. |
|
BC30205 |
End of statement expected. |
|
BC30096 |
'Exit For' can only appear inside a 'For' statement. |
|
BC37329 |
A value of type 'System.Threading.Lock' is not supported in SyncLock. |
Use |
BC42508 (warning) |
A value of type 'System.Threading.Lock' converted to a different type will use likely unintended monitor-based locking. |
A |
BC36943 |
'Await' cannot be used inside a 'Catch' statement, a 'Finally' statement, or a 'SyncLock' statement. |
See the async page. |
BC42324 (warning) |
Using the iteration variable in a lambda expression may have unexpected results. |
Copy the |
SYSLIB0006 (warning) |
Thread.Abort is not supported and throws PlatformNotSupportedException. |
Use cooperative cancellation. |
Visual Basic versus C#
| Topic | Visual Basic | C# |
|---|---|---|
Mutual exclusion |
|
|
|
not usable in |
|
Volatile fields |
no keyword; |
|
Value-type lock operand |
BC30582 |
CS0185 |
|
BC36943 |
CS1996 |
Thread entry |
|
|
Thread-static |
|
|
Parallel loop body |
|
lambda; |
Locals captured by lambdas |
can be passed |
same ( |
Query syntax on |
|
|
|
warning SYSLIB0006, |
same |
Older material you may meet
-
Visual Basic 6 had no real threading, and tutorials that start
ThreadwithThreadStart/Abortpairs date from the Framework era.Abort,SuspendandResumedo not work on .NET. -
Books that show the thread pool through
ThreadPool.QueueUserWorkItemandWaitCallbackare correct but have been superseded byTask.Run.BackgroundWorkerchapters remain valid for existing Windows Forms code. -
SyncLock MeandSyncLock GetType(Foo)appear in older samples; both are discouraged today because the lock object is visible to outsiders. -
Samples that rely on
Application.DoEvents()instead of a background thread are a workaround for a frozen UI; useAsync/Await. -
A lock on a value-type counter is a compile error (BC30582); old examples that "lock" a boxed
Integernever worked. -
Material that shows
Delegate.BeginInvokefor background work fails on .NET withPlatformNotSupportedException. -
The
Volatile.ReadandVolatile.Writemethods, andInterlocked, are the supported memory-ordering tools; there is no Visual BasicVolatilekeyword in any version.
Pitfalls checklist
| Pitfall | What to do |
|---|---|
Unsynchronised |
|
Locking on |
Lock on a private |
Taking two locks in different orders |
Define one global order; use |
Calling out (events, callbacks) while holding a lock |
Copy what you need, release, then call. |
|
|
Blocking pool threads ( |
Use |
Sharing a |
Concurrent collections, immutable snapshots, or one lock for every access. |
Side effects in |
Keep them pure; they can run more than once. |
|
They run once; initialise on first use. |
|
Measure; sequential is often faster. |
Updating controls from a non-UI thread |
|
Assuming |
It does not; use |
Using |
Cancel cooperatively. |
References
-
Managed threading with Managed threading basics, Managed threading best practices, The managed thread pool and Destroying threads
-
Overview of synchronization primitives, Cancellation in managed threads and Thread-local storage
-
Thread, ThreadPool, Monitor, Interlocked, Volatile and Lock
-
SemaphoreSlim, ReaderWriterLockSlim, Barrier, CountdownEvent and LazyThreadSafetyMode
-
Thread-safe collections, ConcurrentDictionary, BlockingCollection and ImmutableInterlocked
-
Task Parallel Library, Data parallelism, Introduction to PLINQ, Parallel and ParallelEnumerable
-
ThreadLocal(Of T), AsyncLocal(Of T), PeriodicTimer and BackgroundWorker
-
The lock statement (C#) for the C# side