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

简介: 谈多线程谈到现在,我们要明确多线程的一个好处是可以进行并行的运算(充分利用多核处理器,对于桌面应用程序来说就更重要一点了,没有WEB服务器,利用多核只能靠自己),还有一个好处就是异步操作,就是我们可以让某个长时间的操作独立运行,不妨碍主线程继续进行一些计算,然后异步的去返回结果(也可以不返回)。

谈多线程谈到现在,我们要明确多线程的一个好处是可以进行并行的运算(充分利用多核处理器,对于桌面应用程序来说就更重要一点了,没有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恐怕无能为力的。下节继续讨论基于事件的异步编程模式。

作者: lovecindywang
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关文章
|
4月前
|
数据库 开发者
.NET 异步编程之谜:async/await 模式究竟隐藏着怎样的神奇力量?
【8月更文挑战第28天】在当今注重效率和响应性的软件开发领域,.NET 的 async/await 模式如同得力助手,简化异步代码编写,使代码更易理解和维护。通过后台执行耗时操作,如网络请求和数据库查询,避免阻塞主线程,显著提升系统响应性。此模式不仅适用于网络请求,还广泛应用于数据库操作和文件读写。合理使用 async/await 可大幅优化性能,但需注意避免过度使用、正确处理调用链及异常,以确保系统稳定性和高效性。深入探索 async/await,助您构建更出色的应用程序。
59 0
|
1月前
|
开发框架 Java .NET
.net core 非阻塞的异步编程 及 线程调度过程
【11月更文挑战第12天】本文介绍了.NET Core中的非阻塞异步编程,包括其基本概念、实现方式及应用示例。通过`async`和`await`关键字,程序可在等待I/O操作时保持线程不被阻塞,提高性能。文章还详细说明了异步方法的基础示例、线程调度过程、延续任务机制、同步上下文的作用以及如何使用`Task.WhenAll`和`Task.WhenAny`处理多个异步任务的并发执行。
|
4月前
|
人工智能 文字识别
通义语音AI技术问题之LCB-net模型对幻灯片中文本信息的使用如何解决
通义语音AI技术问题之LCB-net模型对幻灯片中文本信息的使用如何解决
27 0
|
6月前
|
机器学习/深度学习 JSON 测试技术
CNN依旧能战:nnU-Net团队新研究揭示医学图像分割的验证误区,设定先进的验证标准与基线模型
在3D医学图像分割领域,尽管出现了多种新架构和方法,但大多未能超越2018年nnU-Net基准。研究发现,许多新方法的优越性未经严格验证,揭示了验证方法的不严谨性。作者通过系统基准测试评估了CNN、Transformer和Mamba等方法,强调了配置和硬件资源的重要性,并更新了nnU-Net基线以适应不同条件。论文呼吁加强科学验证,以确保真实性能提升。通过nnU-Net的变体和新方法的比较,显示经典CNN方法在某些情况下仍优于理论上的先进方法。研究提供了新的标准化基线模型,以促进更严谨的性能评估。
177 0
|
6月前
|
开发框架 监控 Java
【.NET Core】多线程之线程池(ThreadPool)详解(二)
【.NET Core】多线程之线程池(ThreadPool)详解(二)
106 3
|
6月前
|
SQL 设计模式 开发框架
.NET异步有多少种实现方式?(异步编程提高系统性能、改善用户体验)
想要知道.NET异步有多少种实现方式,首先我们要知道.NET提供的执行异步操作的三种模式,然后再去了解.NET异步实现的四种方式。
|
6月前
|
开发框架 .NET 对象存储
【.NET Core】深入理解异步编程模型(APM)
【.NET Core】深入理解异步编程模型(APM)
138 1
|
6月前
|
SQL 开发框架 .NET
(20)ASP.NET Core EF创建模型(必需属性和可选属性、最大长度、并发标记、阴影属性)
(20)ASP.NET Core EF创建模型(必需属性和可选属性、最大长度、并发标记、阴影属性)
|
6月前
|
SQL 开发框架 安全
【.NET Core】深入理解任务并行库 (TPL)
【.NET Core】深入理解任务并行库 (TPL)
82 0
|
3月前
|
开发框架 前端开发 JavaScript
ASP.NET MVC 教程
ASP.NET 是一个使用 HTML、CSS、JavaScript 和服务器脚本创建网页和网站的开发框架。
51 7