目录
- 前言
- 新异步的使用
- 新异步的优势
- 对于WinForm、WPF等单线程UI程序
- 对于Web后台服务程序
- Result的死锁陷阱
- 使用AsyncHelper在同步代码里面调用异步
- ConfigureAwait
- 异常处理
- 异步的实现
前言
今天说异步的主要是指C#5的async\await异步。在此为了方便的表述,我们称async\await之前的异步为“旧异步”,async\await为“新异步”。
新异步的使用
只能说新异步的使用太简单(如果仅仅只是说使用)
方法加上async修饰符,然后使用await关键字执行异步方法,即可。对就是如此简单。像使用同步方法逻辑一样使用异步。
public async Task<int> Test()
{
var num1 = await GetNumber(1);
var num2 = await GetNumber(num1);
var task = GetNumber(num2);
//或者
var num3 = await task;
return num1 + num2 + num3;
}
新异步的优势
在此之前已经有了多种异步模式,为什么还要引入和学习新的async\await异步呢?当然它肯定是有其独特的优势。
我们分两个方面来分析:WinForm、WPF等单线程UI程序和Web后台服务程序。
对于WinForm、WPF等单线程UI程序
代码1(旧异步)
private void button1_Click(object sender, EventArgs e)
{
var request = WebRequest.Create("https://github.com/");
request.BeginGetResponse(new AsyncCallback(t =>
{
//(1)处理请求结果的逻辑必须写这里
label1.Invoke((Action)(() => { label1.Text = "[旧异步]执行完毕!"; }));//(2)这里跨线程访问UI需要做处理
}), null);
}
代码2(同步)
private void button3_Click(object sender, EventArgs e)
{
HttpClient http = new HttpClient();
var htmlStr = http.GetStringAsync("https://github.com/").Result;
//(1)处理请求结果的逻辑可以写这里
label1.Text = "[同步]执行完毕!";//(2)不在需要做跨线程UI处理了
}
代码3(新异步)
private async void button2_Click(object sender, EventArgs e)
{
HttpClient http = new HttpClient();
var htmlStr = await http.GetStringAsync("https://github.com/");
//(1)处理请求结果的逻辑可以写这里
label1.Text = "[新异步]执行完毕!";//(2)不在需要做跨线程UI处理了
}
新异步的优势:
- 没有了烦人的回调处理
- 不会像同步代码一样阻塞UI界面(造成假死)
- 不在像旧异步处理后访问UI不在需要做跨线程处理
- 像使用同步代码一样使用异步(超清晰的逻辑)
是的,说得再多还不如看看实际效果图来得实际:(新旧异步UI线程没有阻塞,同步阻塞了UI线程)
对于Web后台服务程序
也许对于后台程序的影响没有单线程程序那么直观,但其价值也是非常大的。且很多人对新异步存在误解。
【误解】:新异步可以提升Web程序的性能。
【正解】:异步不会提升单次请求结果的时间,但是可以提高Web程序的吞吐量。
1、为什么不会提升单次请求结果的时间?
其实我们从上面示例代码(虽然是UI程序的代码)也可以看出。
public class GetDataHelper
{
/// <summary>
/// 同步方法获取数据
/// </summary>
/// <returns></returns>
public string GetData()
{
var beginInfo = GetBeginThreadInfo();
using (HttpClient http = new HttpClient())
{
http.GetStringAsync("https://github.com/").Wait();//注意:这里是同步阻塞
}
return beginInfo + GetEndThreadInfo();
}
/// <summary>
/// 异步方法获取数据
/// </summary>
/// <returns></returns>
public async Task<string> GetDataAsync()
{
var beginInfo = GetBeginThreadInfo();
using (HttpClient http = new HttpClient())
{
await http.GetStringAsync("https://github.com/");//注意:这里是异步等待
}
return beginInfo + GetEndThreadInfo();
}
public string GetBeginThreadInfo()
{
int t1, t2, t3;
ThreadPool.GetAvailableThreads(out t1, out t3);
ThreadPool.GetMaxThreads(out t2, out t3);
return string.Format("开始:{0:mm:ss,ffff} 线程Id:{1} Web线程数:{2}",
DateTime.Now,
Thread.CurrentThread.ManagedThreadId,
t2 - t1);
}
public string GetEndThreadInfo()
{
int t1, t2, t3;
ThreadPool.GetAvailableThreads(out t1, out t3);
ThreadPool.GetMaxThreads(out t2, out t3);
return string.Format(" 结束:{0:mm:ss,ffff} 线程Id:{1} Web线程数:{2}",
DateTime.Now,
Thread.CurrentThread.ManagedThreadId,
t2 - t1);
}
}
3、新建一个web api控制器
[HttpGet]
public async Task<string> Get(string str)
{
GetDataHelper sqlHelper = new GetDataHelper();
switch (str)
{
case "异步处理"://
return await sqlHelper.GetDataAsync();
case "同步处理"://
return sqlHelper.GetData();
}
return "参数不正确";
}
4、发布web api程序,部署到本地iis(同步链接:http://localhost:803/api/Home?str=同步处理 异步链接:http://localhost:803/api/Home?str=异步处理)
5、接着上面的winform程序里面测试请求:(同时发起10个请求)
private void button6_Click(object sender, EventArgs e)
{
textBox1.Text = "";
label1.Text = "";
Task.Run(() =>
{
TestResultUrl("http://localhost:803/api/Home?str=同步处理");
});
}
private void button5_Click(object sender, EventArgs e)
{
textBox1.Text = "";
label1.Text = "";
Task.Run(() =>
{
TestResultUrl("http://localhost:803/api/Home?str=异步处理");
});
}
public void TestResultUrl(string url)
{
int resultEnd = 0;
HttpClient http = new HttpClient();
int number = 10;
for (int i = 0; i < number; i++)
{
new Thread(async () =>
{
var resultStr = await http.GetStringAsync(url);
label1.Invoke((Action)(() =>
{
textBox1.AppendText(resultStr.Replace(" ", "\r\t") + "\r\n");
if (++resultEnd >= number)
{
label1.Text = "全部执行完毕";
}
}));
}).Start();
}
}
6、重启iis,并用浏览器访问一次要请求的链接地址(预热)
7、启动winform程序,点击“访问同步实现的Web”:
也就是说“异步实现”使用更少的web线程完成了同样的请求数量,如此一来我们就有更多剩余的web线程去处理更多用户发起的请求。
接着我们还发现同步实现请求前后的线程ID是一致的,而异步实现前后线程ID不一定一致。再次证明执行await异步前释放了主线程。
【结论】:
- 使用新异步可以提升Web服务程序的吞吐量
- 对于客户端来说,web服务的异步并不会提高客户端的单次访问速度。
- 执行新异步前会释放web线程,而等待异步执行完成后又回到了web线程上。从而提高web线程的利用率。
【图解】:
Result的死锁陷阱
我们在分析UI单线程程序的时候说过,要慎用异步的Result属性。下面我们来分析:
private void button4_Click(object sender, EventArgs e)
{
label1.Text = GetUlrString("https://github.com/").Result;
}
public async Task<string> GetUlrString(string url)
{
using (HttpClient http = new HttpClient())
{
return await http.GetStringAsync(url);
}
}
代码GetUlrString("https://github.com/").Result的Result属性会阻塞(占用)UI线程,而执行到GetUlrString方法的 await异步的时候又要释放UI线程。此时矛盾就来了,由于线程资源的抢占导致死锁。
且Result属性和.Wait()方法一样会阻塞线程。此等问题在Web服务程序里面一样存在。(区别:UI单次线程程序和web服务程序都会释放主线程,不同的是Web服务线程不一定会回到原来的主线程,而UI程序一定会回到原来的UI线程)
我们前面说过,.net为什么会这么智能的自动释放主线程然后等待异步执行完毕后又回到主线程是因为SynchronizationContext的功劳。
但这里有个例外,那就是控制台程序里面是没有SynchronizationContext的。所以这段代码放在控制台里面运行是没有问题的。
static void Main(string[] args)
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
GetUlrString("https://github.com/").Wait();
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
}
public async static Task<string> GetUlrString(string url)
{
using (HttpClient http = new HttpClient())
{
Console.WriteLine(Thread.CurrentThread.ManagedThreadId);
return await http.GetStringAsync(url);
}
}
打印出来的都是同一个线程ID
使用AsyncHelper在同步代码里面调用异步
但可是,可但是,我们必须在同步方法里面执行异步怎办?办法肯定是有的
我们首先定义一个AsyncHelper静态类:
static class AsyncHelper
{
private static readonly TaskFactory _myTaskFactory = new TaskFactory(CancellationToken.None,
TaskCreationOptions.None, TaskContinuationOptions.None, TaskScheduler.Default);
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
return _myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
}
public static void RunSync(Func<Task> func)
{
_myTaskFactory.StartNew(func).Unwrap().GetAwaiter().GetResult();
}
}
然后调用异步:
private void button7_Click(object sender, EventArgs e)
{
label1.Text = AsyncHelper.RunSync(() => GetUlrString("https://github.com/"));
}
这样就不会死锁了。
ConfigureAwait
除了AsyncHelper我们还可以使用Task的ConfigureAwait方法来避免死锁
private void button7_Click(object sender, EventArgs e)
{
label1.Text = GetUlrString("https://github.com/").Result;
}
public async Task<string> GetUlrString(string url)
{
using (HttpClient http = new HttpClient())
{
return await http.GetStringAsync(url).ConfigureAwait(false);
}
}
ConfigureAwait的作用:使当前async方法的await后续操作不需要恢复到主线程(不需要保存线程上下文)。
异常处理
关于新异步里面抛出异常的正确姿势。我们先来看下面一段代码:
private async void button8_Click(object sender, EventArgs e)
{
Task<string> task = GetUlrStringErr(null);
Thread.Sleep(1000);//一段逻辑。。。。
textBox1.Text = await task;
}
public async Task<string> GetUlrStringErr(string url)
{
if (string.IsNullOrWhiteSpace(url))
{
throw new Exception("url不能为空");
}
using (HttpClient http = new HttpClient())
{
return await http.GetStringAsync(url);
}
}
调试执行执行流程:
异步的实现
上面简单分析了新异步能力和属性。接下来让我们继续揭秘异步的本质,神秘的外套下面究竟是怎么实现的。
首先我们编写一个用来反编译的示例:
class MyAsyncTest
{
public async Task<string> GetUrlStringAsync(HttpClient http, string url, int time)
{
await Task.Delay(time);
return await http.GetStringAsync(url);
}
}
反编译代码:
public Task<string> GetUrlStringAsync(HttpClient http, string url, int time)
{
GetUrlStringAsyncdStateMachine stateMachine = new GetUrlStringAsyncdStateMachine()
{
_this = this,
http = http,
url = url,
time = time,
_builder = AsyncTaskMethodBuilder<string>.Create(),
_state = -1
};
stateMachine._builder.Start(ref stateMachine);
return stateMachine._builder.Task;
}
方法签名完全一致,只是里面的内容变成了一个状态机GetUrlStringAsyncdStateMachine 的调用。此状态机就是编译器自动创建的。下面来看看神秘的状态机是什么鬼:
明显多个异步等待执行的时候就是在不断调用状态机中的MoveNext()方法。经验来至我们之前分析过的IEumerable,不过今天的这个明显复杂度要高于以前的那个。猜测是如此,我们还是来验证下事实: