上篇主要分析了async\await之前的一些异步模式,今天说异步的主要是指C#5的async\await异步。在此为了方便的表述,我们称async\await之前的异步为“旧异步”,async\await为“新异步”。

新异步的使用

只能说新异步的使用太简单(如果仅仅只是说使用)

方法加上async修饰符,然后使用await关键字执行异步方法,即可。对就是如此简单。像使用同步方法逻辑一样使用异步。

 public async Task<int> Test()
{
var num1 = await GetNumber();
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线程)

【思考】:旧的异步模式是开启了一个新的线程去执行,不会阻塞UI线程。这点很好理解。可是,新的异步看上去和同步区别不大,为什么也不会阻塞界面呢?

【原因】:新异步,在执行await表达式前都是使用UI线程,await表达式后会启用新的线程去执行异步,直到异步执行完成并返回结果,然后再回到UI线程(据说使用了SynchronizationContext)。所以,await是没有阻塞UI线程的,也就不会造成界面的假死。

【注意】:我们在演示同步代码的时候使用了Result。然,在UI单线程程序中使用Result来使异步代码当同步代码使用是一件很危险的事(起码对于不太了解新异步的同学来说是这样)。至于具体原因稍候再分析(哎呀,别跑啊)。

对于Web后台服务程序

也许对于后台程序的影响没有单线程程序那么直观,但其价值也是非常大的。且很多人对新异步存在误解。

【误解】:新异步可以提升Web程序的性能。

【正解】:异步不会提升单次请求结果的时间,但是可以提高Web程序的吞吐量。

1、为什么不会提升单次请求结果的时间?

其实我们从上面示例代码(虽然是UI程序的代码)也可以看出。

2、为什么可以提高Web程序的吞吐量?

那什么是吞吐量呢,也就是本来只能十个人同时访问的网站现在可以二十个人同时访问了。也就是常说的并发量。

还是用上面的代码来解释。[代码2] 阻塞了UI线程等待请求结果,所以UI线程被占用,而[代码3]使用了新的线程请求,所以UI线程没有被占用,而可以继续响应UI界面。

那问题来了,我们的Web程序天生就是多线程的,且web线程都是跑的线程池线程(使用线程池线程是为了避免不断创建、销毁线程所造成的资源成本浪费),而线程池线程可使用线程数量是一定的,尽管可以设置,但它还是会在一定范围内。如此一来,我们web线程是珍贵的(物以稀为贵),不能滥用。用完了,那么其他用户请求的时候就无法处理直接503了。

那什么算是滥用呢?比如:文件读取、URL请求、数据库访问等IO请求。如果用web线程来做这个耗时的IO操作那么就会阻塞web线程,而web线程阻塞得多了web线程池线程就不够用了。也就达到了web程序最大访问数。

此时我们的新异步横空出世,解放了那些原本处理IO请求而阻塞的web线程(想偷懒?没门,干活了。)。通过异步方式使用相对廉价的线程(非web线程池线程)来处理IO操作,这样web线程池线程就可以解放出来处理更多的请求了。

不信?下面我们来测试下:

【测试步骤】:

1、新建一个web api项目

2、新建一个数据访问类,分别提供同步、异步方法(在方法逻辑执行前后读取时间、线程id、web线程池线程使用数)

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 = ;
HttpClient http = new HttpClient(); int number = ;
for (int i = ; 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”:

8、重复6,然后重新启动winform程序点击“访问异步实现的Web”

看到这些数据有什么感想?

数据和我们前面的【正解】完全吻合。仔细观察,每个单次请求用时基本上相差不大。 但是步骤7"同步实现"最高投入web线程数是10,而步骤8“异步实现”最高投入web线程数是3。

也就是说“异步实现”使用更少的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();//一段逻辑。。。。
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);
}
}

调试执行执行流程:

在执行完118行的时候竟然没有把异常抛出来?这不是逆天了吗。非得在等待await执行的时候才报错,显然119行的逻辑执行是没有什么意义的。让我们把异常提前抛出:

提取一个方法来做验证,这样就能及时的抛出异常了。有朋友会说这样的太坑爹了吧,一个验证还非得另外写个方法。接下来我们提供一个没有这么坑爹的方式:

在异步函数里面用匿名异步函数进行包装,同样可以实现及时验证。

感觉也不比前种方式好多少...可是能怎么办呢。

异步的实现

上面简单分析了新异步能力和属性。接下来让我们继续揭秘异步的本质,神秘的外套下面究竟是怎么实现的。

首先我们编写一个用来反编译的示例:

