Chapter 22. Introduction to Asynchronous Programming

Processes, Threads, and Asynchronous Programming

When you start a program, the system creates a new process in memory. A process is the set of resources that comprise a running program. These include the virtual address space, file handles, and a host of other things required for the program to run.

Inside the process, the system creates a kernel object, called a thread, which represents the actual execution of the program. (Thread is short for "thread of execution.") Once the process is set up, the system starts execution of the thread at the first statement in method Main.

Some important things to know about threads are the following:

  • By default, a process contains only a single thread, which executes from the beginning of the program to the end.

  • A thread can spawn other threads, so that at any time, a process might have multiple threads in various states, executing different parts of the program.

  • If there are multiple threads in a process, they all share the process's resources.

  • Threads are the units that are scheduled by the system for execution on the processor—not processes.

All the sample programs shown so far in this text have used only a single thread, and have executed sequentially from the first statement in the program to the last. This is called synchronous programming. Asynchronous programming refers to programs that spawn multiple threads, which are, at least conceptually, executed at the same time. (They might not actually be executed at the same time.)

If the program is running on a multiprocessor system, the different threads might actually be executing at the same time on different processors. This can considerably improve performance, and as multi-core processors become the norm, we need to write our programs to take advantage of this opportunity.

On a single-processor system, though, clearly only one instruction can be executed by the processor at a time. In this case, the operating system coordinates the threads so that the processor is shared among them. Each thread gets the processor for a short time, called a time slice, before being kicked off the processor and sent to the back of the line. This round-robin sharing of the processor lets all the threads work their ways through the code.

Multithreading Considerations

Using multiple threads in a program, called multithreading, or just threading, creates program overhead and additional program complexity. For example

  • There are time and resource costs in both creating and destroying threads.

  • The time required for scheduling threads, loading them onto the processor, and storing their states after each time slice is pure overhead.

  • Since the threads in a process all share the same resources and heap, it adds additional programming complexity to ensure that they are not stepping on each other's work.

  • Debugging multithreaded programs can be quite difficult, since the timing on each run of the program can be different, producing different results. And the act of running the program in a debugger blows the timing out of the water.

In spite of these considerations, the benefits of threading can far outweigh its costs, as long as it is used wisely—and not overused. For example, you've already seen that on a multiprocessor system, if the different threads can be placed on different processors, it can result in a much more efficient execution.

To help alleviate some of the costs associated with creating and destroying threads, the CLR maintains a thread pool for each process. Initially, a process's thread pool is empty, but after a thread is created and used by a process, and the thread completes its execution, it is not destroyed, but instead added to the process's thread pool. Later, if the process needs another thread, the CLR recycles one from the pool, saving a significant amount of time.

Another common example where multithreading is crucial is in graphical user interface (GUI) programming, where the user expects a quick response any time he or she clicks on a button or uses the keyboard. In this case, if the program needs to perform an operation that is going to take any appreciable time, it must perform that operation on another thread, leaving the main thread available to respond to the user's input. It would be totally unacceptable to have the program unresponsive during that time.

The Complexity of Multithreading

Although multithreading is conceptually easy, getting all the details right can be frustratingly difficult on nontrivial programs. The areas that need to be considered are the following:

  • Communicating between the threads: There are few built-in mechanisms for communicating between threads, so this is often done simply using memory, since memory is visible and accessible by all threads in the same process.

  • Coordinating threads: Although it's easy to create threads, you also need to be able to coordinate their actions. For example, a thread might need to wait for one or more other threads to complete before it can continue its execution.

  • Synchronization of resource usage: Since all the threads in a process share the same resources and memory, you need to make sure that the different threads aren't accessing and changing them at the same time, causing state inconsistencies.

The System.Threading namespace contains classes and types that you can use to build complex multithreaded systems. These include the Thread class itself, and classes such as Mutex, Semaphore, and Monitor, which are used to synchronize resource usage. The use, complexities, and nuances of this tricky subject are beyond the scope of this text, and you'd be better advised to settle down with an in-depth book on the subject.

You can, however, add very powerful multithreading to your programs through two simple techniques—asynchronous delegates and timers—which I will cover in the rest of this chapter. For most programs, these are probably the only techniques you will need.

Asynchronous Programming Patterns

So far in the text, all the code you've seen has been synchronous. C#, however, has an easy-to-use mechanism for executing a method asynchronously, using delegates.

In Chapter 15, we covered the topic of delegates, and you saw that when a delegate object is invoked, it invokes the methods contained in its invocation list. This is done synchronously, just as if the methods had been called by the program.

