浅谈.NET下的多线程和并行计算(十).NET异步编程模型基础上

谈多线程谈到现在,我们要明确多线程的一个好处是可以进行并行的运算(充分利用多核处理器,对于桌面应用程序来说就更重要一点了,没有WEB服务器,利用多核只能靠自己),还有一个好处就是异步操作,就是我们可以让某个长时间的操作独立运行,不妨碍主线程继续进行一些计算,然后异步的去返回结果(也可以不返回)。前者能提高性能是因为能利用到多核,而后者能提高性能是因为能让CPU不在等待中白白浪费,其实异步从广义上来说也可以理解为某种并行的运算。在之前的这么多例子中,我们大多采用手工方式来新开线程,之前也说过了,在大并发的环境中随便开始和结束线程的代价太大,需要利用线程池,使用线程池的话又觉得少了一些控制。现在让我们来总结一下大概会有哪几种常见的异步编程应用模式:

1) 新开一个A线程执行一个任务,然后主线程执行另一个任务后等待线程返回结果后继续

2) 新开一个A线程执行一个任务,然后主线程不断轮询A线程是否执行完毕,如果没有的话可以选择等待或是再进行一些操作

3) 新开一个A线程执行一个任务,执行完毕之后立即执行一个回调方法去更新一些状态变量,主线程和A线程不一定有直接交互

4) 新开一个A线程执行一个任务,执行完毕之后啥都不做

(补充一句,异步编程不一定是依赖于线程的,从广义上来说,使用队列异步处理数据也可以算是一种异步编程模式)

对于这任何一种,我们要使用线程池来编写应用的话都是比较麻烦的,比如如下的代码实现了1)这种应用:

class AsyncObj
{
    public EventWaitHandle AsyncWaitHandle { get; set; }
    public object Result { get; set; }

    public AsyncObj()
    {
        AsyncWaitHandle = new AutoResetEvent(false);
    }
}
AsyncObj ao = new AsyncObj();
ThreadPool.QueueUserWorkItem(state =>
{
    AsyncObj obj = state as AsyncObj;
    Console.WriteLine("asyc operation started @ " + DateTime.Now.ToString("mm:ss"));
    Thread.Sleep(2000);
    Console.WriteLine("asyc operation completed @ " + DateTime.Now.ToString("mm:ss"));
    obj.Result = 100;
    obj.AsyncWaitHandle.Set();
}, ao);

Console.WriteLine("main operation started @ " + DateTime.Now.ToString("mm:ss"));
Thread.Sleep(1000);
Console.WriteLine("main operation completed @ " + DateTime.Now.ToString("mm:ss"));
ao.AsyncWaitHandle.WaitOne();
Console.WriteLine("get syc operation result : " + ao.Result.ToString() + " @ " + DateTime.Now.ToString("mm:ss"));

结果如下:

image

对于2)-4)等情况又是另外一套了,这样我们的代码可能会变得乱七八糟,在.NET中我们的委托以及很多IO操作相关的类库都支持一种叫做异步编程模型APM的编程模型。不仅仅方便了我们进行多线程应用,而且我们如果自己要设计类库的话也可以遵从这个模式(基于APM的接口实现我们自己的类库)。.NET提供了基于IAsyncResult的异步编程模型和基于事件的异步编程模型,这节我们来看看基于IAsyncResult也就是BeginInvoke和EndInvoke(对于非同用的操作来说就是BeginXXX和EndXXX)的编程模型的各种使用方法,可以说这么多种使用方法可以满足我们大部分的要求。

首先来定义一个异步操作:

static int AsyncOperation(int x, int y)
{
    Console.WriteLine("asyc operation started @ " + DateTime.Now.ToString("mm:ss"));
    Thread.Sleep(2000);
    int a, b;
    ThreadPool.GetAvailableThreads(out a, out b);
    Console.WriteLine(string.Format("({0}/{1}) #{2}", a, b, Thread.CurrentThread.ManagedThreadId));
    Console.WriteLine("asyc operation completed @ " + DateTime.Now.ToString("mm:ss"));
    return x + y;
}

我们需要开两个线程同时计算两个异步操作,然后主线程等待两个线程执行完毕后获取结果并且输出它们的和,难以想象代码是多么简单:

var func = new Func<int, int, int>(AsyncOperation);
var result1 = func.BeginInvoke(100, 200, null, null);
var result2 = func.BeginInvoke(300, 400, null, null);
Console.WriteLine("main operation started @ " + DateTime.Now.ToString("mm:ss"));
Thread.Sleep(1000);
Console.WriteLine("main operation completed @ " + DateTime.Now.ToString("mm:ss"));
int result = func.EndInvoke(result1) + func.EndInvoke(result2);
Console.WriteLine("get syc operation result : " + result + " @ " + DateTime.Now.ToString("mm:ss"));

主线程的计算需要1秒,两个异步线程都需要2秒,整个程序理论上需要2秒执行完毕,看看结果如何:

image

当然,在之前我们限制了线程池的线程数为2-4:

ThreadPool.SetMinThreads(2, 2);
ThreadPool.SetMaxThreads(4, 4);

从结果中可以看出,使用委托来异步调用方法基于线程池,调用EndInvoke的时候阻塞了主线程,得到结果后主线程继续。在代码中没看到Thread没看到ThreadPool没看到信号量,我们却完成了一个异步操作,实现了一开始说的1)场景。现在再来看看第二种使用方式:

