Asynchronous delegates

From Figure 14.1, you can see that every delegate not only derives from MulticastDelegate, but also that several methods in addition to the Delegate and MulticastDelegate methods are available. When the compiler encounters the delegate keyword, it creates a class that extends MulticastDelegate and adds the following methods:

  • BeginInvoke

  • EndInvoke

  • Invoke

If you try to call the Invoke method directly from a delegate, you will receive the compile time error Invoke cannot be called directly on a delegate. Because each delegate can have a different signature and it is desirable to have a type safe interface, the compiler does some things automatically for you. The first thing that the compiler does is discover the number of arguments, the types of the arguments, and the return type and use that information to build the Invoke and BeginInvoke methods. These methods will be different for every different delegate that is defined. The signature of Invoke will be identical to the delegate declaration. If the delegate declaration has three arguments, then the Invoke method of the wrapper class will have three arguments. With BeginInvoke, the situation is similar to Invoke except two extra arguments are appended to the argument list, an AsyncCallback delegate, and a state object. The second thing that the compiler does with regards to delegates is that when it sees a delegate variable in the form of a function call, it transforms that sequence to a call to the Invoke method. You have already seen how the compiler expands this in Listing 14.4 and the related discussion. This section only discusses the BeginInvoke and EndInvoke methods.

Unlike Invoke, BeginInvoke and EndInvoke can be directly called from the delegate. BeginInvoke returns immediately on calling the function. It queues up a request to call the delegate. EndInvoke is like many of the other asynchronous callback implementations in that calling it returns the result of the delegate operation, or in other words, the return variable.

Armed with this information, try to rework one of the previous threading samples to use the asynchronous callback mechanism of the Delegate class. Because it is so visual, choose the DiningPhilosophers sample. Reviewing the code for the Dining Philosophers problem from Chapter 11, “Threading,” and the associated discussion would be helpful at this point.

The Dining Philosophers Problem Using delegates (Revisited)

The first problem that you might come across when converting this sample to use asynchronous callbacks is that no handle is returned from BeginInvoke that you can use to stop an asynchronous process like you can with a Thread (using Abort). The finest grain control available seems to be at the point where the delegate completes its work and the callback is called. You might choose to use a variable to decide whether a philosopher should start thinking, eating, and so on. The code that illustrates this is shown in Listing 14.24.

Listing 14.24. Recycling a Dining Philosopher
private void Recycler(IAsyncResult iar)
{
    StateHandler sh = (StateHandler)iar.AsyncState;
    sh.EndInvoke(iar);
    if(allStop != 0)
    {
        Thread.Sleep(r.Next(1000));
        sh.BeginInvoke(cb, sh);
    }
    else
    {
        // You are done eating and thinking
        BeginInvoke(colorChangeDelegate, new object[] {Color.White} );
        BeginInvoke(onPhilosopherLeave, new object[] {this, EventArgs.Empty} );
    }
}

If the allStop flag is non-zero, then the philosopher is still at the table contending for resources. Otherwise, another request is not queued, and the UI is changed to indicate this. The result of this decision is that cleanup does not happen immediately. You eventually see all of the philosophers' blocks turn white—just not all at once as in the threading version.

The next decision is how to start everything. This is relatively easy because not much changed between starting a thread and invoking an asynchronous callback. The process is kick-started with a call to each philosopher's Start method. Listing 14.25 shows this process.

Listing 14.25. Starting Up
    public DiningPhilosopher(Mutex rightChopstick, Mutex leftChopstick)
    {
        chopsticks = new Mutex[2];
        chopsticks[0] = rightChopstick;
        chopsticks[1] = leftChopstick;

        colorChangeDelegate = new ColorChangeDelegate(ColorChange);
        onPhilosopherLeave = new EventHandler(OnPhilosopherLeave);
        stateHandler = null;
        startThinkingTime = 0;
        stopThinkingTime = 0;
        r = new Random();
        cb = new AsyncCallback(Recycler);
        stateHandler = new StateHandler(OnStateChange);
    }
. . .
        public void Start()
    {
        BeginInvoke(colorChangeDelegate, new object[] {Color.Blue} );
        startThinkingTime = Environment.TickCount;
        stateHandler.BeginInvoke(cb, stateHandler);
    }
. . .

