C#异步编程由浅入深(一)
一、什么算异步?
广义来讲,两个工作流能同时进行就算异步,例如,CPU与外设之间的工作流就是异步的。在面向服务的系统中,各个子系统之间通信一般都是异步的,例如,订单系统与支付系统之间的通信是异步的,又如,在现实生活中,你去馆子吃饭,工作流是这样的,点菜->下单->做你的事->上菜->吃饭,这个也是异步的,具体来讲你和厨师之间是异步的,异步是如此重要,因外它代表者高效率(两者或两者以上的工作可以同时进行),但复杂,同步的世界简单,但效率极极低。
二、在编程中的异步
在编程中,除了同步和异步这两个名词,还多了一个阻塞和非阻塞,其中,阻塞和非阻塞是针对线程的概念,那么同步和异步是针对谁呢?其实很多情况下同步和异步并没有具体针对某一事物,所以导致了针对同步阻塞、同步非阻塞、异步阻塞、异步非阻塞这几个概念的模糊不清。并且也确实没有清晰的边界,请看以下例子:
public static void DoWorkA()
{
Thread thread = new Thread(() =>
{
Console.WriteLine("WorkA Done!");
});
thread.Start();
}
public static void DoWordB()
{
Thread thread = new Thread(() =>
{
Console.WriteLine("WorkB Done!");
});
thread.Start();
}
static void Main(string[] args)
{
DoWorkA();
DoWordB();
}
假设运行该代码的CPU是单核单线程,那么请问?DoWorkA()、DoWorkB()这两个函数是异步的吗?因为CPU是单核,所以根本不能同时运行两个函数,那么从这个层次来讲,他们之间其实是同步的,但是,现实的情况是我们一般都认为他们之间是异步的,因为我们是从代码的执行顺序角度考虑的,而不是从CPU本身的工作流程考虑的。所以要分上下文考虑。再请看下面这个例子:
static void Main(string[] args)
{
DoWorkA();
QueryDataBaseSync();//同步查询数据库
DoWorkB();
}
从代码的执行顺序角度考虑,这三个函数执行就是同步的,但是,从CPU的角度来讲,数据库查询工作(另一台机器)和CPU计算工作是异步的,在下文中,没有做特别申明,则都是从代码的执行顺序角度来讨论同步和异步。
再解释一下阻塞和非阻塞以及相关的知识:
阻塞特指线程由运行状态转换到挂起状态,但CPU并不会阻塞,操作系统会切换另一个处于就绪状态的线程,并转换成运行状态。导致线程被阻塞的原因有很多,如:发生系统调用(应用程序调用系统API,如果调用成功,会发生从应用态->内核态->应用态的转换开销),但此时外部条件并没有满足,如从Socket内核缓冲区读数据,此时缓冲区还没有数据,则会导致操作系统挂起该线程,切换到另一个处于就绪态的线程然后给CPU执行,这是主动调用导致的,还有被动导致的,对于现在的分时操作系统,在一个线程时间片到了之后,会发生时钟中断信号,然后由操作系统预先写好的中断函数处理,再按一定策略(如线程优先级)切换至另一个线程执行,导致线程被动地从运行态转换成挂起状态。
非阻塞一般指函数调用不会导致执行该函数的线程从运行态转换成挂起状态。
三、原始的异步编程模式之回调函数
在此之前,我们先稍微了解下图形界面的工作原理,GUI程序大概可以用以下伪代码表示:
While(GetMessage() != 'exit') //从线程消息队列中获取一个消息,线程消息队列由系统维护,例如鼠标移动事件,这个事件由操作系统捕捉,并投递到线程的消息队列中。
{
msg = TranslateMessage();//转换消息格式
DispatherMessage(msg);//分发消息到相应的处理函数
}
其中DispatherMessage根据不同的消息类型,调用不同的消息处理函数,例如鼠标移动消息(MouseMove),此时消息处理函数可以根据MouseMove消息中的值,做相应的处理,例如调用绘图相关函数画出鼠标此刻的形状。
一般来讲,我们称这个循环为消息循环(事件循环、EventLoop),编程模型称为消息驱动模型(事件驱动),在UI程序中,执行这部分代码的线程一般只有一个线程,称为UI线程,为什么是单线程,读者可以去思考。
以上为背景知识。现在,我们思考,假如在UI线程中执行一个会导致UI线程被阻塞的操作,或者在UI线程执行一个纯CPU计算的工作,会发生什么样的结果?如果执行一个导致UI线程被阻塞的操作,那么这个消息循环就会被迫停止,导致相关的绘图消息不能被相应的消息处理函数处理,表现就是UI界面“假死”,直到UI线程被唤起。如果是纯CPU计算的工作,那么也会导致其他消息不能被及时处理,也会导致界面“假死”现象。如何处理这种情况?写异步代码。
我们先用控制台程序模拟这个UI程序,后面以此为基础。
public static string GetMessage()
{
return Console.ReadLine();
}
public static string TranslateMessage(string msg)
{
return msg;
}
public static void DispatherMessage(string msg)
{
switch (msg)
{
case "MOUSE_MOVE":
{
OnMOUSE_MOVE(msg);
break;
}
default:
break;
}
}
public static void OnMOUSE_MOVE(string msg)
{
Console.WriteLine("开始绘制鼠标形状");
}
static void Main(string[] args)
{
while(true)
{
string msg = GetMessage();
if (msg == "quit") return;
string m = TranslateMessage(msg);
DispatherMessage(m);
}
}
1、回调函数
上面那个例子,一但外部有消息到来,根据不同的消息类型,调用不同的处理函数,如鼠标移动时产生MOUSE_DOWN消息,相应的消息处理函数就开始重新绘制鼠标的形状,这样一但你鼠标移动,就你会发现屏幕上的鼠标跟着移动了。
现在假设我们增加一个消息处理函数,如OnMOUSE_DOWN,这个函数内部进行了一个阻塞的操作,如发起一个HTTP请求,在HTTP请求回复到来前,该UI程序会“假死”,我们编写异步代码来解决这个问题。
public static int Http()
{
Thread.Sleep(1000);//模拟网络IO延时
return 1;
}
public static void HttpAsync(Action<int> action,Action error)
{
//这里我们用另一个线程来实现异步IO,由于Http方法内部是通过Sleep来模拟网络IO延时的,这里也只能通过另一个线程来实现异步IO
//但记住,多线程是实现异步IO的一个手段而已,它不是必须的,后面会讲到如何通过一个线程来实现异步IO。
Thread thread = new Thread(() =>
{
try
{
int res = Http();
action(res);
}
catch
{
error();
}
});
thread.Start();
}
public static void OnMouse_DOWN(string msg)
{
HttpAsync(res =>
{
Console.WriteLine("请求成功!");
//使用该结果做一些工作
}, () =>
{
Console.WriteLine("请求发生错误!");
});
}
此时界面不再“假死”了,我们看下代码可读性,感觉还行,但是,如果再在回调函数里面再发起类似的异步请求呢?(有人可能有疑问,为什么还需要发起异步请求,我发同步请求不行吗?这都是在另一个线程里了。是的,在这个例子里是没问题的,但真实情况是,执行回调函数的代码,一般都会在UI线程,因为取得结果后需要更新相关UI组件上的界面,例如文字,而更新界面的操作都是放在UI线程里的,如何把回调函数放到UI线程上执行,这里不做讨论,在.NET中,这跟同步上下文(Synchronization context)有关,后面会讲到),那么代码会变成这样
public static void OnMouse_DOWN(string msg)
{
HttpAsync(res =>
{
Console.WriteLine("请求成功!");
//使用该结果做一些工作
HttpAsync(r1 =>
{
//使用该结果做一些工作
HttpAsync(r2 =>
{
//使用该结果做一些工作
}, () =>
{
});
}, () =>
{
});
}, () =>
{
Console.WriteLine("请求发生错误!");
});
}
写过JS的同学可能很清楚,这叫做“回调地狱”,如何解决这个问题?JS中有Promise,而C#中有Task,我们先用Task来写这一段代码,然后自己实现一个与Task功能差不多的简单的类库。
public static Task<int> HttpAsync()
{
return Task.Run(() =>
{
return Http();
});
}
public static void OnMouse_DOWN(string msg)
{
HttpAsync()
.ContinueWith(t =>
{
if(t.Status == TaskStatus.Faulted)
{
}else if(t.Status == TaskStatus.RanToCompletion)
{
//做一些工作
}
})
.ContinueWith(t =>
{
if (t.Status == TaskStatus.Faulted)
{
}
else if (t.Status == TaskStatus.RanToCompletion)
{
//做一些工作
}
})
.ContinueWith(t =>
{
if (t.Status == TaskStatus.Faulted)
{
}
else if (t.Status == TaskStatus.RanToCompletion)
{
//做一些工作
}
});
}
是不是感觉清爽了许多?这是编写异步代码的第一个跃进。下篇将会介绍,如何自己实现一个简单的Task。后面还会提到C#中async/await的本质作用,async/await是怎么跟Task联系起来的,怎么把自己写的Task库与async/await连结起来,以及一个线程如何实现异步IO。
觉得有收获的不妨点个赞,有支持才有动力写出更好的文章。
C#异步编程由浅入深(一)的更多相关文章
- C#异步编程由浅入深(二)Async/Await的作用.
考虑到直接讲实现一个类Task库思维有点跳跃,所以本节主要讲解Async/Await的本质作用(解决了什么问题),以及Async/Await的工作原理.实现一个类Task的库则放在后面讲.首先回顾 ...
- C#异步编程由浅入深(三)细说Awaiter
上一篇末尾提到了Awaiter这个类型,上一篇说了,能await的对象,必须包含GetAwaiter()方法,不清楚的朋友可以看上篇文章.那么,Awaiter到底有什么特别之处呢? 首先,从上 ...
- C#异步编程(一)
异步编程简介 前言 本人学习.Net两年有余,是第一次写博客,虽然写的很认真,当毕竟是第一次,肯定会有很多不足之处, 希望大家照顾照顾新人,有错误之处可以指出来,我会虚心接受的. 何谓异步 与同步相对 ...
- C#与C++的发展历程第三 - C#5.0异步编程巅峰
系列文章目录 1. C#与C++的发展历程第一 - 由C#3.0起 2. C#与C++的发展历程第二 - C#4.0再接再厉 3. C#与C++的发展历程第三 - C#5.0异步编程的巅峰 C#5.0 ...
- 关于如何提高Web服务端并发效率的异步编程技术
最近我研究技术的一个重点是java的多线程开发,在我早期学习java的时候,很多书上把java的多线程开发标榜为简单易用,这个简单易用是以C语言作为参照的,不过我也没有使用过C语言开发过多线程,我只知 ...
- 异步编程 In .NET
概述 在之前写的一篇关于async和await的前世今生的文章之后,大家似乎在async和await提高网站处理能力方面还有一些疑问,博客园本身也做了不少的尝试.今天我们再来回答一下这个问题,同时我们 ...
- C#异步编程(二)
async和await结构 序 前篇博客异步编程系列(一) 已经介绍了何谓异步编程,这篇主要介绍怎么实现异步编程,主要通过C#5.0引入的async/await来实现. BeginInvoke和End ...
- [.NET] 利用 async & await 的异步编程
利用 async & await 的异步编程 [博主]反骨仔 [出处]http://www.cnblogs.com/liqingwen/p/5922573.html 目录 异步编程的简介 异 ...
- [.NET] 怎样使用 async & await 一步步将同步代码转换为异步编程
怎样使用 async & await 一步步将同步代码转换为异步编程 [博主]反骨仔 [出处]http://www.cnblogs.com/liqingwen/p/6079707.html ...
随机推荐
- TypeScript Version 23 Design Patterns
TypeScript Version 23 Design Patterns TypeScript 设计模式 https://refactoring.guru/design-patterns/types ...
- TypeScript tuple 元组
TypeScript tuple 元组 元组类型允许您用固定数量的元素表示数组,这些元素的类型是已知的,但不必相同. "use strict"; /** * * @author x ...
- URLSearchParams & shape URL params
URLSearchParams https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams var paramsString = ...
- jest & code testing
jest jest & code testing https://jestjs.io/zh-Hans/ 24.9 https://jestjs.io/docs/zh-Hans/getting- ...
- Java & Maven & Spring & Spring Boot
Java & Maven & Spring & Spring Boot Spring Boot sb https://start.spring.io/ Spring 4 htt ...
- git in depth
git in depth git delete remote branch # Deleting remote branches in Git $ git push origin --delete f ...
- ubuntu无法连接有线网
问题描述: ubuntu下仅能连接无线网,不能连接有线网,在有线网的下面是没有选项可供连接. 解决方法: 编辑 /etc/network/interfaces 这个文件 将里面仅仅写两句话 auto ...
- AtCoder Regular Contest 113
比赛地址 A(暴力) 题目链接 题目: 给出\(K\),求出满足\(A\times B\times C\le K\)的\((A,B,C)\)对数 解析: 将C移动到等式右边,得到\(A\times B ...
- 2021-2-25:对于 Java MMAP,如何查看文件映射脏页,如何统计MMAP的内存大小?
我们写一个测试程序: public static void main(String[] args) throws Exception { RandomAccessFile randomAccessFi ...
- 谈一下hashMap中put是如何实现的?
源码: Hash(key):计算出key的hash值. put方法详解: 1.如果table数组为null或者table数组的长度为0,则调用resize()方法扩容并返回table数组.数组的长度为 ...