class MyAsyncTest
{
public async Task<string> GetUrlStringAsync(HttpClient http, string url, int time)
{
await Task.Delay(time);
return await http.GetStringAsync(url);
}
}

反编译代码:

点击看大图

为了方便阅读,我们把编译器自动命名的类型重命名。

GetUrlStringAsync 方法变成了如此模样:

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 = -
};
stateMachine._builder.Start(ref stateMachine);
return stateMachine._builder.Task;
}

方法签名完全一致,只是里面的内容变成了一个状态机 GetUrlStringAsyncdStateMachine  的调用。此状态机就是编译器自动创建的。下面来看看神秘的状态机是什么鬼:

private sealed class GetUrlStringAsyncdStateMachine : IAsyncStateMachine
{
public int _state;
public MyAsyncTest _this;
private string _str1;
public AsyncTaskMethodBuilder<string> _builder;
private TaskAwaiter taskAwaiter1;
private TaskAwaiter<string> taskAwaiter2; //异步方法的三个形参都到这里来了
public HttpClient http;
public int time;
public string url; private void MoveNext()
{
string str;
int num = this._state;
try
{
TaskAwaiter awaiter;
MyAsyncTest.GetUrlStringAsyncdStateMachine d__;
string str2;
switch (num)
{
case :
break; case :
goto Label_00CD; default:
//这里是异步方法 await Task.Delay(time);的具体实现
awaiter = Task.Delay(this.time).GetAwaiter();
if (awaiter.IsCompleted)
{
goto Label_0077;
}
this._state = num = ;
this.taskAwaiter1 = awaiter;
d__ = this;
this._builder.AwaitUnsafeOnCompleted<TaskAwaiter, MyAsyncTest.GetUrlStringAsyncdStateMachine>(ref awaiter, ref d__);
return;
}
awaiter = this.taskAwaiter1;
this.taskAwaiter1 = new TaskAwaiter();
this._state = num = -;
Label_0077:
awaiter.GetResult();
awaiter = new TaskAwaiter();
//这里是异步方法await http.GetStringAsync(url);的具体实现
TaskAwaiter<string> awaiter2 = this.http.GetStringAsync(this.url).GetAwaiter();
if (awaiter2.IsCompleted)
{
goto Label_00EA;
}
this._state = num = ;
this.taskAwaiter2 = awaiter2;
d__ = this;
this._builder.AwaitUnsafeOnCompleted<TaskAwaiter<string>, MyAsyncTest.GetUrlStringAsyncdStateMachine>(ref awaiter2, ref d__);
return;
Label_00CD:
awaiter2 = this.taskAwaiter2;
this.taskAwaiter2 = new TaskAwaiter<string>();
this._state = num = -;
Label_00EA:
str2 = awaiter2.GetResult();
awaiter2 = new TaskAwaiter<string>();
this._str1 = str2;
str = this._str1;
}
catch (Exception exception)
{
this._state = -;
this._builder.SetException(exception);
return;
}
this._state = -;
this._builder.SetResult(str);
} [DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
} }

明显多个异步等待执行的时候就是在不断调用状态机中的MoveNext()方法。经验来至我们之前分析过的IEumerable,不过今天的这个明显复杂度要高于以前的那个。猜测是如此,我们还是来验证下事实:

在起始方法 GetUrlStringAsync 第一次启动状态机 stateMachine._builder.Start(ref stateMachine);

确实是调用了 MoveNext 。因为_state的初始值是-1,所以执行到了下面的位置:

绕了一圈又回到了 MoveNext 。由此,我们可以现象成多个异步调用就是在不断执行MoveNext直到结束。

说了这么久有什么意思呢,似乎忘记了我们的目的是要通过之前编写的测试代码来分析异步的执行逻辑的。

再次贴出之前的测试代码,以免忘记了。

反编译后代码执行逻辑图:

当然这只是可能性较大的执行流程,但也有 awaiter.Iscompleted 为 true 的情况。其他可能的留着大家自己去琢磨吧。

本文已同步至索引目录:《C#基础知识巩固

本文demo:https://github.com/zhaopeiym/BlogDemoCode

【推荐】

https://www.cnblogs.com/zhaopei/p/async_two.html

 
 
 