var func = new Func<string, int, string>(AsyncOperation2);
var result1 = func.BeginInvoke("hello ", 2000, null, null);
var result2 = func.BeginInvoke("world ", 3000, null, null);
Console.WriteLine("main operation started @ " + DateTime.Now.ToString("mm:ss"));
Thread.Sleep(1000);
Console.WriteLine("main operation completed @ " + DateTime.Now.ToString("mm:ss"));
WaitHandle.WaitAny(new WaitHandle[] { result1.AsyncWaitHandle, result2.AsyncWaitHandle });
string r1 = result1.IsCompleted ? func.EndInvoke(result1) : string.Empty;
string r2 = result2.IsCompleted ? func.EndInvoke(result2) : string.Empty;
if (string.IsNullOrEmpty(r1))
{
    Console.WriteLine("get syc operation result : " + r2 + " @ " + DateTime.Now.ToString("mm:ss"));
    func.EndInvoke(result1);
}
if (string.IsNullOrEmpty(r2))
{
    Console.WriteLine("get syc operation result : " + r1 + " @ " + DateTime.Now.ToString("mm:ss"));
    func.EndInvoke(result2);
}

BeginInvoke返回的是一个IAsyncResult,通过其AsyncWaitHandle 属性来获取WaitHandle。异步调用完成时会发出信号量。这样我们就可以更灵活一些了,可以在需要的时候去WaitOne()(可以设置超时时间),也可以WaitAny()或是WaitAll(),上例我们实现的效果是开了2个线程一个3秒,一个2秒,只要有任何一个完成就获取其结果,主线程任务完成之后再去EndInvoke没完成的那个来释放资源(比如有两个排序算法,它们哪个快取决于数据源,我们一起执行并且只要有一个得到结果就继续)。在这里我们的工作方法AsyncOperation2的定义如下:

static string AsyncOperation2(string s, int time)
{
    Console.WriteLine("asyc operation started @ " + DateTime.Now.ToString("mm:ss:fff"));
    Thread.Sleep(time);
    int a, b;
    ThreadPool.GetAvailableThreads(out a, out b);
    Console.WriteLine(string.Format("({0}/{1}) #{2}", a, b, Thread.CurrentThread.ManagedThreadId));
    Console.WriteLine("asyc operation completed @ " + DateTime.Now.ToString("mm:ss:fff"));
    return s.ToUpper();
}

这段程序运行结果如下:

image

可以看到,在2秒的那个线程结束后,主线程就继续了,然后再是3秒的那个线程结束。再来看看第三种,也就是使用轮询的方式来等待结果:

var func = new Func<int, int, int>(AsyncOperation);
var result = func.BeginInvoke(100, 200, null, null);
Console.WriteLine("main operation started @ " + DateTime.Now.ToString("mm:ss"));
Thread.Sleep(1000);
Console.WriteLine("main operation completed @ " + DateTime.Now.ToString("mm:ss"));
while (!result.IsCompleted)
{
    Console.WriteLine("main thread wait again");
    Thread.Sleep(500);
}
int r = func.EndInvoke(result);
Console.WriteLine("get syc operation result : " + r + " @ " + DateTime.Now.ToString("mm:ss"));

程序的输出结果如下,这对应我们一开始提到的第二种场景,在等待的时候我们的主线程还可以做一些(不依赖于返回结果的)计算呢:

image

再来看看第四种,采用回调的方式来获取结果,线程在结束后自动调用回调方法,我们可以在回调方法中进行EndInvoke:

var func = new Func<int, int, int>(AsyncOperation);
var result = func.BeginInvoke(100, 200, CallbackMethod, func);
Console.WriteLine("main operation started @ " + DateTime.Now.ToString("mm:ss"));
Thread.Sleep(1000);
Console.WriteLine("main operation completed @ " + DateTime.Now.ToString("mm:ss"));
Console.ReadLine();

BeginInvoke的第三个参数是回调方法,第四个参数是传给工作方法的状态变量,这里我们把工作方法的委托传给它,这样我们可以在回调方法中获取到这个委托:

static void CallbackMethod(IAsyncResult ar)
{
    Console.WriteLine(string.Format("CallbackMethod runs on #{0}", Thread.CurrentThread.ManagedThreadId));
    var caller = (Func<int, int, int>)ar.AsyncState;
    int r = caller.EndInvoke(ar);
    Console.WriteLine("get syc operation result : " + r + " @ " + DateTime.Now.ToString("mm:ss"));
}

程序的输出结果如下:

image

可以看到,主线程并没有因为工作线程而阻塞,它没有等待它的结果,异步方法结束后自动调用回调方法(运行于新线程),在回调方法中我们把状态变量进行类型转换后得到方法委托,然后通过这个委托来调用EndInvoke获得结果。这里符合我们第3)种应用,这种情况下主线程不一定需要和异步方法进行直接的交互(也就无需等待),当然主线程也完全可以再结合使用轮询或等待信号量等待异步线程完成后从共享变量(需要回调方法把结果写入共享变量)来获取结果。

至于一开始说的第4)种应用需要注意,我们完全可以直接采用线程池来做,如果采用异步编程模型的话,即使不需要得到结果也别忘记调用EndInvoke来释放资源,这是一个好习惯,因为.NET中很多涉及到IO和网络操作的类库都采用了APM方式,对于这些应用如果我们不调用EndInvoke来释放非托管资源的话,GC恐怕无能为力的。下节继续讨论基于事件的异步编程模式。

posted @ 2010-01-08 22:04  lovecindywang  阅读(4156)  评论(4编辑  收藏  举报