Introduction to Asynchronous Programming in .NET 2.0

 
Xiaolin (Colin) Peng
 
WitStream Technologies Inc.
 
Asynchronous programming is to write computer programs to carry out computer operations asynchronously. The main reason to use asynchronous operations is to better use the CPU cycles, therefore achieve high-performance in your applications.
 
Asynchronous programming is usually done through process/thread API. Application developers identify tasks that can be executed in parallel, then create a process/thread to each of those tasks. In runtime, depending on the number of actual tasks and the number of CPUs available, multiple processes/threads may be running at the same time to execute the tasks.
 
   1.2 Thread and ThreadPool
 
     1.2.1 ThreadStart Delegate
 
Threads can be created explicitly through thread API or implicitly through thread pool API on .NET. Let’s see some examples.
 
To create a thread is very simple, if you have a method that is either compute-bound or I/O-bound, and if this method is potentially called many times in the runtime, and more importantly, if this method can be called in parallel, then a thread can be created to execute this method.
 
      void MyThreadMethod()
      {
            //this method is either doing extensive I/O
            //or doing lots of computing
            //it may take some time to finish
      }
 
      void CreateAndRunThread()
      {
            //create a ThreadStart delegate
            ThreadStart threadMethod =
            new ThreadStart(this.MyThreadMethod);
           //create a thread
            Thread myThread = new Thread(threadMethod);
            //start the thread
            myThread.Start();
}
 
The above thread is created through a delegate type ThreadStart. The prototype of this delegate requires the method does not take any arguments and does not return any type.
 
If your have a method that takes arguments and returns a type, and you want to use thread to execute it, then you will have to either refactor it so you can use ThreadStart delegate to create thread or you use ThreadPool.
 
1.2.2      Refactoring
 
 Refactoring a method to use ThreadStart may take time and is not always the best solution.
 
 Let’s say we have a two-argument method that returns a type:
  private MyReturnType MyMethod(MyArgType1 arg1, MyArgType2 arg2)
        {
            //this method is either doing extensive I/O
            //or doing lots computing
            //it may take some time to finish
        }
 
        public void DoWork()
        {
            //MyMethod will be executed many times
            //MyMethod can be executed in parallel
            IList<MyReturnType> results = new List<MyReturnType>();
            for( int i = 0; i < 10; i++)
            {
                MyReturnType result = MyMethod(arg1, arg2);
                results.Add(result);
            }
            //do you work with results
  }
DoWork() method calls MyMethod() in sequential, it may take a long time to finish. To speed up the execution of DoWork() method, we like to create threads to run in parallel. But we can not use ThreadStart delegate because MyMethod does not have the required prototype.
 
To refactor, we can create a new internal type somewhat looks like this:
 
internal class MyClass2
      {
         public MyArgType1 Arg1
         {
             get;
             set;
         }
 
         public MyArgType2 Arg2
         {
             get;
             set;
         }
 
         public MyReturnType Result
         {
             get;
         }
 
         public void Process()
         {
             //this method is a copy of MyMethod execept
             //1. MyMethod arguments are set through properties
             //2. return value is set to the internal variable
             //3. caller will get the result by calling Result property
         }
 
      }
 
Please note that the actual refactoring may be much more complicated. The complexity depends on the actual method MyMethod().
 
The new internal type’s Process() method has the same execution logic as the original MyMethod(). Arguments are set through properties, so Process() method can still access the inputs it needs to finish the operation.
 
Now after the refactoring, we can create threads to execute in parallel.
  public void DoWorkUseThread()
        {
            IList<MyReturnType> results = new List<MyReturnType>();
            IList<Thread> threads = new List<Thread>();
            IList<MyClass2> exeClasses = new List<MyClass2>();
            for (int i = 0; i < 10; i++)
            {
                //create a class
                MyClass2 mc = new MyClass2();
                mc.Arg1 = arg1;
                mc.Arg2 = arg2;
 
                //create a thread
                Thread th = new Thread(new ThreadStart(mc.Process));
                threads.Add(th);
                exeClasses.Add(mc);
                
                //start the thread
                th.Start();
            }
 
            //wait for all the threads to complete
            foreach (Thread t in threads)
                t.Join();
 
            //get the compute results
            foreach (MyClass2 mc in exeClasses)
                results.Add(mc.Result);
 
            //do you work with results
        }
 
Of course, there will be thread synchronization issues whenever multiple threads are running and accessing the same resource. We will talk about the synchronization issues in another section.
1.2.3      ThreadPool
 
Using thread pool can limit creating excessive number of threads, therefore save system resources. User can set the number of threads, including the maximum number and minimum number, a thread pool can create.
 
In order to use ThreadPool to execute a method, the method must have the following one argument prototype:
 
void ComputeBoundOperation(object state);
 
The delegate type for a thread pool to execute is called WaitCallBack, which has the same prototype as the above.
 
The following code segment shows how to use ThreadPool.
  void ComputeBoundOperation(object state)
        {
            //this method is called by a thread pool thread
        }
 
        void ExecuteUseThreadPool()
        {
            for (int i; i < 10; i++)
            {
                //create the argument
                MyType argument;
 
                //execute the method in thread pool
                ThreadPool.QueueUserWorkItem(this.ComputeBoundOperation,
                                             argument);
            }
}
 
Since ComputeBoundOperation only takes objecttype as argument, a type casting is needed in the method to get your actual argument.
 
If the method ComputeBoundOperation() does not have the prototype of WaitCallBack, especially if it takes more than one arguments, then refactoring the method by defining a new type to encapsulate the method arguments is needed . If you don’t want to introduce a new type, then you can use a delegate’s BeginInvoke() method to do the asynchronous operation, see more information in [1][2].
 
 
Whenever shared resources are accessed by both reading and writing from multiple threads, synchronization must be taken care to protect the integrity of the data.
 
