Skip to the content.

异步编程

异步的概念很容易与多线程弄混。我们要提高处理器的性能,仅依靠提高 “时钟频率” 已经不足。而目前主要提高的手段就是 “并行” 计算能力。利用 “多核” 提高应用程序的计算性能。

多核处理器,每个核在同一时刻只能运行一个线程。当 window 给每个核调度线程时会发生线程上下文切换。在线程切换期间,windows 内核要保存当前处理核心线程的状态给到操作系统内部的线程对象,然后从所有的高优先级的 “已就绪” 线程中选择一个,将线程上下文信息从线程对象传输到处理器,最后并执行。如果处理器从不同的处理器切换线程,就会产生更多的开销,因为内部的地址空间都发生了改变。

那么什么是并发:简而言之就是同时做多件事。应用程序可以利用 “并发” 在处理第一个请求的同时可以响应第二个请求。

什么是多线程:并发的一种形式,它采用多个线程来实现并发。

那么回到标题,什么是异步编程。《C# 并发编程经典实例》中对异步编程是这么介绍的:

异步编程:并发的一种形式,它采用 future 模式或回调(callback)机制,以避免产生不必要的线程

我把重点的内容加粗显示了。异步的目的就是为了减少不必要线程的产生,其目的也是为了减少线程上下文切换这种巨额开销的产生。上文提到的 future 代表一些即将完成的操作。如 Task,Task<Result>。异步编程的核心概念是异步操作启动的操作将会在一段时间后完成,这个操作正在执行时不会阻塞原来的线程。触发这个操作的线程可以继续执行其他任务。当这个操作完成时就会通知它的 future 或者回调函数让其知道操作已经完成。

线程

在异步编程下,我们很容易会联想到线程。我们知道线程是一个独立的运行单元,每个进程内有多个线程,每个线程都有独立的栈空间,各自运行自己的任务。

新建一个线程是一个开销很大的操作,因为会分配一些系统资源(包括但不限于线程内核对象,线程环境块,用户模式栈,内核模式栈,DLL 线程连接和分离通知),以及栈空间,在运行时还会造成更多的上下文切换。上下文切换会导致线程间的数据同步(复制)。所以为了减少这种单纯的开辟线程,后来就出现了线程池,线程池为了提高线程的复用率,会按需创建线程并用完后继续保留,以满足后续的需求,这样就避免了在此创建线程的开销。

每个处理器同一时间只能运行一个线程,线程在 “线程时间片(Thread Quantum)” 中执行代码,Thread Quantum 是时钟间隔(Clock Interval)的倍数,在目前的多处理系统中每个时钟间隔大概是 15 ms。当代码从栈顶返回进入等待状态后,或者是 Thread Quantum 时间到期,调度程序则会再选一个就绪线程来执行。下一个执行的线程有可能还是同一个线程,也有可能不是,这取决于处理器的竞争情况。—— 《编写高性能的 .NET 代码》

Thread Quantun:每个线程都有 Thread Quantun,它是占有 CPU 有效时间的一个度量。

从上面来看,如果从硬性标准来看, 如果你的应用程序执行时间没有超过一个 Thread Quantum 的话,那么直接开辟一个 Thread 是没问题的。不过总的来说,不管什么情况,您都应该使用 Task。

并行

PLinq 提供 api Parallel.For 来快捷的并行循环执行。我们用的最多的就是 Parallel.For/ForEach 这两个 api 了。虽然方便好用,但是也有很多细节需要注意。

如果我们想要在这批量并发的请求逻辑里面,需要中断并发执行。我们就可以借助重载函数的 ParallelLoopState 参数。

// 需要中断循环执行,可以传对象 ParallelLoopState
Parallel.ForEach(urls, (url, loopState) => {
    if (url.Contains("bing")) {
        // 调用 Break 中断当前请求之后的所有请求
        loopState.Break();
        // 调用 Stop
        // loopState.Stop();
    }
})

我们在调用并发方法时,我们一定要注意在执行过程最好不要共享变量,因为这会导致阻塞,丝毫体现不出并行的优势。

还有一点要注意的是,我们在执行并行程序的时候,通过代码可以发现每次迭代都会生成一个委托,如果每次迭代完成的时间还不如生成委托的开销大,那用这个就有点浪费了。

我们可以通过并行分区来解决这个问题。

// 分区 For 并行循环
var partitioner = (Partitioner.Create(0, MaxValue));
sum = 0;
Parallel.ForEach(partitioner, (range) => {
    long partialSum = 0;
    for (var i = range.Item1; i < range.Item2; i++) {
        partialSum += (long) Math.Sqrt(i);
    }
    Interlocked.Add(ref sum, partialSum);
});

上述方法是在每个分区里运行一个委托。当你不指定分区的时候,默认情况就是为每个迭代项都会创建一个委托。

真异步与线程异步

真正的异步 I/O 与在另一个线程上执行异步 I/O 这两者是有巨大的差别的。前者是实际上把处理控制权交给你操作系统和硬件,此时系统中的代码都不会发生阻塞,等待它们返回。如果你是在其他线程上执行异步 I/O,你会阻塞这个线程做其它工作,同时仍在等待操作系统返回给你。

Task.Run( ()=> 
{ 
    using (var inputStream = File.OpenRead(filenam e)) { 
        byte[] buffer = new byte[16384]; 
        // 调用同步 I/O 仍然会阻塞线程,在执行I/O时,会与底层硬件交互
        // 操作系统会转向驱动设备程序执行 IRP
        var input = inputStream.Read(buffer, 0, buffe r.Length); ... 
    } 
});