private void OnStart(object sender, System.EventArgs e)
{
    DiningPhilosopher.AllStart();
    foreach(Control b in Controls)
    {
        if(b is DiningPhilosopher)
        {
            ((DiningPhilosopher)b).Start();
        }
    }
}

The first two methods are extracted from the DiningPhilosopher control. The first is a constructor that shows how the callback and the delegate are set up for each control. The second method changes the control to reflect that it is “thinking” (blue), caches the current time stamp, and then queues up a request for an asynchronous callback. The second OnStart method is on the main form and is invoked as a result of clicking the Start button. This shows how the Start method of each control is invoked.

On startup, each control queues a call to OnStateChanged, which is shown in Listing 14.26.

Listing 14.26. State Change
private void OnStateChange()
{
        int elapsed = 0;
        if(WaitHandle.WaitAll(chopsticks, 100, false))
        {
            // Eating
            BeginInvoke(colorChangeDelegate, new object[] {Color.Green} );
            // Eat for a random amount of time (maximum of 1 second)
            Thread.Sleep(r.Next(1000));
            // Put down the chopsticks
            chopsticks[0].ReleaseMutex();
            chopsticks[1].ReleaseMutex();
            // Start to think
            BeginInvoke(colorChangeDelegate, new object[] {Color.Blue} );
            startThinkingTime = Environment.TickCount;
        }
        else
        {
            stopThinkingTime = Environment.TickCount;
            elapsed = stopThinkingTime - startThinkingTime;
            if(elapsed > 0 && elapsed <= 100)
            {
                BeginInvoke(colorChangeDelegate, new object[] {Color.BlueViolet} );
            }
. . .
            else
            {
                BeginInvoke(colorChangeDelegate, new object[] {Color.Red} );
            }
        }
    }

This function has two purposes. First, it waits for a maximum of 100 milliseconds for the chopsticks to become available. If they are available, then the color of the button turns green to indicate that the philosopher is eating. The philosopher eats for a random amount of time and then puts down his chopsticks. To indicate that the philosopher is thinking again, the button turns blue. If waiting for the chopsticks causes a timing out, then the color is changed to reflect how long it has been since the philosopher has eaten (in 100 millisecond increments). A large section of the code in this listing has been removed for brevity.

On return from OnStateChanged, the callback Recycler is called. This routine, as shown in Listing 14.26, just restarts everything again if the allStop variable so indicates. The final output looks identical to the output shown in Figure 11.7, so it will not be repeated here.

Now you have a version of the DiningPhilosopher application that does not explicitly create or start up any threads. If you are suspicious because it seems that you are getting something for nothing, you are right. Try running this application and the Task Manager at the same time. You are likely to see something like Figure 14.2.

Figure 14.2. DiningPhilosophers—Look Ma, no threads?


How does this happen? It appears that just as many threads are in the “threading” version, yet the program never explicitly created a thread. The answer again lies in what is being done for you. BeginInvoke gets its asynchronous behavior from the ThreadPool. In essence, every BeginInvoke call translates into a call to ThreadPool.QueueUserWorkItem. A new thread is created for each DiningPhilosopher—or is there? Remember that the ThreadPool had a finite set of Threads available for each process. Currently, the ThreadPool contains 25 Threads, so if a process hangs onto 25 or more Threads from the ThreadPool, then another Thread in the process might have to block waiting for a Thread to become free. To verify that this is what is happening, I wrote a small application that allows the user to start a number of the DiningPhilosopher applications. The application looks like Figure 14.3.

Figure 14.3. ThreadPool test.


Now when the Task Manager is run, the output looks like Figure 14.4.

Figure 14.4. ThreadPool test with Task Manager.


Notice that 40 Threads (20 philosophers and 2 applications) do not exist—the Thread count topped out at 31. If you were to add another DiningPhilosopher application, the number of Threads would still be 31 (or possibly 32). Again, this is because BeginInvoke uses one of the Threads from the ThreadPool, and the ThreadPool holds 25 Threads per process. I have created N AppDomains with an application running within each AppDomain so the AppDomains must live with this cap and share the ThreadPool with the other AppDomains.

Not only does this give insight into how asynchronous callbacks should be used, but it also further emphasizes the point made in Chapter 11: ThreadPool.QueueUserWorkItem should be used to accomplish short tasks. Hanging onto a Thread allocated from the ThreadPool could cause the application to seem slow and sluggish because of the contention for a central resource (that is, Threads from the ThreadPool).

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

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