NET多线程和异步总结(二)

承接上文。web

线程池

线程池主要有两个好处:编程

  1. 避免线程建立和销毁的开销。
  2. 自动缩放:能够按需增减线程的数量。

总之,Windows系统自带了线程池的功能,一般状况下,你不可能有更好的实现。因此只需了解如何使用。安全

Windows的线程池有两种,分别是非托管线程池和托管线程池(即.NET线程池)。下面分别来介绍。多线程

非托管线程池

  • 每一个进程都有一个线程池,线程池有个IOCP。
  • 其中的线程分为IO线程和工做者线程(或非IO线程)。架构

    • 其中工做者线程监听线程池的IOCP。
    • IO线程专门执行APC的异步完成例程。在空闲时一直是可唤醒状态。
  • 调用Windows API QueueUserWorkItem会让一个监听在IOCP上的工做者线程醒来,并执行例程。
  • 调用BindIOCompletionCallback把一个文件句柄绑定到线程池的IOCP上。当此文件有关的IO操做完成时,一个工做者线程会被唤醒来执行后面的操做。
  • 调用QueueUserWorkItem并传入WT_EXECUTEINPERSISTENTTHREAD标识时,会将一个APC回调放入IO线程的APC队列中。

托管线程池

  • 每一个CLR都有一个线程池,线程池有个IOCP。(通常来讲一个进程有一个CLR,也可能有多个)
  • 其中的线程分为工做者线程和IO线程。app

    • 工做者线程从任务队列中取得任务并执行(不经过IOCP)。
    • IO线程则监听IOCP,在唤醒时执行任务的ContinueWith集合中的任务。
  • 能够调用Task.Run()ThreadPool.QueueUserWorkItem()来添加任务到任务队列中。
  • 使用ThreadPool.UnsafeQueueNativeOverlapped()能够将任务添加到IO线程中。但不多使用。

可见,托管与非托管线程池的差距是巨大的。异步

