关于 C#异步方法的使用

委托和方法的异步调用

本人对原作进行了简单的修改,大部分都与原文相同

也许业内很多高不成低不就的程序员都会对一些知识点会有些迷惑,原因是平常工作用的少,所以也就决定了你对这个事物的了解程度。今天就来看看C#中异步方法的使用。希望对大家有所帮助。

--原文

通常情况下,如果需要异步执行一个耗时的操作,我们会新起一个线程,然后让这个线程去执行代码。但是对于每一个异步调用都通过创建线程来进行操作显 然会对性能产生一定的影响,同时操作也相对繁琐一些。.Net中可以通过委托进行方法的异步调用,就是说客户端在异步调用方法时,本身并不会因为方法的调 用而中断,而是从线程池中抓取一个线程去执行该方法,自身线程(主线程)在完成抓取线程这一过程之后,继续执行下面的代码,这样就实现了代码的并行执行。 使用线程池的好处就是避免了频繁进行异步调用时创建、销毁线程的开销。

如同上面所示,当我们在委托对象上调用BeginInvoke()时,便进行了一个异步的方法调用。而在这种情况下使用异步编程时,就需要进行更多的控制,比如当异步执行方法的方法结束时通知客户端、返回异步执行方法的返回值等。本节就对BeginInvoke()方法、EndInvoke()方法和其相关的IAysncResult做一个简单的介绍。

NOTE:讨论在客户端程序中异步地调用方法。

我们看这样一段代码,它演示了不使用异步调用的通常情况:

 1 class Program7 {
 2     static void Main(string[] args) {
 3 
 4         Console.WriteLine("Client application started! ");
 5         Thread.CurrentThread.Name = "Main Thread";
 6 
 7         Calculator cal = new Calculator();
 8         int result = cal.Add(2, 5);
 9         Console.WriteLine("Result: {0} ", result);
10        
11         // 做某些其它的事情,模拟需要执行3秒钟
12         for (int i = 1; i <= 3; i++) {
13             Thread.Sleep(TimeSpan.FromSeconds(1));
14             Console.WriteLine("{0}: Client executed {1} second(s).",
15                 Thread.CurrentThread.Name, i); 
16         }
17 
18         Console.WriteLine(" Press any key to exit...");
19         Console.ReadKey();
20     }
21 }
22 
23 public class Calculator {
24     public int Add(int x, int y) {
25         if (Thread.CurrentThread.IsThreadPoolThread) {
26             Thread.CurrentThread.Name = "Pool Thread";
27         }
28         Console.WriteLine("Method invoked!");          
29 
30         // 执行某些事情,模拟需要执行2秒钟
31         for (int i = 1; i <= 2; i++) {
32             Thread.Sleep(TimeSpan.FromSeconds(1));
33             Console.WriteLine("{0}: Add executed {1} second(s).",
34                 Thread.CurrentThread.Name, i); 
35         }
36         Console.WriteLine("Method complete!");
37         return x + y;
38     }
39 } 

上面代码有几个关于对于线程的操作,如果不了解可以看一下下面的说明,如果你已经了解可以直接跳过:

  • Thread.Sleep(), 它会让执行当前代码的线程暂停一段时间(如果你对线程的概念比较陌生,可以理解为使程序的执行暂停一段时间),以毫秒为单位,比如 Thread.Sleep(1000),将会使线程暂停1秒钟。在上面我使用了它的重载方法,个人觉得使用 TimeSpan.FromSeconds(1),可读性更好一些。
  • Thread.CurrentThread.Name,通过这个属性可以设置、获取执行当前代码的线程的名称,值得注意的是这个属性只可以设置一次,如果设置两次,会抛出异常。
  • Thread.IsThreadPoolThread,可以判断执行当前代码的线程是否为线程池中的线程。

通 过这几个方法和属性,有助于我们更好地调试异步调用方法。上面代码中除了加入了一些对线程的操作以外再没有什么特别之处。我们建了一个 Calculator类,它只有一个Add方法,我们模拟了这个方法需要执行2秒钟时间,并且每隔一秒进行一次输出。而在客户端程序中,我们使用 result变量保存了方法的返回值并进行了打印。随后,我们再次模拟了客户端程序接下来的操作需要执行2秒钟时间。运行这段程序,会产生下面的输出:

