1、错误的理解
- 以为 async 方法与 Task 是相同原理,只要被调用就会自动开辟新的异步线程?
- 以为一个 await 在等待期间会继续执行下一个 await,直到第一次引用 await 结果集的时候才会开始等待它的返回结果?
经过后续不断的测试,发现完全不是自己开头想象的那样,请参考以下的总结。
2、运行原理解说
- async 异步方法起初设计主要是用来解决WinForm单线程运行时出现的UI阻塞问题;
- async 可理解为是一个可与主线程轮流共生的方法;
1、当执行到await任务时、主线程的指针会自动切换到上一处 没有使用await调用async方法 的位置、继续往后运行;
(这时就释放了UI线程,从而解决了WinForm单线程UI阻塞的问题)2、当await任务运行完毕后、主线程的指针会再自动切回来;
(这时的async方法就又回到平时的单线程状态了、同时UI也被重新锁定了,所以可以直接修改、无需使用Control.Invoke() )
- 在《C# Async/Await异步函数原理》这篇文章中会有更专业的解说法;
看了上边的解说后应该就清楚以下的几个疑惑了吧?
问:为什么 async 方法内部必须得有 await ?
答:因为await任务才是异步的主要;问:为什么 async 方法没有 await 就不会异步运行?
答:因为await任务才是异步的主要;问:调用 async 方法后会立即开辟异步线程?
答:不是,先是同步运行,直到运行到内部的await时才会临时变成异步,当await运行完毕后会再切回去;问:调用 async 方法必须使用 await 开头?
答:不是,如果不用即时结果的情况可以不用await,可使用Task<T>异步接收;问:为什么 await 只能写在 async 方法中?
答:因为系统需要对异步方法做特殊处理才能实现异步,所以需要在异步方法的开头加上async关键字做标记;问:为什么 async 中写了多个 await 却还是同步运行?
答:因为此异步非多线程异步、而是单线程异步,每次运行到await时都会临时释放当前线程,让先去处理当前方法以外的事情;问:await 等待和 Task.Result 等待有什么不同?
答:Task.Result 是占用当前线程资源的同步等待,不像await会临时释放当前线程;问:async 异步方法适用于哪些场景?
答:比如需要实现在单线程下处理多个相互不冲突的任务时,避免单个业务出现多线程难以管理;
3、使用方法
- 程序需要先升级到.NET4.5框架、或.NET4.0框架通过NuGet安装 Microsoft.Bcl.Async ;
- async 方法无法使用out和ref参数,且返回值必须是void或Task或Task<TResult>;
- async 方法后的后缀名应以"Async"做为结尾,如“DownloadAsync”;
- await 只能在async方法内使用;
- await 需要和Task搭配使用;
- await 在多次出现时会按顺序运行;
- await 任务发生故障时会直接抛给主线程;
4.1、模拟单线程处理多任务
效果:
代码:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace ConsoleApplication1
{
public class Program
{
private static Stopwatch watch = new Stopwatch();
public static void Main()
{
watch.Start();
//模拟单线程运行多任务,同时还可以避免抢夺资源导致出现死锁的情况
var tasks = new List<Task<string>>();
{
tasks.Add(GetDataAsync("事务A")); //当内部执行到await时会临时跳出,此时的程序会在此处继续往后走,直到await完成时再切回去;
tasks.Add(GetDataAsync("事务B")); //当内部执行到await时会临时跳出,此时的程序会在此处继续往后走,直到await完成时再切回去;
tasks.Add(GetDataAsync("事务C")); //当内部执行到await时会临时跳出,此时的程序会在此处继续往后走,直到await完成时再切回去;
}
OutPut("开始等待...(此处可以处理一些其他事务,利用上等待期间的空闲资源)");
Task.WaitAll(tasks.ToArray());
OutPut("全部完成!" + string.Join("", tasks.Select(x => "\r\n\t" + x.Result).ToArray()));
watch.Stop();
Console.ReadKey();
}
/// <summary>
/// 模拟异步获取数据
/// </summary>
/// <param name="name">参数</param>
/// <returns></returns>
public static async Task<string> GetDataAsync(string name)
{
OutPut($"开始 {name}");
string result = "";
//await 可以临时释放线程,待完成后再折回
result += " " + await Task.Run(delegate
{
Thread.Sleep(3000); //模拟延时操作
return "数据A";
});
//await 可以临时释放线程,待完成后再折回
result += " " + await Task.Run(delegate
{
Thread.Sleep(3000); //模拟延时操作
return "数据B";
});
result = $"{name}:{result}";
OutPut($"完成 {result}");
return result;
}
/// <summary>
/// 输出
/// </summary>
/// <param name="msg"></param>
public static void OutPut(string msg) => Console.WriteLine($"{(watch.ElapsedMilliseconds / 1000D).ToString("0.000s")} <{Thread.CurrentThread.ManagedThreadId.ToString("00")}> {msg}");
}
}
4.2、模拟 WinForm 异步运行,防止UI阻塞
效果:
代码:
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
/// <summary>
/// 模拟异步加载、异步更新UI(原方法)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void button1_Click(object sender, EventArgs e)
{
var start = DateTime.Now;
textBox1.Text = Log($"开始");
//创建一个异步任务,为释放主线程,防止UI线程阻塞
Task.Run(delegate
{
//模拟一个耗时1秒的同步任务
Thread.Sleep(1000);
//异步更新UI
textBox1.BeginInvoke((Action)delegate
{
textBox1.Text += Log($"完成1 <{(DateTime.Now - start).TotalSeconds.ToString("0.00s")}>");
});
//模拟一个耗时1秒的同步任务
Thread.Sleep(1000);
//异步更新UI
textBox1.BeginInvoke((Action)delegate
{
textBox1.Text += Log($"完成2 <{(DateTime.Now - start).TotalSeconds.ToString("0.00s")}>");
});
//模拟一个耗时1秒的同步任务
Thread.Sleep(1000);
//异步更新UI
textBox1.BeginInvoke((Action)delegate
{
textBox1.Text += Log($"完成3 <{(DateTime.Now - start).TotalSeconds.ToString("0.00s")}>");
});
});
}
/// <summary>
/// 模拟异步加载、异步更新UI(新方法)
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private async void button2_Click(object sender, EventArgs e)
{
var start = DateTime.Now;
textBox2.Text = Log($"开始");
//模拟一个同步任务,使用异步运行 (此处通常是一个Task<T>)
await Task.Delay(1000);
//更新UI
textBox2.Text += Log($"完成1 <{(DateTime.Now - start).TotalSeconds.ToString("0.00s")}>");
//模拟一个同步任务,使用异步运行 (此处通常是一个Task<T>)
await Task.Delay(1000);
//更新UI
textBox2.Text += Log($"完成2 <{(DateTime.Now - start).TotalSeconds.ToString("0.00s")}>");
//模拟一个同步任务,使用异步运行 (此处通常是一个Task<T>)
await Task.Delay(1000);
//更新UI
textBox2.Text += Log($"完成3 <{(DateTime.Now - start).TotalSeconds.ToString("0.00s")}>");
//模拟一个耗时的任务,使用异步运行,完成后自动更新UI
textBox2.Text += Log($@"{await Task.Run(delegate
{
Thread.Sleep(1000);
return $"完成4 <{(DateTime.Now - start).TotalSeconds.ToString("0.00s")}>";
})}");
}
/// <summary>
/// 日志生成
/// </summary>
/// <param name="text"></param>
/// <returns></returns>
private string Log(string text) => $"\r\n{DateTime.Now.ToString("HH:mm:ss:fff")} <Thread-{Thread.CurrentThread.ManagedThreadId.ToString("00")}> {text}";
}
}