查阅 MSDN 异步编程以了解更多
异步编程
在 C# 中,异步编程是一种编程范式,它允许程序异步执行任务,而不会阻塞主线程或其他任务的执行。在异步程序中,程序可以继续执行其他任务,而无需等待一个任务完成
优势
异步编程可以为需要执行 I/O 操作或其他耗时任务的应用程序提供显著的优势,例如访问数据库、下载文件或执行网络通信。通过使用异步编程,应用程序可以提高性能、响应性、可扩展性和资源效率,同时简化代码并提高容错性
误解
- 异步编程总是更快:虽然异步编程可以提高性能,但它并不能保证在所有情况下都能提供更快的执行速度,如果一个应用程序没有大量的 I/O 操作或耗时任务,使用异步编程可能不会提供任何性能优势,甚至可能引入不必要的复杂性 
- 异步编程总是优于同步编程:虽然异步编程可以提高应用程序的可扩展性和响应性,但它可能并不总是最好的解决方案。异步编程可能会引入额外的复杂性,而且对于所有的用例可能都不是必需需要的 
- 异步编程可以完全消除对线程的需求:虽然异步编程可以减少显式线程的需求,但它并不能完全消除它。异步编程仍然依赖于底层线程和线程池在后台执行任务 
还有并不能完全消除锁和同步的需求、或认为异步容易调试等等
关键字
async 和 await
目标
支持读起来像一连串语句的代码,但会根据外部资源分配和任务完成时间以更复杂的顺序执行
内容
async
用以标记函数的关键字,例如:
public async Task TestAsync(){ }
但仅这还不是真正的异步函数
await
必须在 async 标记下的函数内使用的关键字,例如:
public async Task TestAsync()
{
	await Task.delay(1000);
	Console.WriteLine("TestDone");
}
async 和 await 一起使用才构成了异步函数
如此例所示,await 关键字将暂停 TestAsync 方法的执行,并返回不完整的 Task,在此期间,线程将返回到线程池,以便自己可用于另一个请求,await 完成后将恢复继续向下执行
但是,如何知道 await 已经完成呢?这里就需要用到 Task,可先了解一下此站另一个 blog - 线程
总结
- 该操作不会在线程池线程运行 
- 如果仅使用 - async关键字,那么函数内容仍会以同步方式进行
- 一般异步函数命名的最后结尾都是 Async 
- 帮助我们从异步操作中提取结果 
- 验证操作成功 
- 提供在异步方法中执行其余代码的延续 
Task
Task 抽象类是在 C# 5.0 (.NET Framwork 4.0) 引出的概念
大多数情况下,都是返回 Task 或者 Task<T>,极少数情况会返回 void
因为 Task 返回类型可提供更加丰富的监视内容和可等待内容结果,而 void 没有这些,仅在异步事件处理程序使用 void 返回类型
优势
- 抽象类允许我们轻松的使用线程池和本地线程,且绝大多数 - Task都在属于线程池的后台线程中执行
- 异步操作的状态管理: - Task对象跟踪异步操作的状态,包括是否已经完成、是否已经取消、是否已经出错等等,开发人员可以通过访问- Task的属性和方法来获取和操作异步操作的状态信息
- 异步操作的结果返回: - Task对象代表异步操作的最终结果,当异步操作完成时,它可以返回异步操作的结果或者抛出异常。异步方法的返回值通常是一个- Task对象或者- Task<T>对象,开发人员可以通过访问- Task对象的- Result属性或者使用- await关键字来获取异步操作的结果
- 异步操作的协调和组合: - Task对象可以通过多种方式来协调和组合异步操作,例如使用- Task.WhenAll或- Task.WhenAny方法来等待多个异步操作的完成或者组合多个异步操作的结果
- 并行和异步编程的支持: - Task对象可以帮助开发人员实现并行和异步编程模式,使用异步方法和异步操作可以提高应用程序的性能和响应性,避免阻塞 UI 线程或者阻塞 CPU
Task 对象不是线程,也不会创建线程,它只是代表了异步操作的状态和行为。异步操作的实际执行可能在线程池线程、IO 线程、UI 线程或者其它线程中执行,具体取决于异步操作的实现方式和上下文
比较
例一
public static void Main()  
{  
    double i = 0;  
    var mres = new ManualResetEventSlim(false);  
    ThreadPool.QueueUserWorkItem<ManualResetEventSlim>((_msg) =>   
    {   
		i = Math.Exp(1);   
        mres.Set();  
    }, mres, false);  
    
    mres.Wait();  
    Console.WriteLine(i);  
	// output:2.71828182845
}
上面这个例子首先创建了 ManualResetEventSlim API 与其中的 Set 和 Wait 方法来控制线程池运行,再创建线程池执行内容
现在,我们可以用如下 Task 来简化操作
public static void Main()  
{  
    var task = Task.Run(() => Math.Exp(1));  
    Console.WriteLine(task.Result);  
}
// output:2.71828182845
与用前一个 ThredPool 类方法相比,Task 省去了创建变量和控制线程发生的过程,且用前个方法不能返回类型,而此方法在这个例子中,返回了 Task<double> 类型(具有 Result 属性)
例二
in BaseClass.cs
public class CarBuilding  
{  
    private static Body BuildBody(int weight, int length, int width) => new Body(weight, length, width);  
    private static Engine BuildEngine(int horsePower) => new Engine(horsePower);  
    private static Suspension BuildSuspension(int supportedKg) => new Suspension(supportedKg);  
    private static Painting Paint(string color, int bodyArea) => new Painting(color, bodyArea);  
	private static void Test(Body body, IEnumerable<Suspension> suspensions, Engine engine)  
	{  
	    if (suspensions.Sum(s => s.SupportedKg) <= body.Weight || engine.Horsepower * 4 <= body.Weight)  
	        throw new ArgumentException("The car weighs too much");  
	}
    
