Programming with Timer Callbacks
Many applications have the need to call a specific method during regular intervals of time.
static void PrintTime(object state)
{
Console.WriteLine(DateTime.Now.ToLongTimeString());
}Notice that PrintTime() method has a single parameter of type System.Object and returns void. This is not optional, given that the TimerCallback delegate can only call methods that match this signature.
The next step is to configure an instance of the TimerCallback delegate and pass it into the Timer object.
// Create the delegate for the Timer type.
TimerCallback timeCB = new TimerCallback(PrintTime);
Timer t = new Timer(
timeCB,
null, // Any info to pass into the called method (null for no info) -> you can write "info"
0,// Amount of time to wait before starting (in milliseconds)
1000 // Interval of time between calls (in milliseconds)
);
_ = new Timer(); // Using a stand-alone discardUnderstanding The ThreadPool
The thread pool holds onto created (but inactive) threads until needed.
If you want to queue a method call for processing by a worker thread in the pool, you can use the ThreadPool. QueueUserWorkItem() method. This method has been overloaded to allow you to specify an optional System.Object for custom state data in addition to an instance of the WaitCallBack delegate.
public void PrintNumbers()
{
Console.WriteLine(Environment.CurrentManagedThreadId);
Printer p = new Printer();
WaitCallback w = new WaitCallback(PrintTheNumbers);
// Queue the method 10 times
for (int i = 0; i < 10; i++)
{
ThreadPool.QueueUserWorkItem(w, p);
}
}
private void PrintTheNumbers(object? state)
{
Printer task = (Printer)state;
task.PrintNumbers();
}Parallel Programming Using the Task Parallel Library
Microsoft introduced a new approach multithreaded application development using a parallel programming library termed the Task Parallel Library (TPL).
The System.Threading.Tasks Namespace
The types of System.Threading.Tasks are referred to as the TPL (Task Parallel Library).
The Role of the Parallel Class
A key class of the TPL is System.Threading.Tasks.Parallel. This class contains methods that allow you to iterate over a collection of data in a parallel fashion, mainly through 2 primary static methods, Parallel.For() and Parallel.Foreach(), each of which defines numerous overloaded versions.
Data Parallelism with the Parallel Class
The first way to use the TPL is to perform "data parallelism" . Simply put, this term refers to the task of iterating over an array or collection in a parallel manner using the Parallel.For() or Parallel.ForEach() method.
Note: When you are building a multithreaded GUI app, secondary threads can never directly access user interface controls.
- TPL = Task Parallel Library
- Distributes workload across CPUs automatically.
- You don't manage threads - TPL handles thread pool, scheduling state.
- Works alongside System.Threading (Monitor,Interlocked still valid.)
// BEFORE - sequential single thread, UI freezes
private void ProcessFiles()
{
string[] files = Directory.GetFiles(@"C:\Pictures", "*.jpg", SearchOption.AllDirectories);
foreach (string c in files)
{
string fileName = Path.GetFileName(c);
using (Bitmap bi = new Bitmap(c))
{
bi.RotateFlip(RotateFlipType.Rotate180FlipXY);
bi.Save(Path.Combine(@"C:\MyDir",fileName));
}
}
}
// AFTER - Parallel, Multithreade, much faster
string[] files = Directory.GetFiles(@"C:\Pictures", "*.jpg", SearchOption.AllDirectories);
Parallel.ForEach(files, c =>
{
string fileName = Path.GetFileName(c);
// UI thread access - must use Dispatcher.
Dispatcher?.Invoke(() =>
{
this.Title = $"{fileName}";
});
using (Bitmap bi = new Bitmap(c))
{
bi.RotateFlip(RotateFlipType.Rotate180FlipXY);
}
});Without Dispatcher = Runtime crash.
The Task Class
The Task class allows you to easily invoke a method on a secondary thread and can be used as a simple alternative to using asynchronous delegates.
Task = a single unit of work that runs asynchronously.
Parallel.For/Foreach manages tasks for you automatically.
Task class gives you MANUAL control over individual tasks.
// WAY 1
// Creating and Running a Task
Task.Run(() =>
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId}");
});
// WAY 2
Task t = new Task(() => DoWork());
t.Start();
// WAY 3 - Task.Factory.StartNew
Task.Factory.StartNew(() => DoWork());
// Way 1 - waiting for task
Task t1 = Task.Run(() => DoHeavyWork());
t1.Wait();
// Wait for multiple
Task t2 = Task.Run(() => DoWork1());
Task t3 = Task.Run(() => DoWork2());
Task.WaitAll(t2, t3); // Wait for all to finsh
Task.WaitAny(t2, t3); // Wait for any to finish
// 3 - Returning a value - Task<T>
Task<int> task = Task.Run(() => {
return 42;
});
int result = task.Result;
// 4 - CONTIUNUTATION
Task.Run(() => Asd())
.ContinueWith(prev.Task => {
Console.WriteLine("Download finish");
ProcessFile();
});Handling Cancellation Request
Cancellation = let the user stop a long-running parallel operation.
CancellationTokenSource -> the "remote control" (sends cancel signal)
CancellationToken -> the "receiver" (checks for cancel signal)
ThrowIfCancellationRequest() -> "am I cancelled? if us, throw"
// Cancel button
private void cmdCancel_Click(object sender, EventArgs e)
{
_cancelToken.Cancel(); // Sends the cancel signal
}
// PROCESS BUTTON - starts parallel work
private void cmdProcess_Click(object sender, EventArgs e)
{
// Reset the token.
_cancelToken = new CancellationTokenSource();
this.Title("Processing...");
Task.Factory.StartNew(() => ProcessFiles());
}
// pass the token
ParallelOptions opt = new ParallelOptions()
{
CancellationToken = _cancelToken.Token,
MaxDegreeOfParallelism = Environment.ProcessorCount
};
try
{
string[] files = { "MyFile" };
Parallel.ForEach(files, opt, currentfile =>
{
// CHECK: has the user clicked cancel?
_cancelToken.Token.ThrowIfCancellationRequested();
// If yes -> throws OperationCancelledException
// If no -> continues working
string fileName = "";
Dispatch?.Invoke(...);
using (Bitmap..)
{
}
});
}
catch (Exception ex)
{
// User clicked Cancel - All threads stopped
Dispather?.Invoke(() => this.Title = ex.Message);
throw;
}Task Parallelism Using the Parallel Class
The TPL can also be used to easily fire off any number of asynchronous tasks using the Parallel.Invoke() method.
Data Parallelism = Same operation on MANY items (ForEach)
Task Parallelism = DIFFERENT operation at the same time (Invoke)
Parallel.Invoke(
() => { Thread.Sleep(2000); Console.WriteLine("Job A done (2 sec)"); },
() => { Thread.Sleep(3000); Console.WriteLine("Job B done (3 sec)"); },
() => { Thread.Sleep(4000); Console.WriteLine("Job C done (4 sec)"); }
);Parallel LINQ Queries (PLINQ)
If you choose, you can use a set of extension methods that allow you to construct a LINQ query that will perform its workload in parallel (if possible). Fittingly, LINQ queries that are designed to tun in parallel are termed PLINQ queries.
- AsParallel() : Specifies that the rest of query should be parallelized, if possible.
- WithCancellation() : Specifies that PLINQ should periodically monitor the state of the provided cancellation token and cancel execution if it is required.
- WithDegreeOfParallelism() : Specifies the max. number of processors that PLINQ should use to parallelize the query.
- ForAll() : Enables results to be processed in parallel without first merging back to the consumer thread, as would be the case when enumerating a LINQ result using the foreach keyword.
void ProcessIntData()
{
int[] source = Enumerable.Range(1, 10_000_000).ToArray();
int[] modThreeIsZero = (from num in source where num % 3 == 0 orderby num descending select num).ToArray();
}Opting to a PLINQ Query
If you want to inform the TPL to execute this query in parallel (if possible), you will want to use the AsParallel() extension method so:
int[] modThreeIsZero = (from num in source.AsParallel() where num % 3 == 0 orderby num descending select num).ToArray();By including a call to AsParallel(), the TPL will attempt to pass the workload off to any available CPU.
Cancelling a PLINQ Query
It's also possible to use a CancellationTokenSource object to inform a PLINQ query to stop processing under the correct conditions.
CancellationToken token = new();
int[] modThreeIsZero = (from num in source.AsParallel().WithCancellation(token) where num % 3 == 0 orderby num descending select num).ToArray();