If a delegate object has only a single method (which I'll call the referenced method) in its invocation list, it can execute that method asynchronously. The delegate class has two methods, called BeginInvoke and EndInvoke, that are used to do this. These methods are used in the following way:

  • When you call the delegate's BeginInvoke method, it starts its referenced method executing on a separate thread from the thread pool, and then returns immediately to the initial thread. The initial thread can then continue on while the referenced method executes in parallel in the thread pool thread.

  • When your program wants to retrieve the results of the completed asynchronous method, it either checks the IsCompleted property of the IAsyncResult returned by BeginInvoke or calls the delegate's EndInvoke method to wait for the delegate to finish.

The three standard patterns for using this process are illustrated in Figure 22-1. In all three patterns, the initial thread initiates an asynchronous method call and then does some additional processing. The patterns differ, however, in the ways in which the initial thread receives the information that the spawned thread has completed.

  • In the wait-until-done pattern, after spawning the asynchronous method and doing some additional processing, the initial thread halts and waits for the asynchronous method to finish before continuing on.

  • In the polling pattern, the initial thread checks periodically whether the spawned thread has completed, and if not, it continues additional processing.

  • In the callback pattern, the initial thread continues execution without waiting or checking whether the spawned thread has completed. Instead, when the referenced method in the spawned thread is finished, the spawned thread then calls a callback method, which handles the results of the asynchronous method before calling EndInvoke.

The standard patterns for asynchronous method calls

Figure 22-1. The standard patterns for asynchronous method calls

BeginInvoke and EndInvoke

Before we look at examples of the asynchronous programming patterns, let's take a closer look at the BeginInvoke and EndInvoke methods. Some of the important things to know about BeginInvoke are the following:

  • When calling BeginInvoke, the actual parameters in the parameter list consist of the following:

    • The parameters required by the referenced method

    • Two additional parameters, called the callback parameter and the state parameter

  • BeginInvoke retrieves a thread from the thread pool and starts the referenced method running on the new thread.

  • BeginInvoke returns to the calling thread a reference to an object implementing the IAsyncResult interface. This interface reference contains information about the current state of the asynchronous method. The initial thread then continues execution.

The following code shows an example of calling a delegate's BeginInvoke method. The first line declares a delegate type called MyDel. The next line declares a method called Sum, which matches the delegate.

  • The following line declares a delegate object called del, of the delegate type MyDel, and initializes its invocation list with the Sum method.

  • Finally, the last line of code calls the BeginInvoke method of the delegate object, and supplies it with the two delegate parameters 3 and 5, and the two BeginInvoke parameters callback and state, which are set to null in this example. When executed, the BeginInvoke method performs two actions:

    • It gets a thread from the thread pool and starts method Sum running on the new thread, supplying it with 3 and 5 as its actual parameters.

    • It collects information about the state of the new thread and makes it available through a reference to an interface of type IAsyncResult, which it returns to the calling thread. The calling thread stores it in a variable called iar.

BeginInvoke and EndInvoke

The EndInvoke method is used to retrieve the values returned by the asynchronous method call, and to release resources used by the thread. EndInvoke has the following characteristics:

  • It takes as a parameter the reference to the IAsyncResult returned by the BeginInvoke method, and finds the thread it refers to.

  • If the thread pool thread has exited, EndInvoke does the following:

    • It cleans up the exited thread's loose ends and disposes of its resources.

    • It finds the value returned by the referenced method and returns that value as its return value.

  • If the thread pool thread is still running when EndInvoke is called, the calling thread stops and waits for it to finish before cleaning up and returning the value. Because EndInvoke cleans up after the spawned thread, you must make sure that an EndInvoke is called for each BeginInvoke.

  • If the asynchronous method triggers an exception, the exception is raised when EndInvoke is called.

The following line of code shows an example of calling EndInvoke to retrieve the value from an asynchronous method. You must always include the reference to the IAsyncResult object as a parameter.

BeginInvoke and EndInvoke

EndInvoke supplies all the output from the asynchronous method call, including ref and out parameters. If a delegate's referenced method has ref or out parameters, they must be included in EndInvoke's parameter list before the reference to the IAsyncResult object, as shown here:

BeginInvoke and EndInvoke

The Wait-Until-Done Pattern

Now that you understand the BeginInvoke and EndInvoke delegate methods, we can look at the asynchronous programming patterns. The first one we'll look at is the wait-until-done pattern. In this pattern, the initial thread initiates an asynchronous method call, does some additional processing, and then stops and waits until the spawned thread finishes. It is summarized as follows:

IAsyncResult iar = del.BeginInvoke( 3, 5, null, null );
        // Do additional work in the calling thread, while the method
        // is being executed asynchronously in the spawned thread.
        ...
   long result = del.EndInvoke( iar );

The following code shows a full example of this pattern. This code uses the Sleep method of the Thread class to suspend itself for 100 milliseconds (1/10 of a second). The Thread class is in the System.Threading namespace.

using System.Threading;                         // For Thread.Sleep()
      ...
   delegate long MyDel( int first, int second );   // Declare delegate type

   class Program
   {
      static long Sum(int x, int y)                // Declare method for async
      {
         Console.WriteLine("                       Inside Sum");
         Thread.Sleep(100);

         return x + y;
      }
static void Main( )
      {
         MyDel del = new MyDel(Sum);

         Console.WriteLine( "Before BeginInvoke" );
         IAsyncResult iar = del.BeginInvoke(3, 5, null, null); // Start async
         Console.WriteLine( "After  BeginInvoke" );

         Console.WriteLine( "Doing stuff" );

         long result = del.EndInvoke( iar );    // Wait for end and get result
         Console.WriteLine( "After  EndInvoke: {0}", result );
      }
   }

This code produces the following output:

Before BeginInvoke
After  BeginInvoke
Doing stuff
                      Inside Sum

After  EndInvoke: 8

The AsyncResult Class

Now that you've seen BeginInvoke and EndInvoke in action in their simplest forms, it's time to take a closer look at IAsyncResult, which is an integral part of using these methods.

BeginInvoke returns a reference to an IAsyncResult interface that is inside a class object of type AsyncResult. The AsyncResult class represents the state of the asynchronous method. Figure 22-2 shows a representation of some of the important parts of the class. The important things to know about the class are the following:

  • When you call a delegate object's BeginInvoke method, the system creates an object of the class AsyncResult. It doesn't, however, return a reference to the class object. Instead it returns a reference to the IAsyncResult interface contained in the object.

  • An AsyncResult object contains a property called AsyncDelegate, which returns a reference to the delegate that was invoked to start the asynchronous method. This property, however, is part of the class object, but not part of the interface.

  • The IsCompleted property returns a Boolean value indicating whether the asynchronous method has completed.

  • The AsyncState property returns a reference to the object that was listed as the state parameter in the BeginInvoke method invocation. It returns a reference of type object. I will explain this in the section on the callback pattern.

An AsyncResult class object

Figure 22-2. An AsyncResult class object

The Polling Pattern

In the polling pattern, the initial thread initiates an asynchronous method call, does some additional processing, and then uses the IsCompleted method of the IAsyncResult object to check periodically whether the spawned thread has completed. If the asynchronous method has completed, the initial thread calls EndInvoke and continues on. Otherwise, it does some additional processing and checks again later. The "processing" in this case just consists of counting from 0 to 10,000,000.

The Polling Pattern

This code produces the following output:

After BeginInvoke
Not Done
                 Inside Sum
Not Done
Not Done
Done
Result: 8

The Callback Pattern

In the previous two patterns, wait-until-done and polling, the initial thread continues on with its flow of control only after it knows that the spawned thread has completed. It then retrieves the results and continues.

The callback pattern is different in that once the initial thread spawns the asynchronous method, it goes on its way without synchronizing with it again. When the asynchronous method call completes, the system invokes a user-supplied method to handle its results, and to call the delegate's EndInvoke method. This user-defined method is called a callback method, or just callback.

The two extra parameters at the end of the BeginInvoke parameter list are used with the callback method as follows:

  • The first of the two parameters, the callback parameter, is the name of the callback method.

  • The second parameter, the state parameter, can be either null or a reference to an object you want passed into the callback method. You'll be able to access this object through the method's IAsyncResult parameter using its AsyncState property. The type of this parameter is object.

The Callback Method

The signature and return type of the callback method must be of the form described by the AsyncCallback delegate type. This form requires that the method take a single parameter of type IAsyncResult and have a void return type, as shown here:

void AsyncCallback( IAsyncResult iar )

There are several ways you can supply the callback method to the BeginInvoke method. Since the callback parameter in BeginInvoke is a delegate of type AsyncCallback, you can supply it as a delegate, as shown in the first code statement that follows. Or you can just supply the name of the callback method and let the compiler create the delegate for you. Both forms are semantically equivalent.

The Callback Method

The second BeginInvoke parameter is used to send an object to the callback method. It can be an object of any type, but the parameter is of type object, so inside the callback method you will have to cast it to the correct type.

Calling EndInvoke Inside the Callback Method

Inside the callback method, your code should call the delegate's EndInvoke method and take care of handling the output results of the asynchronous method execution. To call the delegate's EndInvoke method, though, you need a reference to the delegate object, which is in the initial thread—not here in the spawned thread.

If you're not using BeginInvoke's state parameter for anything else, you can use it to send the delegate reference to the callback method, as shown here:

Calling EndInvoke Inside the Callback Method

Otherwise, you can extract the delegate's reference from the IAsyncResult object sent into the method as the parameter. This is shown in the following code and illustrated in Figure 22-3.

  • The single parameter to the callback method is a reference to the IAsyncResult interface of the asynchronous method that has just completed. Remember that the IAsyncResult interface object is inside the AsyncResult class object.

  • Although the IAsyncResult interface doesn't have a reference to the delegate object, the AsyncResult class object enclosing it does have a reference to the delegate object. So the first line inside the example method body gets a reference to the class object by casting the interface reference to the class type. Variable ar now has a reference to the class object.

  • With the reference to the class object, you can now call the AsyncDelegate property of the class object and cast it to the appropriate delegate type. This gives you the delegate reference, which you can then use to call EndInvoke.

using System.Runtime.Remoting.Messaging;     // Contains AsyncResult class

void CallWhenDone( IAsyncResult iar )
{
   AsyncResult ar = (AsyncResult) iar;       // Get class object reference
   MyDel del = (MyDel) ar.AsyncDelegate;     // Get reference to delegate

   long Sum = del.EndInvoke( iar );          // Call EndInvoke
      ...
}
Extracting the delegate's reference inside the callback method

Figure 22-3. Extracting the delegate's reference inside the callback method

The following code puts it all together, and is an example of using the callback pattern.

using System.Runtime.Remoting.Messaging;  // To access the AsyncResult type
      ...
   delegate long MyDel(int first, int second);

   class Program {
      static long Sum(int x, int y)
      {
         Console.WriteLine("                         Inside Sum");
         Thread.Sleep(100);
         return x + y;
      }

      static void CallWhenDone(IAsyncResult iar) {
         Console.WriteLine("                         Inside CallWhenDone.");
         AsyncResult ar = (AsyncResult) iar;
         MyDel del = (MyDel)ar.AsyncDelegate;

         long result = del.EndInvoke(iar);
         Console.WriteLine
            ("                         The result is: {0}.", result);
      }

      static void Main() {
         MyDel del = new MyDel(Sum);

         Console.WriteLine("Before BeginInvoke");
         IAsyncResult iar =
            del.BeginInvoke(3, 5, new AsyncCallback(CallWhenDone), null);

         Console.WriteLine("Doing more work in Main.");
         Thread.Sleep(500);
         Console.WriteLine("Done with Main. Exiting.");
      }
   }

This code produces the following output:

Before BeginInvoke
Doing more work in Main.
                        Inside Sum
                        Inside CallWhenDone.
                        The result is: 8.

Done with Main. Exiting.

Timers

Timers provide another way to run an asynchronous method on a regular, recurring basis. Although there are several Timer classes available in the .NET BCL, I'll describe the one in the System.Threading namespace.

The important things to know about this timer class are the following:

  • The timer uses a callback method that is called each time the timer expires. The callback method must be in the form of the TimerCallback delegate, which has the following form. It takes a single parameter of type object, and has a void return type.

    void TimerCallback( object state )
  • When the timer expires, the system sets up the callback method on a thread from the thread pool, supplies the state object as its parameter, and starts it running.

  • You can set a number of the timer's characteristics, including the following:

    • The dueTime is the amount of time before the first call of the callback method. If dueTime is set to the special value Timeout.Infinite, the timer will not start. If it's set to 0, the callback will be called immediately.

    • The period is the amount of time between each successive call of the callback method. If its value is set to Timeout.Infinite, the callback won't be called after the first time.

    • The state is either null or a reference to an object to be passed to the callback method each time it's executed.

The constructor for the Timer class takes as parameters the name of the callback method, the dueTime, the period, and the state. There are several constructors for Timer; the one that is probably the most commonly used has the following form:

Timer( TimerCallback callback, object state, uint dueTime, uint period)

The following code statement shows an example of the creation of a Timer object:

Timers

Once a Timer object is created, you can change its dueTime or period using the Change method.

The following code shows an example of using a timer. The Main method creates the timer so that it will call the callback for the first time after two seconds, and once every second after that. The callback method simply prints out a message, including the number of times it has been called.

Timers

This code produces the following output before being terminated after about 5 seconds:

Timer started.
Processing timer event 1
Processing timer event 2
Processing timer event 3
Processing timer event 4

There are several other timer classes supplied by the .NET BCL, each having its own uses. The other timer classes are the following:

  • System.Windows.Forms.Timer: This class is used in Windows Forms applications to periodically place WM_TIMER messages into the program's message queue. When the program gets the message from the queue, it processes the handler synchronously on the main user interface thread. This is extremely important in Windows Forms applications.

  • System.Timers.Timer: This class is more extensive, and contains a number of members for manipulating the timer through properties and methods. It also has a member event called Elapsed, which is raised when each period expires. This timer can run on either a user interface thread or a worker thread.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.166.255