Fundamentals 23 min read

A Detailed Explanation of Asynchronous Programming

The article explains asynchronous programming by contrasting concurrency, parallelism, and synchronization, illustrates how splitting serial work into independent async tasks can improve performance but introduces resource, locking, and state‑tracking challenges, and offers strategies such as careful task limits, locking, queues, and result monitoring.

Tencent Cloud Developer
Tencent Cloud Developer
Tencent Cloud Developer
A Detailed Explanation of Asynchronous Programming

一、背景

业务中经常会有这样的场景:

二、几个名词的概念

并发:多个任务在同一个时间段内同时执行,如果是单核心计算机,CPU会不断地切换任务来完成并发操作。

并行:多任务在同一个时刻同时执行,计算机需要有多核心,每个核心独立执行一个任务,多个任务同时执行,不需要切换。

同步:多任务开始执行,任务A、B、C全部执行完成后才算是结束。

异步:多任务开始执行,只需要主任务A执行完成就算结束,主任务执行的时候,可以同时执行异步任务B、C,主任务A可以不需要等待异步任务B、C的结果。

并发、并行,是逻辑结构的设计模式。同步、异步,是逻辑调用方式。串行是同步的一种实现,就是没有并发,所有任务一个一个执行完成。并发、并行是异步的2种实现方式。

(三)思考问题

你找一家汽车托运公司,把2辆车一起托运到广州。这种方式是同步、异步,并发、并行的哪种情况呢?

(四)问题1:并发/并行执行会遇到的问题

假设:某个接口的并发请求会达到1万的qps,所以对接口的性能、响应时长都要求很高。

接口内部又有大量redis、mysql数据读写,程序中还有很多处理逻辑。如果接口内的所有逻辑处理、数据调用都是串行化,那么单个请求耗时可能会超过100ms,为了性能优化,就会把数据读取的部分与逻辑计算的部分分开来考虑和实现,能够独立的部分单独剥离出来作为异步任务来执行,这样就把串行化的耗时优化为并发执行,充分利用多核计算机的性能,减少单个接口请求的耗时。

假设的数据具体化,如:这个接口的数据全部是可以独立获取(支持并发),需要读取来自不同数据结构的redis共10个,读取不同数据表的数据共10个。那么一次请求,数据获取就会启动10个redis读取任务,10个mysql读取任务。每秒钟1万接口请求,会有10万个redis读取任务和10万个mysql读取任务。这21万的并发任务,在一秒钟内由16/32核的后端部署单机来完成,虽然在同一时刻的任务数量不一定会是21万(速度快的话会少于21万,如果处理速度慢,出现请求积压拥堵,会超过21万)。

这时候,会遇到的瓶颈。

内存,如果每个任务需要500k内存,那么210k*0.5M=210*0.5G=105G

CPU,任务调度,像golang的协程可能开销还小一些,如果是java的线程调度,操作系统会因为调度而空转。

网络,每次数据读取5k,那么200k*5k=200*5M=1G。

端口,端口号最多能分配出来65536个,明显不够用了。

据源,redis可以支持10万qps的请求,但是mysql就难以支持10万qps了。

上面可能出现的瓶颈中,通过计算机资源扩容可以解决大部分问题,比如:部署50个后端实例,每个实例只需要应对200的qps,压力就小了很多。对于数据源,mysql可以有多个slave来支持只读的请求。

但是,如果接口的并发量更大呢?或者某个/某些数据源读取出现异常,需要重试,或者出现拥堵,接口响应变慢,任务数量也就会出现暴增,后端服务的各方面瓶颈又会随之出现。

所以,我们需要特别注意和关心后端开启的异步任务数量,要做好异常情况的防范,及时中断掉拥堵/超时的任务,避免任务暴增导致整个服务不可用。

(五)问题2:共享数据的读写顺序和依赖关系

共享数据的并发读写,是并发编程中的老大难问题,如:读写脏数据,旧数据覆盖新数据等等。

而数据的依赖关系,也就决定了任务的执行先后顺序。

为了避免共享数据的竞争读写,为了保证任务的先后关系,就需要用到锁、队列等手段,这时候,并发的过程又被部分的拉平为串行化执行。

举例:NBA季后赛,去现场看球,要抢购球票,体育馆最多容纳1万人(1万张球票)。

体育馆不同距离、不同位置的票,价格和优惠都不相同。有单人位、有双人位,也有3、4人位。你约着朋友共10个人去看球,要买票,要选位置。这时候抢票就会很尴尬,因为位置连着的可能会被别人抢走,同时买的票越多,与人冲突的概率就越大,会导致抢票特别困难。

同时,这个系统的开发也很头大,抢购(秒杀)的并发非常大,预计在开始的一秒钟会超过10万人同时进来,再加上刷票的机器人,接口请求量可能瞬间达到100万的QPS。