Client application started!

Method invoked!
Main Thread: Add executed 1 second(s).
Main Thread: Add executed 2 second(s).
Method complete!
Result: 7

Main Thread: Client executed 1 second(s).
Main Thread: Client executed 2 second(s).
Main Thread: Client executed 3 second(s).

Press any key to exit...

如果你确实执行了这段代码,会看到这些输出并不是一瞬间输出的,而是执行了大概5秒钟的时间,因为线程是串行执行的,所以在执行完Add()方法之后才会继续客户端剩下的代码。

接下来我们定义一个AddDelegate委托,并使用BeginInvoke()方法来异步地调用它。在上面已经介绍过,BeginInvoke()除了 最后两个参数为AsyncCallback类型和Object类型以外,前面的参数类型和个数与委托的方法定义相同。另外BeginInvoke()方法返回了一个实现了IAsyncResult接口的对象(实际上就是一个AsyncResult(System.Runtime.Remoting.Messaging命名空间里)类型实例,注意这里IAsyncResult和 AysncResult是不同的)。

AsyncResult的用途有这么几个:传递参数,它 包含了对调用了BeginInvoke()的委托的引用;它还包含了BeginInvoke()的最后一个Object类型的参数;它可以鉴别出是哪个方 法的哪一次调用,因为通过同一个委托变量可以对同一个方法调用多次。

EndInvoke()方法接受IAsyncResult类型的对象 (以及ref和out类型参数,这里不讨论了,对它们的处理和返回值类似),所以在调用BeginInvoke()之后,我们需要保留 IAsyncResult,以便在调用EndInvoke()时进行传递。这里最重要的就是EndInvoke()方法的返回值,它就是方法的返回值。除 此以外,当客户端调用EndInvoke()时,如果异步调用的方法没有执行完毕,则会中断当前线程而去等待该方法,只有当异步方法执行完毕后才会继续执 行后面的代码。所以在调用完BeginInvoke()后立即执行EndInvoke()是没有任何意义的。我们通常在尽可能早的时候调用 BeginInvoke(),然后在需要方法的返回值的时候再去调用EndInvoke(),或者是根据情况在晚些时候调用。说了这么多,我们现在看一下 使用异步调用改写后上面的代码吧:

 1 public delegate int AddDelegate(int x, int y);
 2 
 3 class Program8 {   
 4 
 5     static void Main(string[] args) {
 6 
 7         Console.WriteLine("Client application started! ");
 8         Thread.CurrentThread.Name = "Main Thread";
 9                    
10         Calculator cal = new Calculator();
11         AddDelegate del = new AddDelegate(cal.Add);
12         IAsyncResult asyncResult = del.BeginInvoke(2,5,null,null);  // 异步调用方法
13 
14         // 做某些其它的事情,模拟需要执行3秒钟
15         for (int i = 1; i <= 3; i++) {
16             Thread.Sleep(TimeSpan.FromSeconds(i));
17             Console.WriteLine("{0}: Client executed {1} second(s).",
18                 Thread.CurrentThread.Name, i);
19         }
20 
21         int rtn = del.EndInvoke(asyncResult);
22         Console.WriteLine("Result: {0} ", rtn);
23 
24         Console.WriteLine(" Press any key to exit...");
25         Console.ReadKey();
26     }
27 }
28 
29 public class Calculator { /* 与上面同,略 */}

此时的输出为:

Client application started!

Method invoked!
Main Thread: Client executed 1 second(s).
Pool Thread: Add executed 1 second(s).
Main Thread: Client executed 2 second(s).
Pool Thread: Add executed 2 second(s).
Method complete!
Main Thread: Client executed 3 second(s).
Result: 7


Press any key to exit...

现在执行完这段代码只需要3秒钟时间,两个for循环所产生的输出交替进行,这也说明了这两段代码并行执行的情况。可以看到Add()方法是由线程 池中的线程在执行,因为Thread.CurrentThread.IsThreadPoolThread返回了True,同时我们对该线程命名为了 Pool Thread。另外我们可以看到通过EndInvoke()方法得到了返回值。

