Section 7.6. Asynchronous Invocation Pitfalls


7.6. Asynchronous Invocation Pitfalls

By now you have probably come to appreciate the elegance of .NET asynchronous calls, and the ease with which you can turn a synchronous component and its client code into an asynchronous implementation. However, no technology is without its pitfalls. Following is a rundown of the technical pitfalls you are likely to encounter when using .NET asynchronous calls. There is also a major conceptual consequence when dealing with asynchronous calls rather than synchronous calls, which merits a dedicated section of its own at the end of this chapter.

7.6.1. Threading Concurrency and Synchronization

In using asynchronous method calls, you must be aware of potential problems concerning thread concurrency, state corruption, and re-entrance. An asynchronous method is invoked on a thread from the .NET thread pool. When the call is made, the called object may already be servicing a normal call from a synchronous client on another thread, along with additional asynchronous calls on different threads. A completion callback method is also a potential pitfall, because it too is executed on a different thread and can have multiple threads calling it.

In general, you should invoke methods asynchronously only on thread-safe objectsthat is, objects that allow multiple threads to safely access them concurrently. Even when using thread-safe objects, you must keep a watchful eye out for race conditions and deadlocks. In addition, the object whose method you invoke asynchronously must not have thread affinity (i.e., it must not rely on always running on the same thread) or use thread-specific resources such as thread local storage or thread-relative static variables.

7.6.2. Thread-Pool Exhaustion

.NET speeds up asynchronous calls by using threads from the thread pool. However, the pool isn't boundless. A pool with too many threads becomes a liability, because the operating system wastes a lot of cycles just on thread context switches. If you have too many pending asynchronous method calls in progress, you may reach the pool's upper limit. At that point, no further asynchronous calls will be dispatched, and all future asynchronous calls will in effect be serialized, waiting for worker threads to return to the pool. The default .NET thread pool is configured to use 25 threads per CPU. Avoid indiscriminate use of asynchronous calls or any long or blocking operations in the methods invoked asynchronously. Consider using your own worker threads for long-duration blocking operations.

7.6.3. Premature Access by Reference and Reference Types

If the asynchronous method signature contains value types passed by reference or reference types, even though these parameters will be part of the call to BeginInvoke( ), you shouldn't try to access them before calling EndInvoke( ). Code can be especially error-prone when you use reference types as both input and output parameters. Example 7-16 demonstrates this point. In the example, the values of the X and Y member variables of the MyPoint object change silently between the call to BeginInvoke( ) and the return of EndInvoke( ). The only safe way to access the object passed as an incoming and outgoing parameter to BeginInvoke( ) is to call EndInvoke( ) first.

Example 7-16. Reference types have correct values only after the call to EndInvoke( )
 public class MyPoint {    public int X;    public int Y; } public delegate void MyDelegate(MyPoint obj); public class MyClient {    public void Swap(MyPoint obj)    {       int temp = obj.X;       obj.X = obj.Y;       obj.Y = temp;    }    public void AsyncSwap(  )    {       MyPoint point = new MyPoint (  );       point.X = 3;       point.Y = 4;       MyDelegate swapDel = Swap;       IAsyncResult asyncResult = swapDel.BeginInvoke(point,null,null);       //BeginInvoke(  ) does not change the values of reference types or value types       //passed by reference. This may or may not assert:       Debug.Assert(point.X == 3);       Debug.Assert(point.Y == 4);       swapDel.EndInvoke(asyncResult);       Debug.Assert(point.X == 4);       Debug.Assert(point.Y == 3);    } }

7.6.4. Lengthy Constructors

The asynchronous invocation mechanism described in this chapter can be used only on methods. In .NET, constructors are always synchronous. This can pose a problem if a component's constructor performs operations that take a long time to complete (such as opening a database connection). In such cases, you should use the two-phase create pattern. First put the trivial (and usually fast) code for initialization in the constructor, and then provide a separate method called Initialize( ) to perform the lengthy operations. This allows your clients to construct objects of your components asynchronously if they need to. A more advanced solution is to use a class factory and have the class factory expose methods such as Create( ) for synchronous instantiation and BeginCreate( ) and EndCreate( ) for asynchronous instantiation.

7.6.5. Cleaning Up After EndInvoke

Whenever calling BeginInvoke( ), the delegate will construct a new AsyncResult object and return it. That object will have a reference to a single WaitHandle object, accessible via the AsyncWaitHandle property. Calling EndInvoke( ) on the delegate used to dispatch the call does not close the handle. Instead, that handle will be closed when the AsyncResult object is garbage-collected. As with any other case of using an unmanaged resource, you have to be mindful about your application deterministic finalization needs. It is possible (in theory at least), for the application to dispatch asynchronous calls faster than .NET's ability to collect those handles, resulting with a resource leak. To compensate, you can explicitly close that handle after calling EndInvoke( ). For example, using the same definitions as Example 7-7, you would write:

     void OnMethodCompletion(IAsyncResult asyncResult)     {        AsyncResult resultObj = (AsyncResult)asyncResult;        BinaryOperation oppDel = (BinaryOperation)resultObj.AsyncDelegate;        int result = oppDel.EndInvoke(asyncResult);        asyncResult.AsyncWaitHandle.Close();        //Rest of the implementation     }

In a similar manner you can fix up EventsHelper.UnsafeFireAsync( ) to always close the handle after dispatching the event asynchronously by providing an anonymous method for a completion callback, and closing the handle in that method:

     public static void UnsafeFireAsync(Delegate del,params object[] args)     {        if(del == null)        {           return;        }        Delegate[] delegates = del.GetInvocationList(  );        AsyncFire asyncFire = InvokeDelegate;        AsyncCallback cleanup = delegate(IAsyncResult asyncResult)                                {                                   asyncResult.AsyncWaitHandle.Close( );                                };        foreach(Delgate sink in delegates)        {           asyncFire.BeginInvoke(sink,args,cleanUp,null);        }     }



Programming. NET Components
Programming .NET Components, 2nd Edition
ISBN: 0596102070
EAN: 2147483647
Year: 2003
Pages: 145
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net