那么,简单的实现方式:所有的请求都异步执行,订单全部进入消息队列,下单马上响应处理中,请等待。然后,后端程序再从消息队列中串行化处理每一个订单,把出现冲突的订单直接报错,这样,估计1秒钟可以处理1000个订单,10秒钟可以处理1万个订单。考虑订单的冲突问题,1万张球票的9000张可能在30秒内卖出去,此时只处理了3万个订单,第一秒钟进来的100万订单已经在消息队列中堆积,又有30秒钟的新订单进来,需要很久才可以把剩下的1000张球票卖出去啊。同理,下单的用户需要等待太久才知道自己的订单结果,这个过程轮询的请求也会很多很多。

换一种方案,不使用队列串行化处理订单,直接并发的处理每一个订单。那么处理流程中的数据都需要梳理清楚。

针对每一个用户的请求加锁,避免同一个用户的重入;

每一个/组座位预生成一个key:0,默认0说明没有下单;

预估平均每一个订单包含2个/组座位,需要更新2个座位key;

下单的时候给座位key执行INCR key数字递增操作,只有返回1的订单才是成功,其他都是失败;

如果同一个订单中的座位key有冲突的情况下,需要回滚成功key(INCR key=1)重置(SET key 0);

订单成功/失败,处理完成后,去掉用户的请求锁;

订单数据入库到mysql(消息队列,避免mysql成为瓶颈);

综上,需要用到1个锁(2次操作),平均2个座位key(每个座位号1-2次操作),这里只有2个座位key可以并发更新。为了让redis不成为数据读写的瓶颈(超过100w的QPS写操作),不能使用单实例模式,而要使用redis集群,使用由10-20个redis实例组成的集群,来支持这么高的redis数据读写。

算上redis数据读写、参数、异常、逻辑处理,一个请求大概耗时10ms左右,单核至少可以支持100并发,由于这里有大量IO处理,后端服务可以支持的并发可以更高些,预计单核200并发,16核就可以支持3200并发。总共需要支持100万并发,预计需要312台后端服务器。

这种方案比队列的方案需要的服务器资源更多,但是用户的等待时间很短,体验就好很多。

(六)状态处理:忽略结果

看下案例1的情况。

异步的日志上报,是否成功发送到服务端呢?

异步的指标数据上报,是否正确汇总统计和发送到服务端呢?

异步的任务,数据发送到消息队列,是否被后端应用程序消费呢?

服务端是否正常存储和处理完成呢?

如果因为网络原因,因为并发量太大导致服务负载问题,因为程序bug的原因,导致数据没能正确上报和处理,这时候的数据不一致、丢失的问题,就会难以及时排查和事后补发。

如果在本地完整记录一份数据,以备数据审查,又要考虑高并发高性能的瓶颈,毕竟本地日志读写性能受到磁盘速度的影响,性能会很差。

案例2:模板化创建服务的过程中,有创建代码仓库、开启日志采集和自定义镜像中心等耗时很长的任务。这里开启日志采集和自定义镜像中心如果出现异常,对整个服务的运行没有影响,而且开发者发现问题后也可以自己手动操作下,再次开启日志采集和自定义镜像功能。所以在模板化处理中,这些异步处理任务就没有关注任务的状态。

那么问题就很明显,模板化创建服务的过程中,是不能保证全部功能都正常执行完成的,会有部分功能可能有异常,而且也没有提示和后续指引。

当然模板化创建服务的程序,也可以把全部任务的状态都检查结果,只是会增加一些处理的复杂度和难度。

所以,对于异步任务的状态处理,需要关注结果的话,有两种主要的方法,分别是:轮询查询和等待回调。

轮询查询:用户在创建服务之后,浏览器会不断轮询服务端接口,看看创建服务的结果,各个步骤的处理结果,服务配置是否都成功完成了。

等待回调:主程序需要持续等待异步任务的回调,不能过早的退出。

轮询查询的局限性:1.频率和实时性,2.增加请求压力。

等待回调的局限性:1.等待超时,2.异常情况,3.回调地狱。

实际工作中,有遇到类似上面的两个案例吗?你会如何处理呢?所有的异步任务,都会检查状态结果吗?为什么呢?

异步编程,是人脑适配电脑,还是电脑服务人脑?

在大部分的编程中,大家都只需要考虑同步的方式来写代码逻辑。少部分时候,就要考虑使用异步的方式。而且,有很多的开发框架、类库已经把异步处理封装,可以简化异步任务的开发和调试工作。

所以,对于开发者来说,默认还是同步方式思考和开发,当不得不使用异步的时候,才会考虑异步的方式。毕竟让人脑适配电脑,这个过程还是有些困难的。

王毅

腾讯应用开发工程师

有丰富的系统设计和开发经验,做过信息管理系统、社区、电商、搜索等系统,现在参与奇点微服务云平台的相关设计和开发工作。

ConcurrencySynchronizationasynchronous programmingParallelismProgramming Concepts
Tencent Cloud Developer
Written by

Tencent Cloud Developer

Official Tencent Cloud community account that brings together developers, shares practical tech insights, and fosters an influential tech exchange community.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.