zl程序教程

您现在的位置是:首页 >  后端

当前栏目

ASP.NET Core – Thread, Task, Async 线程与异步编程

2023-09-27 14:23:55 时间

前言

平常写业务代码, 很少会写到多线程. 久了很多东西都忘光光了. 刚好最近在复习 RxJS. 有一篇是讲 scheduler 的.

会讲到 JavaScript 异步相关的资讯. 既然如此那就一次过把相关的东西都复习一下呗.

以前写过的文章 : 异步编程 (发布于 2015-04-02)

 

主要参考

腾飞 – async & await 的前世今生(Updated) (必读)

老赵 – 正确使用异步操作 (必读)

 

线程基本概念

进程和线程

一个 Application 会用到一个进程和最少一个线程 (不同 App 的进程相互独立, 进程内的线程相互独立)

线程池

创建和销毁线程是很费劲的, 所以通常会有线程池用来缓存线程, 类似 SQL connection pool.

池中控制一定数量的线程, 想用就去池中拿, 用完了就放回去.

为什么要多线程

因为想 multitask. 看过 F1 赛车换轮胎的过程吗? 是不是一堆人一起上? 同样道理, 要快就让多个线程一起上 (当然前提是分工要分的好)

并发和锁

线程多就可能导致并发问题. 线程 1 读了一个值 (static value) 想拿来做逻辑判断, 但同一时间线程 2 写了这个值, 这样就可能导致逻辑错误. 

SQL 经常遇到这种事儿. 解决方法就是加锁.

 

异步编程基本概念

何为异步编程

参考: 知乎 – 认识异步编程

异步对比的是同步. 同步就是程序一行一行的运行. 异步则是并行或者分叉. 而这个分叉不会导致主程序暂停.

它们会一起跑, 主程序可以给一个 callback 让分叉结束后调用, 也可以选择等待分叉结束.

 

异步编程和多线程的关系

参考 : 一篇文章,搞懂异步和多线程的区别

多线程是实现异步的手段. 但不是唯一手段.

 

Compute-Bound Operation 和 IO-Bound Operation 与异步的关系

有 2 种常见的异步. 我们常常傻傻分不清楚.

Compute-Bound Operation 指的是那些很花时间的 CPU 操作. 比如 100万次 for loop.

如果只用主程序运行, 那么就会很耗时, 把它们拆成多个线程去完成 (每个线程负责 25万次) 那么时间就快了 4 倍. (当然前提是你有多核 CPU 或者多 CPU)

这是一种异步, 用到了多线程来完成. (F1 赛车的例子)

IO-Bound Operation 指的是那些很花时间的非 CPU 操作, 比如读写 IO, Network 通讯.

如果主程序等待这些操作的话, 那么就很浪费. 所以这里需要一个异步, 当进行 IO 时, 主程序应该释放 CPU 资源, 等待 IO 的 callback.

这是另一种异步, 它需要硬件和 OS 支持的. .NET 有许多 build-in 方法都属于这类异步 e.g. File, SQL, Web Request

下面所有提到的 Task 都是属于第一种异步 (CPU 操作), 因为第二种没什么好说的 .NET build-in 好了, 而且也只能用它 build-in 好的.

 

创建 Task

早年, 我们是会用 new Thread() 这种直接创建线程的操作的. 但现在我们只用 Task.

一个 main thread, 2 个 subthread.

public class Program
{
    public static async Task Main(string[] args)
    {
        Console.WriteLine("Main Thread Id : " + Environment.CurrentManagedThreadId);
        var task1 = Task.Run(() => Action("Task1")); // create and run subthread 1
        var task2 = Task.Run(() => Action("Task2")); // create and run subthread 2
        for (int index = 0; index < 30; index++)
        {
            Console.WriteLine($"Main Thread Index : {index}");
        }
        await Task.WhenAll(task1, task2);
    }

    public static void Action(string taskName)
    {
        Console.WriteLine($"{taskName} Id: {Environment.CurrentManagedThreadId}");
        for (int index = 0; index < 30; index++)
        {
            Console.WriteLine($"{taskName} Index : {index}");
        }
    }
}

效果

3 个 thread id 都不相同. 它们是并发执行的. 所以顺序会很乱.

 

await Task & callback

参考: C#中的Task.WhenAll和Task.WhenAny方法介绍

通常 main thread 最终会等待所有 subthread 完成.

从前 Task 有许多方法 Task.Wait, Task.Result, Task.GetAwaiter().GetResult() 但这些统统不要了, 统一用 await async 就好. 参考: Avoid GetAwaiter().GetResult() at all cost

public static async Task Main(string[] args)
{
    var task = Task.Run(Function); // 没有参数就不需要匿名方法
    var value =  await task; 
    Console.WriteLine(value); // value
}

public static string Function()
{
    return "value";
}

等待多个 task 用 Task.WhenAll

await Task.WhenAll(task1, task2);

等待第一个完成的 task 用 Task.WhenAny

var task = await Task.WhenAny(t1, t2, t3);
var value = task.Result;

它返回的是 Task 哦, 但直接 .Result 就可以取值了, 因为它已经执行完了. 其余的 Task 虽然开始执行, 但未必完成了

不想等待的话可以传入 callback 通过 ContinueWith

var task = Task.Run(Function).ContinueWith(t => {
    var value = t.Result; // value
});

 

并发

public class Program
{
    public static int index = 0;

    public static async Task Main(string[] args)
    {
        await Task.WhenAll(Task.Run(Action), Task.Run(Action));
        Console.WriteLine("done");
    }

    public static void Action()
    {
        var currentIndex = index;
        for (var i = 0; i < 100000; i++)
        {
            index++;
            if (index != currentIndex + i + 1)
            {
                Console.WriteLine("concurreny");
                break;
            }
        }
    }
}

2 个 task 都读写 static index 于是问题就出现了. 解决方法是加锁.

public class Program
{
    private static object _lock = new object();
    public static void Action()
    {
        lock (_lock) {
            for (var i = 0; i < 100000; i++)
            {
                ...
            }
        }
    }
}

 

ConfigureAwait

参考:

A deep dive into ConfigureAwait

Stack Overflow – ConfigureAwait(false) relevant in ASP.NET Core?

configureAwait 声明异步执行完是否用回之前的 context, 绝大部分情况下是不需要的, 所以 set false. 它可以提升性能, 减少 deadlock 等等好处.

但是 .NET Framework default 是 true, 所以早年经常需要写 configureAwait(false)

幸好 .NET Core 已经修改了 async 的实现. 它的效果相当于 configureAwait(false). 所以我们再也不需要理会它了.

 

总结

大部分时候, 我们写 await 都是 for IO 异步(e.g. File, SQL, Network). 这种情况是不会增加线程的. 相反在等待期间它会把主线程让出去给其它人用.

只有需要大量 CPU 运算的情况下, 我们才需要创建更多线程去分工处理.