有时候,我们可能会将获得返回值的操作放到另一段 代码或者客户端去执行,而不是向上面那样直接写在BeginInvoke()的后面。比如说我们在Program中新建一个方法GetReturn(), 此时可以通过AsyncResult的AsyncDelegate获得del委托对象,然后再在其上调用EndInvoke()方法,这也说明了 AsyncResult可以唯一的获取到与它相关的调用了的方法(或者也可以理解成委托对象)。所以上面获取返回值的代码也可以改写成这样:

static int GetReturn(IAsyncResult asyncResult) {
    AsyncResult result = (AsyncResult)asyncResult;
    AddDelegate del = (AddDelegate)result.AsyncDelegate;
    int rtn = del.EndInvoke(asyncResult);
    return rtn;
}

然后再将int rtn = del.EndInvoke(asyncResult);语句改为int rtn = GetReturn(asyncResult);。注意上面IAsyncResult要转换为实际的类型AsyncResult才能访问 AsyncDelegate属性,因为它没有包含在IAsyncResult接口的定义中。

BeginInvoke的另外两个参数分别是AsyncCallback和Object类型,其中AsyncCallback是一个委托类型,它用于方法的回调,即是说当异步方法执行完毕时自动进行调用的方法。它的定义为:

public delegate void AsyncCallback(IAsyncResult ar);

Object类型用于传递任何你想要的数值,它可以通过IAsyncResult的AsyncState属性获得。下面我们将获取方法返回值、打印返回值的操作放到了OnAddComplete()回调方法中:

public delegate int AddDelegate(int x, int y);

class Program9 {

    static void Main(string[] args) {

        Console.WriteLine("Client application started! ");
        Thread.CurrentThread.Name = "Main Thread";

        Calculator cal = new Calculator();
        AddDelegate del = new AddDelegate(cal.Add);
        string data = "Any data you want to pass.";
        AsyncCallback callBack = new AsyncCallback(OnAddComplete);
        del.BeginInvoke(2, 5, callBack, data);      // 异步调用方法

        // 做某些其它的事情,模拟需要执行3秒钟
        for (int i = 1; i <= 3; i++) {
            Thread.Sleep(TimeSpan.FromSeconds(i));
            Console.WriteLine("{0}: Client executed {1} second(s).",
                Thread.CurrentThread.Name, i);
        }

        Console.WriteLine(" Press any key to exit...");
        Console.ReadKey();
    }

    static void OnAddComplete(IAsyncResult asyncResult) {
        AsyncResult result = (AsyncResult)asyncResult;
        AddDelegate del = (AddDelegate)result.AsyncDelegate;
        string data = (string)asyncResult.AsyncState;

        int rtn = del.EndInvoke(asyncResult);
        Console.WriteLine("{0}: Result, {1}; Data: {2} ",
            Thread.CurrentThread.Name, rtn, data);
    }
}
public class Calculator { /* 与上面同,略 */}

产生的输出为:

Client application started!

Method invoked!
Main Thread: Client executed 1 second(s).
Pool Thread: Add executed 1 second(s).
Main Thread: Client executed 2 second(s).
Pool Thread: Add executed 2 second(s).
Method complete!
Pool Thread: Result, 7; Data: Any data you want to pass.

Main Thread: Client executed 3 second(s).

Press any key to exit...

这里有几个值得注意的地方:1、我们在调用BeginInvoke()后不再需要保存IAysncResult了,因为AysncCallback委托将该对象定义在了回调方法的参数列表中;2、我们在OnAddComplete()方法中获得了调用BeginInvoke()时最后一个参数传递的值,字 符串“Any data you want to pass”;3、执行回调方法的线程并非客户端线程Main Thread,而是来自线程池中的线程Pool Thread。另外如前面所说,在调用EndInvoke()时有可能会抛出异常,所以在应该将它放到try/catch块中,这里我就不再示范了。

结语:这篇文章简单的描述了异步委托方法的使用和说明,本文是从原作者内容中摘抄而来,做了很少的改动,望见谅。

感谢阅读,希望对你能有所帮助。

原文地址:http://www.cnblogs.com/JimmyZhang/archive/2008/08/22/1274342.html