    public static void Main()  
    {        
	    
    }  
}
public class Body  
{  
    public Body(int weight, int length, int width)  
    {        
	    Weight = weight;  
        Length = length;  
        Width = weight;  
        
        Console.WriteLine($"{Weight} and {Length} and {Width}");  
    }   
     
    public int Weight { get; set; }  
    public int Length { get; set; }  
    public int Width { get; set; }  
}  
  
public class Engine  
{  
    public Engine(int horsePower)  
    {        
	    Horsepower = horsePower;  
    }   
     
    public int Horsepower { get; set; }  
}  
  
public class Suspension  
{  
    public Suspension(int supportedKg)  
    {        
	    SupportedKg = supportedKg;  
	    
		Console.WriteLine(SupportedKg);
    } 
       
    public int SupportedKg { get; set; }  
}  
  
public class Painting  
{  
    public Painting(string color, int bodyArea)  
    {        
	    Color = color;  
        BodyArea = bodyArea;  
        
        Console.WriteLine($"{Color} and {BodyArea}");  
    }  
    
    public string Color { get; set; }  
    public int BodyArea { get; set; }  
}
在 Main 函数中添加以下内容:
public static void Main()  
{  
    Body body = null!;  
    var bodyThread = new Thread(() =>  
    {  
        body = BuildBody(100, 5, 2);  
    });    
    bodyThread.Start();  // Thread 方法
    
    var bodyTask = Task.Run(() => BuildBody(50,1,1));  // Task 方法
	// output: 
	// 100 and 5 and 100
	// 50 and 1 and 50
}
这里两个方法有着区别,Task 在这更具优势,上文也有提到(简洁、具有返回值)
继续往 Main 函数添加内容
public static void Main()  
{  
    Body body = null!;  
    var bodyThread = new Thread(() =>  
    {  
        body = BuildBody(100, 5, 2);  
    });    
    bodyThread.Start();  
    
    bodyThread.Join();  
    Painting painting;  
    var paintingThread = new Thread(() =>  
    {  
        painting = new Painting("red", body.Width * body.Length);  
    });}
    
	paintingThread.Start();
	paintingThread.Join();
	// output:
	// 100 and 5 and 100
	// red and 500
