# C# 中的Task创建指南


本文还处于草稿阶段,难免还有错误修改改正,逻辑还不是很清晰,笔者会努力完善,长期更新!

[0000] 前言

标题起得有些"大",意在集大家的力量,总结出来一份关于Task相对"正确"的知识总结,欢迎读者提出宝贵意见!本文内容来自于笔者在编码的时候种种疑问,来自于对异步编程在操作系统中实际运行过程的好奇。平时使用Task战战兢兢,既想提高效率,又怕它不受控制,到处乱来。与其这样,不如此时此刻一起来了解它神秘的面纱吧!Just do IT.

[0001] 为什么要编写异步代码

新型应用广泛使用文件和网络 I/O。 默认情况下 I/O API 一般会阻塞,导致糟糕的用户体验和硬件利用率,除非希望学习和使用富有挑战的模式。 基于任务的异步 API 和语言级异步编程模型改变了这种模型,只需了解几个新概念就可默认进行异步执行。

异步代码具有以下特点:

  • 等待 I/O 请求返回的同时,可通过生成处理更多请求的线程,处理更多的服务器请求。
  • 等待 I/O 请求的同时生成 UI 交互线程,并通过将长时间运行的工作转换到其他 CPU 核心,让 UI 的响应速度更快。
  • 许多较新的 .NET APIs 都是异步的。
  • 在 .NET 中编写异步代码很简单!

来源: https://docs.microsoft.com

[0010] 关于C#中的异步编程模式

.NET 提供了执行异步操作的三种模式:

  • 基于任务的异步模式 (TAP) ,该模式使用单一方法表示异步操作的开始和完成。 TAP 是在 .NET Framework 4 中引入的。 这是在 .NET 中进行异步编程的推荐方法。 C# 中的 async 和 await 关键词以及 Visual Basic 中的 Async 和 Await 运算符为 TAP 添加了语言支持。 有关详细信息,请参阅基于任务的异步模式 (TAP)。

  • 基于事件的异步模式 (EAP) ,是提供异步行为的基于事件的旧模型。 这种模式需要后缀为 Async 的方法,以及一个或多个事件、事件处理程序委托类型和 EventArg 派生类型。 EAP 是在 .NET Framework 2.0 中引入的。 建议新开发中不再使用这种模式。 有关详细信息,请参阅基于事件的异步模式 (EAP)。

  • 异步编程模型 (APM) 模式(也称为 IAsyncResult 模式),这是使用 IAsyncResult 接口提供异步行为的旧模型。 在这种模式下,同步操作需要 BeginEnd 方法(例如,BeginWriteEndWrite以实现异步写入操作)。 不建议新的开发使用此模式。 有关详细信息,请参阅异步编程模型 (APM)。

模式的比较

为了快速比较这三种模式的异步操作方式,请考虑使用从指定偏移量处起将指定量数据读取到提供的缓冲区中的Read方法:

public class MyClass  
{  
    public int Read(byte [] buffer, int offset, int count);  
}  

此方法对应的 TAP 将公开以下单个 ReadAsync 方法:

public class MyClass  
{  
    public Task ReadAsync(byte [] buffer, int offset, int count);  
}  

对应的 EAP 将公开以下类型和成员的集:

public class MyClass  
{  
    public void ReadAsync(byte [] buffer, int offset, int count);  
    public event ReadCompletedEventHandler ReadCompleted;  
}  

对应的 APM 将公开 BeginReadEndRead 方法:

public class MyClass  
{  
    public IAsyncResult BeginRead(  
        byte [] buffer, int offset, int count,   
        AsyncCallback callback, object state);  
    public int EndRead(IAsyncResult asyncResult);  
}  

来源: https://docs.microsoft.com

分割线,未完区域--------------------------------

[0011] 实践

使用Async

请一路Async,否则会不可控。
网络请求,文件读写时系统自带的Async方法不会创建多线程,而是使用完成端口,依靠中断来实现!
线程池中的线程分为 WorkerThread 和 CompletionPortThread .
平时我们使用的线程是WorkerThread,IO读写使用的是CompletionPortThread

1. 创建IO密集型任务

以下代码不会创建多个线程(WorkerThread),代码会在当前线程工作,且不会堵塞哦。
执行起来非常类似同步程序, 使用 await RunActionAsync(()=>{});
后,会立即执行程序

public Task RunActionAsync(Action action)
{
    TaskCompletionSource source = new TaskCompletionSource(TaskCreationOptions.AttachedToParent);
    Task task = source.Task;

    try
    {
        action.Invoke();
    }
    catch (Exception ex)
    {
        source.SetException(ex);
    }

    source.SetResult(Task.CompletedTask);

    return task;
}

2. 计算密集型任务

以下代码会创建新线程(WorkerThread),位于线程池,线程池默认最小WorkerThread为CPU核心数,CompletionPortThread为1000(实际最小值依实际运行情况而定,可手工修改)
运行时并不会立即执行Action,按照默认执行计划(TaskScheduler.Default执行,比如用for循环一堆Task.Run(async ()=> {await httpgetAsync(); echo(i); )任务,执行时你会发现i都是最后一个值

await Task.Run(()=>{});

以下代码会创建新线程(WorkerThread),在不在ThreadPool关键在于TaskCreationOptions枚举,如果为LongRunning,则直接会创建一个非线程池的线程执行任务,如果不是,则会在线程池里寻找线程,如果没有,会在线程池里新申请线程(创建一个耗时一秒),执行任务。
会立即执行Action

Task.Factory.StartNew(_ =>
{
    action.Invoke();
},
    null,
    CancellationToken.None,
    TaskCreationOptions.LongRunning,
    TaskScheduler.Default)

3.一些坑

以下代码.NET Core不支持哦,请使用Task.Factory.StartNew代替

Task.Factory.FromAsync(
    new Func((cb, obj) => action.BeginInvoke(biz, cb, obj)),
    new Action(ar => action.EndInvoke(ar)), null)

参考

The danger of TaskCompletionSource class

了解 .NET 的默认 TaskScheduler 和线程池(ThreadPool)设置,避免让 Task.Run 的性能急剧降低

.NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况

.NET 中使用 TaskCompletionSource 作为线程同步互斥或异步操作的事件

定义一组抽象的 Awaiter 的实现接口,你下次写自己的 await 可等待对象时将更加方便

.NET 除了用 Task 之外,如何自己写一个可以 await 的对象?

.NET 中什么样的类是可使用 await 异步等待的?

Asynchronous I/O in C#: I/O Completion Ports

Asynchronous I/O in C#: I/O Completion Ports

Migrating Delegate.BeginInvoke Calls for .NET Core

声明

本文采用知识共享署名-非商业性使用-相同方式共享 2.5 中国大陆许可协议进行许可,发表在CSDN和博客园,欢迎读者转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接!请读者/爬虫们尊重版权