浅析C#中的Thread ThreadPool Task和async/await

 

.net 项目中不可避免地要与线程打交道,目的都是实现异步、并发。从最开始的new Thread()入门,到后来的Task.Run(),如今在使用async/await的时候却有很多疑问。

先来看一段代码:使用Task实现异步

                    Task.Run(() =>
                    {
                        message =  (IBytesMessage)consumer.Receive(m_Interval);
                    }); 

Receive()方法是一个延迟返回的方法,m_Interval是超时时间。如果采用同步方式执行Receive()的话,那整个程序就会被这个方法堵塞。我个人最习惯的处理方式就用Task.Run()。可惜项目要求必须使用.net framework3.5,所以只能退而求其次,放弃Task,使用Thread或者ThreadPool。

使用Thread实现异步:

  new Thread(() =>
                    {
                        message =  (IBytesMessage)consumer.Receive(m_Interval);
                    }).Start(); 

直接new Thread().start()这个写法是很危险的,这里只做参考。在C# 5以后的书籍中,你可能会看到这样一句话:一旦你输入了new Thread(),那就糟糕了,说明项目的代码太过时了。

*2022/1/15更新、Thread类目前仍有必需的场景。例如:当你需要使用STA线程时、必须通过new Thread() 然后SetApartmentMode为STA。ThreadPool、Task内部的线程都是MTA模式。

使用ThreadPool实现异步:

  
ThreadPool.QueueUserWorkItem(Listen);  
    
private void Listen(object state) { message = (IBytesMessage)consumer.Receive(m_Interval); }

ThreadPool 内部有一套完整的线程管理机制,可以让开发者完全忽略Thread的生命周期控制。但ThreadPool中的线程,都是后台线程,当主线程执行完毕时,程序并不会等待后台线程的执行,而是直接退出。Thread则是前台线程,主程序会等待所有前台线程执行完毕后才会退出。当然可以通过设置Thread的IsBackground属性来修改线程为前台或者后台。

另外在使用ThreadPool的时候需要注意QueueUserWorkItem的参数类型是:

public delegate void WaitCallback(object state) 所以,Listen方法有一个未用到的参数state。

综上,Task还是最优的解决方案

说到这,问题看似解决了,.net 4.0及以上 Task是不二之选,低版本择优先选择ThreadPool,特殊情况(要求STA模式、比如在线程中显示一个wpf/winform 窗口)考虑Thread。那么 .net4.5的新特性 async/await 有什么用呢?上述情况需要用到async/await 吗?

这里我们需要看一下完整的代码

 private void Listen(object state)
        {
            message = (IBytesMessage)consumer.Receive(m_Interval);
            if (message != null)
            {
                m_IAsyncMesssgae.OutputMessage(message.ToString());
            }
            else
            {
                m_IAsyncMesssgae.OutputException(new Exception("Wait timed out."));
            }
        }
        public void OnStartAsync()
        {
            try
            {
                if (m_IsConnected && !m_IsListening)
                {
                    connectionWPM?.Start();
                    m_IsListening = true;
                   ThreadPool.QueueUserWorkItem(Listen);                   
                }
            }
            catch (Exception ex)
            {
                m_IAsyncMesssgae.OutputException(ex);
            }
            finally
            {
                OnStop();
            }
        }

这里红色字体的m_IAsyncMesssgae是一个回调的接口实例,也就说,此代码中,通过接口回调的方式把Receive()方法延迟返回的message返回给调用者。目前的代码是可以满足需求的。

我们试着用async和await实现一下这个需求。 

 public async void OnStartAsync()
        {
                if (m_IsConnected && !m_IsListening)
                {
                    connectionWPM?.Start();
                    m_IsListening = true;
                    message = await Task.Run(()=> {return (IBytesMessage)consumer.Receive(m_Interval); });                  
                }
        }

1)async/await 和刚才说的Thread Task ThreadPool并不是一个概念。前者是控制异步和并发的关键字,后者是对线程的三种实现方式。

2)async/await只能和Task结合使用,async标记的方法 只能有三种返回值Task,Task<T>,void(不建议,因为async/await 就是为了获取异步方法的返回值)。

3)await等待的内容也必须是Task或者Task<T> 上面代码隐藏了一个内容,其实Task.Run()也是一个返回值为Task<T>的方法。

4)await还有一个作用是将Task<T>转成T。

5)在同一个用async标记的方法内,所有在await代码段之后的代码 都要等待await后的内容执行完成后才能执行。

6)如果一个非async方法 调用async方法获取异步返回值,那么就无法成功获取异步返回值。

 

再把返回值void修改一下:

  public async Task<IBytesMessage> OnStartAsync()
        {
            if (m_IsConnected && !m_IsListening)
            {
                connectionWPM?.Start();
                m_IsListening = true;
                message = await Task.Run(()=> {return (IBytesMessage)consumer.Receive(m_Interval); });                  
            }
            return message;
        }

这样一来,外部调用时候,就不需要接口回调了,直接调用OnStartAsync就可以了。切记!调用OnStartAsync的方法必须也是async,否则就直接返回message的默认值,而不是等待TaskRun()的执行。await只在所属的async方法内奏效。

调用OnStartAsync也有种不同的写法:

 

//写法1
async  Task  Handle()
{
    string re = await OnStartAsync();
    //dosth
}
//写法2
async Task Handle()
{
    var re = OnStartAsync();
    //dosth
    do(await re);
}
//写法3
void Handle() { var re = OnStartAsync(); do(re); }
 

 

 写法1:dosth需要等待 OnStartAsync执行完毕后再执行。

 写法2:dosth先执行,然后再执行do(await re)

 写法3:根本就无法获取正确的返回值,实则没有等待异步执行,而是直接返回了。

 

以上,水平有限,如有不足,敬请指正。如有侵权 请联系作者删除。

posted on 2019-03-20 16:16  洞春香酒肆  阅读(3140)  评论(1编辑  收藏  举报

导航