There are many ways to do thread synchronization; Monitor is the most used one. The key in synchronization is to make sure a shared resource is accessed in the order you like.
 
Generally speaking, if a resource is accessed for reading by multiple threads, you don’t need any synchronization, if it is accessed for writing only or both reading and writing by multiple threads, you need to make sure, when a thread is reading or writing the resource, the other threads, if trying to write to the resource, have to wait until the first one finishes its reading/writing.
 
Look at the following code segment, in ThreadSynchronizationExample class, there are two resources sharedResouce1 and sharedResouce2. Methods CacheAndGetResource1 and CacheAndGetResource2 both read and write to the resources. If at any time, there is only one thread calling those methods, then those resources are not really shared because, at any time, only one thread is reading or writing to them, therefore no synchronization is needed.
 
      class ThreadSynchronizationExample
      {
          private IList<MyType> sharedResouce1 = new List<MyType>();
          private IList<MyType> sharedResouce2 = new List<MyType>();
 
          ///<summary>
          /// find resource 1 with given key, if it is cached, return the
          /// cached one, if not, load it from data source, and cache it
          /// and return it
          ///</summary>
          ///<param name="key"></param>
          ///<returns></returns>
          public MyType CacheAndGetResource1(string key)
          {
              //find in the cache
              foreach (MyType mt in this.sharedResouce1)
                  if (mt.Key == key)
                      return mt;
 
              MyType myType = LoadFromDB(key);
              this.sharedResouce1.Add(myType);
              return myType;
          }
 
          ///<summary>
          /// find resource 2 with given key, if it is cached, return the
          /// cached one, if not, load it from data source, and cache it
          /// and return it
          ///</summary>
          ///<param name="key"></param>
          ///<returns></returns>
          public MyType CacheAndGetResource2(string key)
          {
              //find in the cache
              foreach (MyType mt in this.sharedResouce2)
                  if (mt.Key == key)
                      return mt;
 
              MyType myType = LoadFromDB(key);
              this.sharedResouce2.Add(myType);
              return myType;
          }
 
          private MyType LoadFromDB(string key)
          {
              //load data from database
              return new MyType();
          }
 
}
 
Things will be different if there are more than one threads calling those two methods. Let’s assume we have 4 threads, two are calling CacheAndGetResource1, and two are calling CacheAndGetResource2. Now resources sharedResouce1 and sharedResouce2 are accessed by multiple threads, therefore synchronized access to those resources is needed.
 
We pretty much understand that we need some locks in the two methods so that any method can be called by only one thread any time. But what object we use for the synchronization? Lots of people intend to use “ this” without much thinking. What is the problem if lock is done like this?
 
        public MyType CacheAndGetResource1(string key)
        {
            lock (this)
            {
                //find in the cache
                foreach (MyType mt in this.sharedResouce1)
                    if (mt.Key == key)
                        return mt;
 
                MyType myType = LoadFromDB(key);
                this.sharedResouce1.Add(myType);
                return myType;
            }
  }
 
        public MyType CacheAndGetResource2(string key)
        {
            lock (this)
            {
                //find in the cache
                foreach (MyType mt in this.sharedResouce2)
                    if (mt.Key == key)
                        return mt;
 
                MyType myType = LoadFromDB(key);
                this.sharedResouce2.Add(myType);
                return myType;
            }
  }
 
The problem is that if there is one thread performing operation in CacheAndGetResource1, no thread can call CacheAndGetResource2, which is not good since we never say two threads can not call those two methods at the same time; we only said no two threads should call any of those methods at the same time. Why? Because one object has only one sync block index (see [1] for more information).
 
What is the solution? Two objects should be used to achiever the synchronization, generally, if there are n shared resources, n distinctive objects are needed for synchronization.
 
Here is the right code:
 
      class ThreadSynchronizationExample
      {
        private IList<MyType> sharedResouce1 = new List<MyType>();
        private object resouces1Lock = new object();
        private IList<MyType> sharedResouce2 = new List<MyType>();
        private object resouces2Lock = new object();
 
        ///<summary>
        /// find resource 1 with given key, if it is cached, return the
        /// cached one, if not, load it from data source, and cache it
        /// and return it
        ///</summary>
        ///<param name="key"></param>
        ///<returns></returns>
        public MyType CacheAndGetResource1(string key)
        {
            lock (resouces1Lock)
            {
                //find in the cache
                foreach (MyType mt in this.sharedResouce1)
                    if (mt.Key == key)
                        return mt;
 
                MyType myType = LoadFromDB(key);
                this.sharedResouce1.Add(myType);
                return myType;
            }
        }
 
        ///<summary>
        /// find resource 2 with given key, if it is cached, return the
        /// cached one, if not, load it from data source, and cache it
       /// and return it
        ///</summary>
        ///<param name="key"></param>
        ///<returns></returns>
        public MyType CacheAndGetResource2(string key)
        {
            lock (resouces2Lock)
            {
                //find in the cache
                foreach (MyType mt in this.sharedResouce2)
                    if (mt.Key == key)
                        return mt;
 
                MyType myType = LoadFromDB(key);
                this.sharedResouce2.Add(myType);
                return myType;
            }
        }
 
        private MyType LoadFromDB(string key)
        {
            //load data from database
            return new MyType();
        }
    }
 
 
Reference:
[1] Jeffrey Richter, CLR via C#, Microsoft Press 2006
[2] Colin Peng, Asynchronous Programming Model in .NET 2.0, http://www.witstream.com/research/APM.htm, WitStream Technologies Inc, 2007
 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值