C#异步(下)的更多相关文章

  1. 通过ifrmae异步下载文档

    //通过ifrmae异步下载文档 function iframeGetFile(opts) { var defaultOpts = { filePath: '', onload: function ( ...

  2. 异步 HttpContext.Current 为空null 另一种解决方法

    1.场景 在导入通讯录过程中,把导入的失败.成功的号码数进行统计,然后保存到session中,客户端通过轮询显示状态. 在实现过程中,使用的async调用方法,出现HttpContext.Curren ...

  3. 重回博客 谈一谈Node中的异步和单线程

    重回博客,这个帐号之前注册后就只发了一篇博客.听朋友建议,决定一周两次更新. 第一篇谈论一下最近想的比较多的异步的问题. 传统多线程异步 传统的异步是多线程的,当要同时做两件事的时候,他们是执行在不同 ...

  4. 深入Asyncio(七)异步上下文管理器

    Async Context Managers: async with 在某些场景下(如管理网络资源的连接建立.断开),用支持异步的上下文管理器是很方便的. 那么如何理解async with关键字? 先 ...

  5. 设计和编写一个异步通用Picker选择器,用于时间日期、城市、商品分类的选择

    目录 一.功能规划 二.最底层基础实现 (1)Picker界面和功能实现 (2)不同类型的选择器基础实现 三.数据源层 (1)时间日期 (2)多级同步分类,如:城市 (3)多级异步分类,如:城市 四. ...

  6. EntityFramework DbContext 线程安全

    先看这一段异常信息: A second operation started on this context before a previous asynchronous operation compl ...

  7. Android中pullToRefresh使用

    pullToRefresh的导入 首先,点击new按钮 -> import Module 然后在 New Module界面选择已经在本地的含有源代码的pullToRefresh. 打开如下图所示 ...

  8. WCF技术剖析之十一:异步操作在WCF中的应用(下篇)

    原文:WCF技术剖析之十一:异步操作在WCF中的应用(下篇) 说完了客户端的异步服务调用(参阅WCF技术剖析之十一:异步操作在WCF中的应用(上篇)),我们在来谈谈服务端如何通过异步的方式为服务提供实 ...

  9. Tornado 协程

    同步异步I/O客户端 from tornado.httpclient import HTTPClient,AsyncHTTPClient def ssync_visit(): http_client ...

随机推荐

  1. Oarcle之group by关键字与having关键字

    group by关键字    *group by :分组由 作用: 用于对于查询的数据进行分组并进行处理 例如:select deptno ,job from emp group by deptno, ...

  2. 在sparkStreaming实时存储时的问题

    1.实时插入mysql时遇到的问题,使用的updateStaeBykey有状态的算子 必须设置checkpoint  如果报错直接删掉checkpoint 在创建的时候自己保存偏移量即可 再次启动时读 ...

  3. MySQL-ISNULL()、IFNULL()和NULLIF()函数

    以下三个函数都可以用于where子条件,作为数据删除.更新的记录定位依据. 如: SELECT * FROM usergrade WHERE ISNULL(USERNAME); 一.ISNULL(ex ...

  4. Dell3470无法开机或开机黑屏情况下检测屏幕是否正常

    故障现象:Dell3470无法开机,点击开关按键无任何反应 故障分析:释放静电后故障依旧.更换电源适配器后故障依旧,初判主板故障,无法确认屏是否正常 解决方法:除去拆机单独测试外,Dell售后告知一个 ...

  5. ASCII对应码表-键值(完整版)

    ASCII对应码表-键值(完整版) Bin (二进制) Oct (八进制) Dec (十进制) Hex (十六进制) 缩写/字符 解释 0000 0000 00 0 0x00 NUL(null) 空字 ...

  6. mysqldiff差异比较

    1.安装mysql-utilities[root@localhost soft]# yum install  mysql-utilities.noarch 2.比较数据库zentao和数据库db_ze ...

  7. PL/SQL变量的作用域和可见性

    变量的作用域和可见性设计变量在块中的位置,不同的位置使得变量具有不同的有效性与可访问性. 变量的作用域是指可以使用变量的程序单元部分,可以是包和子程序包等. 当一个变量在它的作用域中可以用一个不限定的 ...

  8. vue 学习链接地址

    使用Vue.js构建Web应用程序 http://www.jdon.com/48545# 一步步带你做vue后台管理框架(一)——介绍框架 http://www.cnblogs.com/herozho ...

  9. Bootstrap3基础 下载bootstrap3压缩包和相应的jQuery文件

      内容 参数   OS   Windows 10 x64   browser   Firefox 65.0.2   framework     Bootstrap 3.3.7   editor    ...

  10. vue3.0使用axios报错问题记录

    vue-cli3.0使用axios的时候出现错误,记录一下 报错信息: Uncaught TypeError: Cannot set property $axios of #<Vue> w ...