上面内容用的 Thread 写法写出两个子线程运行的方法,下面写一个 Task 写法
public static void Main()  
{  
    var bodyTask = Task.Run(() => BuildBody(50, 5, 2));  
    var paintingTask = bodyTask.ContinueWith(  
        task => Paint("red", task.Result.Width * task.Result.Length)  
    );
}
// output:
// 50 and 5 and 50
// red and 250
比较一下,用 Task 方法显而易见的简洁了,其中采用了链式编程,使用了 ContinueWith 方法,意思是执行 bodyTask 线程池,并开始函数内部委托的执行,使任务之间的依赖性清晰且定义明确
而且 ContinueWith 方法也提供第二个参数,如写法:task => Paint("red", task.Result.Width * task.Result.Length),TaskContinuationOptions.OnlyOnRanToCompletion,第二个参数就表明前一个线程池必须完成后才执行此委托
在 Main 函数中添加以下内容:
Thread 多个线程创建并执行写法
using System.Collections.Concurrent;
------------------------------------------------
public static void Main()  
{  
    var suspensions = new ConcurrentBag<Suspension>();  
    var suspensionThreads = Enumerable  
        .Range(0, 10)  
        .Select(i =>  
        {  
            var t = new Thread(() =>  
            {  
                suspensions.Add(BuildSuspension(40));  
            });            
            t.Start();  
            return t;  
        });  
        
    foreach (var suspThread in suspensionThreads)  
        suspThread.Join();  
	// output:40 40 40 ... 40 40 (10 个 40)
}
ConcurrentBag:表示线程安全、无序的对象集合
这个 API 就是为了解决多个线程池的性能问题
而用 Task 创建则无需此步骤,如下
public static void Main()  
{  
    var suspensionTasks = Enumerable  
        .Range(0, 10)  
        .Select(i => Task.Run(() => BuildSuspension(40)));  
    var suspensionsTask = Task.WhenAll(suspensionTasks)  
        .ContinueWith(task => task.Result.ToList());  
        
    // output:40 40 40 ... 40 40 (10 个 40)
}
同样省略了很多步骤,在里面使用了 WhenAll 方法,表明执行完所有内容并返回一个 Task<T> 类型
也可进一步优化为:Task.Factory.ContinueWhenAll(suspensionTasks, tasks => tasks.ToList());
在 Main 函数添加以下内容:
用 Thread 表示线程抛出异常
public static void Main()  
{  
    Exception? thrownException = null;
      
    var body = new Body(1,1,1);  
    IEnumerable<Suspension> suspensions = new Suspension[1];  
    var engine = new Engine(1);  
    
    var testing = new Thread(() =>  
    {  
        try  
        {  
            Test(body, suspensions, engine);  
        }        
        catch (Exception exc)   
        {   
            thrownException = exc;  
        }    
        });    
        testing.Start();  
  
    testing.Join();  
    if (thrownException is not null)  
        throw thrownException;  
}
只有当线程抛出异常,才会使 thrownException 有值,没有异常则是 null
Tasks 方法同样有内置工具检测异常,如下
public static void Main()  
{  
    var body = new Body(1,1,1);    
	IEnumerable<Suspension> suspensions = new Suspension[1];    
	var engine = new Engine(1);   
	try   
	{  
        var testingTask = Task.Run(  
            () => Test(body, suspensions,engine)  
        );    
    }    
    catch (AggregateException exc)  
    {        
	    throw exc.InnerExceptions[0];  
    }
}
在这种情况下,我们只重新抛出第一个内部异常,但我们也可以使用AgggregateException.Handle() 方法处理它
TAP
Task-based Asynchronous Pattern 基于 Task的异步编程模型
了解任务代表异步方法的执行而不是结果非常重要,Task 有几个属性,指示操作是否成功完成(状态、已完成、取消、错误)
死锁
一般出现在 UI 或 ASP.NET 程序中
内容
static object lock1 = new object();  
static object lock2 = new object();  
  
public static void Main()  
{  
    Thread t1 = new Thread(DoWork1);  
    Thread t2 = new Thread(DoWork2);  
    t1.Start();  
    t2.Start();  
    t1.Join();  
    t2.Join();  
    Console.WriteLine("Done.");  
}  
  
