Title: Use BeginInvoke and EndInvoke to perform tasks asynchronously in C#
This example uses the Emboss method to create embossed images. How that method works isn't important to the discussion of working asynchronously so it isn't covered here. Download the example to see how it works. The only thing about that method that matters for this discussion is that it is slow so running it on multiple cores or CPUs can save time.
The program uses the following code to emboss its four images synchronously.
// Emboss the images.
pictureBox1.Image = Emboss(Images[0]);
pictureBox1.Refresh();
pictureBox2.Image = Emboss(Images[1]);
pictureBox2.Refresh();
pictureBox3.Image = Emboss(Images[2]);
pictureBox3.Refresh();
pictureBox4.Image = Emboss(Images[3]);
pictureBox4.Refresh();
The images that the program embosses are stored in the Images array. The code passes each image into the Emboss method and displays the result in a PictureBox.
To emboss the images asynchronously, the program must first make a delegate representing the Emboss method. The following code shows the delegate's declaration.
// Make a delegate representing the Emboss extension method.
private delegate Bitmap EmbossDelegate(Bitmap bm);
This delegate simply represents a method that takes a Bitmap as a parameter and that returns a Bitmap.
The following code shows the key lines where the program embosses the images asynchronously.
// Copy the images.
Bitmap bm1 = (Bitmap)Images[0].Clone();
Bitmap bm2 = (Bitmap)Images[1].Clone();
Bitmap bm3 = (Bitmap)Images[2].Clone();
Bitmap bm4 = (Bitmap)Images[3].Clone();
// Start the threads.
EmbossDelegate caller = Emboss;
IAsyncResult result1 = caller.BeginInvoke(Images[0], null, null);
IAsyncResult result2 = caller.BeginInvoke(Images[1], null, null);
IAsyncResult result3 = caller.BeginInvoke(Images[2], null, null);
IAsyncResult result4 = caller.BeginInvoke(Images[3], null, null);
// Wait for the threads to complete.
pictureBox1.Image = caller.EndInvoke(result1);
pictureBox2.Image = caller.EndInvoke(result2);
pictureBox3.Image = caller.EndInvoke(result3);
pictureBox4.Image = caller.EndInvoke(result4);
The program will pass Bitmap objects into asynchronously running threads. Unfortunately if the user interface is using a Bitmap to update a PictureBox while the thread tries to access it, you may get a cryptic error message saying "the object is in use by another process" or something similarly uninformative. To avoid that, the program makes copies of the images and then sends the copies into the asynchronous threads.
The program makes a delegate variable named caller that refers to the Emboss method. It then calls the caller object's BeginInvoke method for each image, passing the images as the first argument to BeginInvoke. The second and third arguments are for a callback method to be called when each thread finishes. This example doesn't use callbacks so it passes null for those arguments.
Each call to BeginInvoke starts the Emboss method running in a separate thread. After the program makes all of the calls to BeginInvoke, there are five threads running, one for each method call plus the main UI thread. The system will distribute the threads on the computer's cores if possible.
The main program should continue doing as much as it can but eventually it needs to wait for the other threads to complete. To do that, it calls the caller object's EndInvoke method passing it the IAsyncResult object it received when it called BeginInvoke. The IAsyncResult object lets EndInvoke know for which thread to wait.
It doesn't matter in what order the program waits for the threads because it needs to wait for all of them. The only important thing here is that it starts all of the threads before it waits for any of them.
Note also that only the UI thread, the user-interface thread that created the form's controls, can interact directly with the form's controls. That means, for example, that the Emboss method cannot set a PictureBox control's Image property. You can use the form's Invoke method to work around that problem if necessary but this example doesn't need to because it's the main UI thread that eventually sets the PictureBox controls' Image properties after the calls to EndInvoke return.
In one set of tests on my dual-core laptop, creating the embossed images took roughly 2.10 seconds synchronously but only 1.31 seconds asynchronously. Running asynchronously on two cores takes slightly more than half the time needed to run synchronously due to overhead, but it's still a pretty nice improvement. For a longer operation the time saved would be worth the extra complexity.
Download the example to experiment with it and to see additional details.
|