.NET多线程及异步编程模型

  1. .NET 3.5及以前
    只有Thread, ThreadPool等接口。直接操做线程。
  2. .NET 4
    引入了Task
  3. .NET 4.5.1 (C# 5.0)
    引入了TAP(基于Task的异步模式),引入了 async/await

下面来着重分析Task编程模式。async

Task初级

Task最重要的两个方法是Task.Run()new Task().Start()。使用这两个方法:异步编程

  • 调用者传递一个委托给Task。
  • 线程池的一个线程会执行此委托。
  • 返回值将保存在Task.Result中。
  • 调用Task.Wait()Task.Result会阻塞地等待工做线程执行结束。

如下图示演示了这个过程具体经历了什么。函数

clipboard.png

在任务完成时继续执行其余Task

调用Task.ContinueWith(continueAction, continueOptions)。其中,ContinuationOptions是一个枚举,提供了更多选项。

clipboard.png

这个过程如何理解呢?请看下图。

clipboard.png

Task里面有什么

如下是一些主要的属性:

  • Id
  • State
  • Reference of parent task
  • Reference of Task scheduler
  • Delegate
  • AsyncState (to pass method’s objects)
  • ExecutionContext
  • CancellationToken
  • Collection of ContinueWithTask

async/await的机制

它其实是Task.ContinueWith()的语法糖。

为此咱们来看一段程序:

private async void btnDoStuff_Click()
{
    lblStatus.Content = "Doing Stuff";

    await Task.Delay(4000);

    lblStatus.Content = "After await";
}

它至关于

private void btnDoStuff_Click()
{
    lblStatus.Content = "Doing Stuff";

    Task t = Task.Delay(4000);

    t.ContinueWith(task => lblStatus.Content = "After await");
}

可是ContinueWith里面的是一个委托,最好写成函数。另写一个函数最简单,不过C#使用另外一种“状态机”技术把这个函数写在了原来的方法内。也就是说,第一段代码实际上会被编译为(并不精确):

private void btnDoStuff_Click(int step)
{
    switch (step)
    {
        case 0:
            lblStatus.Content = "Doing Stuff";
            Task t = Task.Delay(4000);
            t.ContinueWith(task => btnDoStuff_Click(task.Step));
            break;
        case 1:
            lblStatus.Content = "After await";
            break;
    }
}

方法被添加了一个参数step, 第一次调用方法时step为0,此后每进入一次则加一。由此则用一个方法实现了两端逻辑,这即是状态机重入技术。C#的另外一个语法糖:用yield实现IEnumerable接口也是采用这种技术。

Execution Context 执行上下文

它在多线程的比如空气:你能够不知道它,但它很是重要。ExecutionContext是为了解决线程本地存储在多线程中没法传递的问题:总得有一种机制可以传递全局信息。不然只能经过函数调用参数传递了。
当一个线程发起异步调用的时候,ExecutionContext会自动的在线程之间传递如下信息:

  • 线程安全设置
  • Host设置(与web服务有关)
  • Logical Call Context, 能够在其中保存和传递对象。
  • 线程的Culture(从.NET 4.6之后)。

Synchronization Context 同步上下文

它是为了描述异步调用返回时的行为所建立的抽象。它有两个基本接口方法:

  • Send 同步地等待任务执行完毕。
  • Post 把任务发出去就无论了。

那么异步调用返回时的行为是什么意思?既然是抽象,那就会有具体的实现。后面咱们会看到几种实现。

当开始异步调用时,C#会捕获(capture)当前线程的同步上下文,并保存到Task中。在异步调用返回时,须要恢复(resume)同步上下文。此时就会调用同步上下文的Send或者Post

下面是几种典型的同步上下文实现:

  1. UI同步上下文。因为UI界面操做必须在UI线程中进行,所以这个上下文作的事情就是把须要恢复的工做Marshal起来交给UI线程。(可能有人会好奇如何交给UI线程去作。简单来讲, UI线程有个Windows消息循环,同步上下文将任务封装在一个特定消息中,UI线程获得这个消息后,就去执行其中的任务)。
  2. ASP.NET同步上下文。它有如下特色:

    • 不会切换线程,由于后台线程没什么区别。
    • 会把线程的Principle和Culture传递过去。(由于ASP.NET依赖于此)
    • 在异步页面中记录还没有完成的IO数量。
  3. 默认同步上下文。就是线程池的调度器,基本上没有特别的操做。

最后,调用ConfigureAwait(false)时,就会跳过恢复同步上下文这一过程。因此,有时候必要(当不必传递任何信息时,使用它能够提升效率),有时候又会出错。例如,UI程序的异步调用原本没问题,你加了这个语句,反而会形成修改界面的操做可能不在UI线程中执行,从而出错。可是注意,不管如何,执行上下文都是会传递的。

结合以上,第一段程序的更精确的编译后版本是这样的:

private void btnDoStuff_Click(int step)
{
    switch (step)
    {
        case 0:
            lblStatus.Content = "Doing Stuff";
            Task t = Task.Delay(4000);
            t.ContinueWith(
                task => SynchronizationContext.Current.Post(
                 state => btnDoStuff_Click(task.Step), 
                 task)
            );
            break;
        case 1:
            lblStatus.Content = "After await";
            break;
    }
}

究竟是谁在执行异步调用?

这个问题曾经困扰我好久。若是我当前的线程调用一个异步调用后返回了,那究竟是谁在完成真正调用的工做呢?答案是一个(或几个)共享的线程:线程池中的IO线程。

以下是一段代码:

async void GetButton_OnClick(object o, EventArgs e)
{
    Task<Image> task = GetFaviconAsync(_url);

    Image image = await task;

    AddAFavicon(image);
}

async Task<Image> GetFaviconAsync(string url)
{
    var task = _webClient.DownloadDataTaskAsync(url);

    byte[] bytes = await task;

    return MakeImage(bytes);
}

线程的执行状况以下图:

clipboard.png

大部分的时间都在用户线程中。只有调用到很是底层,IO完成以后,才有IO线程被唤醒(见11),而后它调用Task的同步上下文的Post,将剩下的任务再交给用户线程去执行。

下面是一个动态的解释:

clipboard.png

ASP.NET应用的异步线程模型

关于IIS的架构和工做过程,有一些资料,这里也不打算深究。提供两张图,以供理解。

clipboard.png

clipboard.png

相关文章
相关标签/搜索