static void DoWork1()  
{  
    lock (lock1)  
    {        
	    Console.WriteLine("Thread 1 acquired lock1.");  
        Thread.Sleep(1000);  
        lock (lock2)  
        {            
	        Console.WriteLine("Thread 1 acquired lock2.");  
        }    
    }
}  
  
static void DoWork2()  
{  
    lock (lock2)  
    {        
	    Console.WriteLine("Thread 2 acquired lock2.");  
        Thread.Sleep(1000);  
        lock (lock1)  
        {            
	        Console.WriteLine("Thread 2 acquired lock1.");  
        }    
    }
}
在上面的代码中,定义了两个锁对象 lock1 和 lock2,然后创建了两个线程 t1 和 t2,分别在 DoWork1 和 DoWork2 方法中获取锁。然后在这两个方法中互相等待对方释放锁,导致程序陷入死锁状态。运行程序后,程序将无法继续执行下去,需要手动终止程序。为了避免死锁,我们需要使用合适的同步和互斥机制,避免线程之间的互相等待
lock代码块是 C# 中用于实现线程同步的机制。lock关键字可以将一段代码块标记为“临界区”,确保同一时刻只有一个线程可以进入这个代码块,从而避免多个线程同时访问共享资源,导致数据竞争和不确定性的问题,在上述示例中进入lock代码块之前,线程会尝试获取lock1的锁,如果这个锁当前没有被其他线程占用,那么线程将获取到锁,并进入临界区代码。在临界区代码执行完成之后,线程会释放锁,以便其他线程可以获取锁进入临界区
也尽量减少使用 Result 或 GetResult 或 Wait 等方法(在某些地方适用,如控制台程序)
Result属性为阻止属性。 如果你在其任务完成之前尝试访问它,当前处于活动状态的线程将被阻止,直到任务完成且值为可用。 在大多数情况下,应通过使用await访问此值,而不是直接访问属性
该函数内容需要阻止主线程而等待新开的线程先完成,这可能会导致死锁,这正是我们试图避免使用异步和等待关键字的事情
解决措施
- 代码同步化,不争抢线程资源 
- 不使用 - await关键字
- 使用 - ConfigureAwait()
上下文
上下文(Context)是一种用于在不同线程之间共享数据的机制。上下文对象可以在一个线程中创建,并在另一个线程中使用,它可以是线程上下文、进程上下文、同步上下文、应用程序域上下文、执行上下文等等
指程序中当前执行代码的环境或状态。可以将上下文视为一个程序运行时的快照,其中包含了程序的当前状态、已定义的变量、对象以及正在执行的代码的位置等信息
当前上下文
通常也指异步上下文,它与线程池线程一起管理异步操作的执行。异步上下文是一种保存与异步操作相关的上下文信息的机制,包括线程 ID、SynchronizationContext 对象等。异步操作在等待其他操作完成时可能会保存当前上下文,以便可以在异步操作完成时恢复它。当前上下文与异步操作的执行是密切相关的,异步操作必须在正确的上下文中执行才能正常工作
理解
- 如果使用的是 UI 线程,那么就是 UI 上下文 
- 如果使用的是 ASP.NET 请求响应,那么这就是 ASP.NET 请求上下文 
- 其余情况都是线程池上下文 
当我们等待 Task 时,当等待决定暂停方法执行时,会捕获请求当前上下文。一旦方法准备好恢复执行,应用程序将从线程池中获取线程,将其分配给上下文,并恢复执行
在 ASP.NET Core 应用程序中没有 SynchronizationContext。ASP.NET Core 避免捕获上下文并排队,它所做的只是从线程池中获取线程并将其分配给请求,因此,应用程序要做的后台工作要少得多
示例
// WinForms 示例程序(同样原理用于 WPF 程序).
private async void DownloadFileButton_Click(object sender, EventArgs e)
{
  // 因为我们使用了异步关键字 awiat,这个 UI 线程不会被文件下载阻止
  await DownloadFileAsync(fileNameTextBox.Text);
  // 由于我们恢复了 UI 上下文,可以直接调用 UI 元素
  resultTextBox.Text = "File downloaded!";
}
private static void button()  
{  
    Console.WriteLine($"main1 thread {Thread.CurrentThread.ManagedThreadId}");  
    TestConfigureAwait().Wait();  
    Console.WriteLine($"main2 thread: {Thread.CurrentThread.ManagedThreadId}");  
}  
  
