Skip to content
AgileLogics
6 min read

Tasks, Parallelism, and Cancellation

Prerequisites: Async and await

Async/Await is for waiting efficiently. For CPU-heavy work — calculations, image processing — you can split the work across processor cores.

Parallel.For and Task.Run

Imports System.Threading
Imports System.Threading.Tasks

Module Program
    Function IsPrime(n As Integer) As Boolean
        If n < 2 Then Return False
        For d = 2 To CInt(Math.Sqrt(n))
            If n Mod d = 0 Then Return False
        Next
        Return True
    End Function

    Sub Main()
        Dim count = 0
        Parallel.For(1, 200001, Sub(n)
                                    If IsPrime(n) Then Interlocked.Increment(count)
                                End Sub)
        Console.WriteLine($"primes below 200001: {count}")

        Dim squares = Enumerable.Range(1, 10).AsParallel().AsOrdered().Select(Function(x) x * x).ToArray()
        Console.WriteLine(String.Join(" ", squares))

        Dim total = Task.Run(Function() Enumerable.Range(1, 1000).Sum()).Result
        Console.WriteLine(total)
    End Sub
End Module
primes below 200001: 17984
1 4 9 16 25 36 49 64 81 100
500500
  • Parallel.For runs the loop body on several threads at once. Because many threads update count, it must use Interlocked.Increment — a plain count += 1 would lose updates.
  • AsParallel() (PLINQ) runs a LINQ query in parallel; AsOrdered() keeps the original order.
  • Task.Run moves work to a background thread — in a desktop app, Await Task.Run(...) keeps the UI responsive during a long calculation.

Protecting shared data

Imports System.Collections.Concurrent
Imports System.Threading.Tasks

Module Program
    Sub Main()
        Dim counts As New ConcurrentDictionary(Of Integer, Integer)
        Parallel.For(0, 10000, Sub(i)
                                   counts.AddOrUpdate(i Mod 3, 1, Function(key, old) old + 1)
                               End Sub)
        For Each key In counts.Keys.OrderBy(Function(k) k)
            Console.WriteLine($"{key}: {counts(key)}")
        Next

        Dim gate As New Object()
        Dim total = 0
        Parallel.For(1, 101, Sub(i)
                                 SyncLock gate
                                     total += i
                                 End SyncLock
                             End Sub)
        Console.WriteLine(total)
    End Sub
End Module
0: 3334
1: 3333
2: 3333
5050

SyncLock lets only one thread at a time run the enclosed code. Collections in System.Collections.Concurrent are safe to use from many threads.

Cancellation

Long operations should be cancellable. Pass a CancellationToken and check it:

Imports System.Threading

Module Program
    Async Function CountAsync(token As CancellationToken) As Task
        For i = 1 To 10
            Await Task.Delay(200, token)
            Console.WriteLine($"tick {i}")
        Next
    End Function

    Sub Main()
        Using cts As New CancellationTokenSource(500)   ' cancel after 500 ms
            Try
                CountAsync(cts.Token).GetAwaiter().GetResult()
            Catch ex As OperationCanceledException
                Console.WriteLine("cancelled")
            End Try
        End Using
    End Sub
End Module
tick 1
tick 2
cancelled

In a desktop app, a Cancel button calls cts.Cancel(). Most .NET async methods (HttpClient, file and database APIs) accept a token.

Reporting progress

IProgress(Of T) reports progress back to the caller — in Windows Forms, the callback runs on the UI thread, so it can update a ProgressBar directly:

Module Program
    Async Function ProcessAsync(progress As IProgress(Of Integer)) As Task
        For percent = 25 To 100 Step 25
            Await Task.Delay(50)
            progress.Report(percent)
        Next
    End Function

    Sub Main()
        Dim reporter = New Progress(Of Integer)(Sub(p) Console.WriteLine($"{p}%"))
        ProcessAsync(reporter).GetAwaiter().GetResult()
        Threading.Thread.Sleep(100)   ' let the last report print in a console app
    End Sub
End Module
25%
50%
75%
100%