Thread和Task

并行编程很多时候也被称为多线程编程模式,这个多线程说说明了并行编程的本质定义,就是创建多个线程,去并行执行一些任务,从而达到更好的性能。 这里的多线程,一般都是指操作系统中的线程概念。一般的编程语言平台,比如C++和C#,都会有相应的创建线程,调度线程,销毁线程的方法。在C#中就用Thread来实现的。但是我这里想重点提出的,不是线程,而是C#中特有的Task。简单来说,Task就是CLR帮你在线程之上又做了一层抽象。CLR会维护一个线程池来执行Task,开发者就不需要管理线程了。因为操作系统中的线程还是很贵的(虽然比进程便宜多了),每个线程需要维护自己的栈,线程之间的切换也需要额外的CPU开销。所以除非有特殊的需求,在一般的C#程序开发中使用Task是更好的选择。

async/await

在.NET 4.5 里引入的async/await两个关键词应该是最常见的和Task相关的方法(工具)了。关于async/await的具体实现我这里就不展开了,简单来理解的话,async用来定义一个方法是awaitable的类型,而当程序执行到await的时候,当前的Task会被挂起,CLR会创建的一个新的Task去执行await的awaitable方法,并且把执行结果返回,然后会启动一个新的Task,从原来的程序await的地方继续往下执行。通过这种方式就简单的实现了多线程,简化的复杂的创建线程,控制管理等内容。

async/await 使用注意事项

避免使用async void

Async的方法从语法上来说,可以定义的返回值有三种:Task, Task 和void。从上面的简单解释可以看出来,返回值是Task的方法才能和整个Task的框架更好的兼容,比如异常捕捉等。之所以还有void的返回值,主要是为了兼容事件句柄(event handler)。事件句柄要求返回值必须是void.这种事件句柄一般是在UI上定义的,这种情况下建议再包一层Task返回值的方法,例如:

private async void button1_Click(object sender, EventArgs e)
{
    await Button1ClickAsync();
}
public async Task Button1ClickAsync()
{
    // Do asynchronous work.
    await Task.Delay(1000);
}

要使用async的话,就要全部都使用async. (async all the way)

这个是在实际中经常出现的问题。从上面的解释中可以理解到,由于async的方法是交由CLR去调度执行在具体的线程上的,所以你不知道哪些Task可能会被调度在同一个线程上,如果你用了同步的方法锁住了线程,就有可能造成死锁。原理明白后就是要注意具体有哪些方法其实是同步的方法了,最常见的是 .Result.Wait()。下表总结了一些常见的错误:

To Do This … Instead of This … Use This
Retrieve the result of a background task Task.Wait or Task.Result await
Wait for any task to complete Task.WaitAny await Task.WhenAny
Retrieve the results of multiple tasks Task.WaitAll await Task.WhenAll
Wait a period of time Thread.Sleep await Task.Delay

当然全部使用async中还是有一些例外的,例外就是在Main函数中。Console Application中的Main函数是必须要使用同步方法的。建议是只在最外层使用同步方法,而在其他地方都用async。例如:

class Program
{
    static void Main()
    {
        MainAsync().Wait();
    }
    static async Task MainAsync()
    {
        try
        {
            // Asynchronous implementation.
            await Task.Delay(1000);
        }
        catch (Exception ex)
        {
            // Handle exceptions.
        }
    }
}

总结

简单总结一下,.NET提供了一种很棒的多线程编程的抽象:Task。从原理上说,就是CLR会维护一个线程池,调度程序定义的Task到线程上进行执行。async/await是.NET4.5后提供的关键词,可以很方便的定义和使用Task,但是需要注意使用async的话要一路都全部使用async,不能混合同步方法和异步方法,否则会导致死锁。 关于c#的并行编程还有很多可以深入的地方,这里只是谈了下我自己的一些浅显的认识,希望能有所帮助。如果有错误的地方,还请不吝赐教。

参考资料

  1. http://blog.slaks.net/2013-10-11/threads-vs-tasks/
  2. https://msdn.microsoft.com/en-us/magazine/jj991977.aspx