private static async Task TestConfigureAwait()  
{  
    await Task.Run(() =>  
    {  
        Thread.Sleep(6000);  
        Console.WriteLine($"task thread: {Thread.CurrentThread.ManagedThreadId}");  
    }).ConfigureAwait(false); 
     
    Console.WriteLine($"testConfigureAwait thread: {Thread.CurrentThread.ManagedThreadId}");  
}
在这个示例中,是一个 UI 上下文内容,和使用了 ConfigureAwait(false) 来解决死锁问题
异常捕获
public static void Test()  
{  
    try  
    {  
        var result = SomeAsyncCode().Result;  
    }    
    catch (ArgumentException aex)  
    {        
	    Console.WriteLine($"Caught ArgumentException: {aex.Message}");  
    }
}  
  
private static async Task<string> SomeAsyncCode()  
{  
    await Task.Delay(10);  
    throw new ArgumentException("Oh noes!");  
}
在这个示例中我们尝试使用 try-catch 来捕捉异常,我们执行 SomeAsyncCode(),获取到它的 Result 属性。可以发现我们并没有进入到 catch 块。
这是因为,如果没有等待关键字,任务会吞噬异常。此外,不再有继续,操作也没有经过验证。如果继续执行此操作,将获得一个异常,但这将是映射异常,而不是我们从异步方法中抛出的异常
--- EXCEPTION #1/2 [ArgumentException]
在发生 ArgumentException 时,使用其他关键字尝试解决
在此示例,我们不能用 await 关键字来等待结果,我们使用 SomeAsyncCode().GetAwaiter().GetResult() 来捕捉,运行后结果正确,成功进入 catch 块
也可以使用 Global Error Handling 技术来解决或跟踪异常
控制台程序
在 C# 7.1 之前其实是不能对控制台程序对主入口 Main 函数标记为 async
原因
如果 await 看到可等待项尚未完成,那么它会异步发挥作用。它告诉 awaitable 在方法完成后运行剩余部分,然后从异步方法返回。当将方法的剩余部分传递给 await 时,awaiter 还将捕获当前上下文
异步方法将在完成之前返回给其调用者。这在 UI 应用程序(方法仅返回 UI 事件循环)和 ASP.NET 应用程序(该方法从线程返回,但使请求保持保留状态)中完美工作
对于控制台程序来说效果不太好:Main 函数返回到操作系统,因此程序退出
解决
控制台程序没有很大的必要使用异步,但为简单的一些操作演示,也是能解决的
让主线程强制等待异步完成,如以下示例:
static void Main(string[] args)
{
	MainAsync(args).GetAwaiter().GetResult();
	// or MainAsync(args).Wait()
}
static async Task MainAsync(string[] args)
{
	Bootstrapper bs = new Bootstrapper();
	var list = await bs.GetList();
}
主要就是 GetAwaiter 方法和 GetResult 方法,前者是获取到有 Task 标记的异步程序,后者是等待此有 Task 标记的异步程序结束
也可以直接使用 MainAsync(args).Wait(),Wait 方法与上面类似,但上面的两个方法会避免一些 AggregateException 包装错误
总结
以上内容为搜罗实验整理,如有错误欢迎留言指正交流,如有内容变动会及时更新
更多参考:
| Old | New | Description | 
|---|---|---|
| task.Wait | await task | Wait/await for a task to complete | 
| task.Result | await task | Get the result of a completed task | 
| Task.WaitAny | await Task.WhenAny | Wait/await for one of a collection of tasks to complete | 
| Task.WaitAll | await Task.WhenAll | Wait/await for every one of a collection of tasks to complete | 
| Thread.Sleep | await Task.Delay | Wait/await for a period of time | 
| Task constructor | Task.Run or TaskFactory.StartNew | Create a code-based task |