首页 > 其他分享 >理解同步异步与阻塞非阻塞——傻傻分不清楚的终极指南

理解同步异步与阻塞非阻塞——傻傻分不清楚的终极指南

时间:2024-08-04 21:17:05浏览次数:12  
标签:异步 调用 read 阻塞 分不清楚 print main

同步异步与阻塞非阻塞这两组概念在 IO 场景下非常常见,由于他们在表现出来的效果上很相似,很容易造成混淆和困扰,要想理清楚这两组概念首先需要认识到这两组概念强调的是不同维度的事。

同步异步强调的是两个操作之间的顺序关系,两个操作之间是有序的还是无序的;

阻塞与非阻塞强调的是一个调用发起后调用发起方的行为,是被动等待还是主动获得执行权

下面以 Python 代码为例介绍这几个概念。

同步关系与异步关系

因为同步异步强调的是两个操作之间的顺序关系,所以加上关系俩字更好理解和区分。

同步 "Synchronous" 这个词源自希腊语 "syn"(意为"一起")和 "chronos"(意为"时间"),它的字面意思是"在同一时间发生"。在通信和计算机领域中,“同步”则有两层含义,一个是"一起发生",另一个是"按顺序进行",这两层含义缺一不可,它意味着多个操作按照预定的顺序和时间协调进行,从而保持整体的一致性和协调性。

这里可以联想一下并发控制中为什么存在“同步互斥”这样的概念?目的就是为了协调多进程访问临界区时,必须等临界区中的 A 进程退出临界区后,B 进程才可以进入临界区执行,本质上是将并行(异步)关系变成了串行(同步)关系。

再回想一下 SQL 隔离级别中最高级串行化 Serializable 是不是更能理解了?同样是将并行(异步)关系变成串行(同步)关系。

同步关系 (Synchronous)

同步指的是某个操作 A 必须等待前一个操作 B 完成之后才能开始,也就是说 A 在 B 完成之前不会启动。

也可以描述为 A sync before B,意味着操作 A 在操作 B 之后按顺序执行,并且 A 必须等待 B 完成后才开始。

说白了同步意味着 A 和 B 之间的执行有先后顺序关系,中国有句古话:先穿袜子再穿鞋,先当孙子再当爷,讲述的就是这个道理。

同步例子,其中 task_Atask_B 是同步关系,只有 task_A 执行完了task_B 才能执行。

import time

def task_A():
    print("Task A started")
    # 模拟耗时操作
    time.sleep(2)
    print("Task A finished")

def task_B():
    print("Task B started")
    # 模拟耗时操作
    time.sleep(1)
    print("Task B finished")

# 同步执行:B 必须等待 A 完成
task_A()
task_B()

输出

Task A started
Task A finished
Task B started
Task B finished

异步关系 (Asynchronous)

在异步操作中,操作 A 不需要等待前一个操作 B 完成之后才能开始,A 和 B 可以同时进行,或者 A 可以在等待 B 的过程中执行其他操作。

可以描述为 A async with B 意味着操作 A 和操作 B 可以同时执行或 A 不需要等待 B 完成。

说白了 A 和 B 的执行没半毛钱关系,你在穿鞋的同时也可以喘气儿,先喘再穿还是先穿再喘甚至边穿边喘都可以,怎么喜欢怎么来,互不影响。

异步例子,task_Atask_B 同时执行,都不需要等待对方,各自爱怎么跑怎么跑。

import asyncio

async def task_A():
    print("Task A started")
    # 模拟耗时操作
    await asyncio.sleep(2)
    print("Task A finished")

async def task_B():
    print("Task B started")
    # 模拟耗时操作
    await asyncio.sleep(1)
    print("Task B finished")

# 异步执行:A 和 B 可以同时进行
async def main():
    await asyncio.gather(task_A(), task_B())

asyncio.run(main())

输出

Task A started
Task B started
Task B finished
Task A finished

阻塞调用与非阻塞调用

阻塞和非阻塞重点强调的是调用方在发出调用后的行为,为了更好的理解这一对儿概念,可以在阻塞和非阻塞后面加上“调用”俩字,变成阻塞调用和非阻塞调用。

阻塞调用 (Blocking)

阻塞调用发出后,调用方会挂起等待,当被调用方执行完成并返回结果后,调用方才会被唤醒并接到结果继续执行之后的操作。

说白了阻塞调用就是发出调用后傻等着,整个进程都等在调用发出这一行。

代码示例,下面代码中 blocking_operation 内部有一个耗时操作,main 函数中进行阻塞调用,blocking_operation 不返回就一直在这等。

import time

def blocking_operation():
    print("Starting blocking operation")
    time.sleep(2)  # 模拟耗时操作
    print("Blocking operation finished")

def main():
    print("Before blocking call")
    blocking_operation()  # 阻塞调用
    print("After blocking call")

main()

输出

Before blocking call
Starting blocking operation
Blocking operation finished
After blocking call

非阻塞调用 (Non-blocking)

非阻塞调用发出后,调用方不会挂起等待,而是立即返回,之后可以选择继续别的操作。被调用方在后台(可能以各种形式实现)处理原本的业务逻辑,处理完成后可以通过回调、信号等机制通知调用方。

说白了非阻塞调用就是发出调用后马上返回,无论能不能得到想要结果都义无反顾的返回,啪的一下很快啊。至于结果没拿到怎么办?可以循环重试啊。

代码示例,下面代码中 non_blocking_operation 中有一个耗时操作,但调用时以非阻塞方式调用,立刻返回并继续执行 main 函数后面内容而不是一直等待。

import asyncio

async def non_blocking_operation():
    print("Starting non-blocking operation")
    await asyncio.sleep(2)  # 模拟耗时操作
    print("Non-blocking operation finished")

async def main():
    print("Before non-blocking call")
    task = asyncio.create_task(non_blocking_operation())  # 非阻塞调用
    print("After non-blocking call")
    await task  # 等待任务完成

asyncio.run(main())

输出

Before non-blocking call
After non-blocking call
Starting non-blocking operation
Non-blocking operation finished

两两结合

现在说说这两组概念的两两结合,设想这样一个场景,在一个主流程 main 中希望调用 read 发起 IO 读取数据,根据 mainread 的顺序关系以及 main 发出调用后的状态可分为如下几种情况:

同步阻塞

同步意味着 main 只有在 read 完成后才能继续执行,同步意味着有序;

阻塞意味着只要 read 不返回则 main 就必须挂起等待。下面是一段示例:

import time

def read():
    print("read start")
    time.sleep(2)
    print("read finished")

    return "data"

def main():
    print("Before read")
    data = read()  # 同步阻塞
    print("After read:", data)

main()

输出

Before read
read start
read finish
After read: data

同步非阻塞

首先说结论这种模式很少有实际应用。

同步意味着 main 只有在 read 完成后才能继续执行,同步意味着有序;

非阻塞意味着 read 调用会马上返回所以 main 可以立刻获得 CPU 时间片得以继续执行,但由于 mainread 之间是同步关系,main 必须等待 read 真正完成后才能继续执行,那么 main 只能主动放弃执行进而等待类似回调机制的通知。

因为 main 已经获得了执行权但却又不真正执行,等同于浪费了 CPU 的调度和时间片,所以这种情况在实际应用中很少就不写例子了(实际上我没想到有什么典型的例子可以写)。

异步阻塞

首先还是说结论这种模式的应用也非常少。

异步意味着 mainread 的执行互不影响,相互之间并不存在谁要等谁的情况,可以各自愉快滴运行,异步意味着无序。

阻塞意味着 main 调用 read 后必须等待 read 的结果返回,实际上这也浪费了 mainread 之间的异步关系,本可以并行执行的,现在只能挂起等待,所以实际应用并不多,也没有特别好的例子可写的。

异步非阻塞

异步意味着 mainread 的执行互不影响,相互之间并不存在谁要等谁的情况,可以各自愉快滴运行,异步意味着无序。

非阻塞意味着 read 调用后可以马上返回,同时由于二者是异步关系,所以可以实现 mainread 各自都可以继续向下执行,并发效率是最高的。

import asyncio

async def read():
    print("read start")
    await asyncio.sleep(2)
    print("read finished")

    return "data"

async def main():
    print("Before read")
    task = asyncio.create_task(read())  # 异步非阻塞
    print("After read")
    data = await task
    print("Read data:", data)

asyncio.run(main())

输出

Before read
After read
read start
read finished
Read data: data

异步非阻塞的应用价值

曾几何时江湖上流传着一个名为 c10k 的问题,说的是服务器如何应对 10000 个网络连接的场景。这其中的主要矛盾是人民群众日益增长的互联网应用的需要与落后的服务器并发能力之间的矛盾,因为 fork 多进程模型在处理大量连接时资源消耗是非常严重的,通过增加服务器集群数量已经不能解决根本问题,迫切需要一种新的解决方案的出现,异步非阻塞就是在这样的背景下提出来的。

最早接触异步非阻塞是 Python 的 tornado 框架,记得当时 tornado 的官网上还有 c10k 问题的介绍,主打的就是一个支持高并发高性能的网络框架,可以完美应对 c10k,tornado 一度成为了 Python Web 领域高性能的代名词。

不过经过这么多年的发展,结合多路复用 IO 以及各种语言分别加入了异步编程特性,c10k 已经不再被视为一个问题,反而成为了高性能高并发技术的里程碑。

下面就以 Python 为例写一段代码,体现异步非阻塞的价值所在。

Python 在 3.5 版本之后引入了 async await 等一系列原生支持的协程语法,之前想要实现协程一般使用 yield。有了 async await 通过协程实现异步编程就简单多了。

这段代码使用 aiohttp 库实现了一个 http server,其中 handle 方法通过 sleep 模式执行一段 IO 操作,其中 time.sleep(5) 表示以同步方式执行,await asyncio.sleep(5) 表示以异步方式执行。

import aiohttp  
import asyncio  
import time  
  
async def handle(request):  
    # 用 sleep 模拟一个耗时的IO操作,下面分为同步和异步两种方式  
  
    # 同步 即事件循环与io是同步关系  
    # 事件循环需要等待io完成后才能获得执行权继续执行  
    # 在io没完成之前,事件循环是无法处理其他客户端的请求的  
    time.sleep(5)  
  
    # 异步 即事件循环与io是异步关系  
    # 事件循环和io操作是并行运行的  
    # 在io没完成之前,事件循环可以获得执行权去处理其他客户端的请求  
    # await asyncio.sleep(5)  
  
    return aiohttp.web.Response(text="Hello World!")  
  
  
app = aiohttp.web.Application()  
app.router.add_get('/', handle)  
  
if __name__ == '__main__':  
    aiohttp.web.run_app(app, host='0.0.0.0', port=8080)

启动服务

======== Running on http://0.0.0.0:8080 ========
(Press CTRL+C to quit)

再编写一个并发请求的脚本,可以同时发起 http 请求,观察请求执行时间可以看出,同步和异步两种方式的区别,其中 time 命令可以统计 curl 执行时间,输出的 real 表示耗时秒数。

time curl 127.0.0.1:8080 &  
time curl 127.0.0.1:8080 &

脚本启动后可以观察使用同步和异步两种方式的耗时的不同

# 同步
> ./time.sh
Hello World!
real	0m5.037s
user	0m0.008s
sys	    0m0.008s
Hello World!
real	0m10.049s
user	0m0.008s
sys	    0m0.010s

# 异步
> ./time.sh
Hello World!Hello World!
real	0m5.052s
real	0m5.049s
user	0m0.007s
user	0m0.006s
sys	    0m0.010s
sys	    0m0.012s

能看到同步方式下第一次请求耗时 5s 而第二次请求耗时 10s,也就相当于两个并发请求被串行化了。在异步方式下两次请求分别耗时 5s,互不影响。
异步非阻塞结合协程在高并发场景下,可以花费较少代价便能够支持大量网络连接,这是非常有价值的。

总结

想要彻底搞清楚同步和异步、阻塞和非阻塞,就要明确他们分别是从两个维度出发强调的不同概念。前者强调的是两个操作之间的顺序关系,后者强调的是调用方发出调用后的行为,搞清楚这两个维度才能够清晰的理清楚他们之间的关系。

标签:异步,调用,read,阻塞,分不清楚,print,main
From: https://www.cnblogs.com/caipi/p/18342212

相关文章

  • kettle从入门到精通 第八十二课 ETL之kettle kettle中的【阻塞数据直到步骤都完成】使
     1、在使用步骤【阻塞数据直到步骤都完成】(英文为Blockthisstepuntilstepsfinish)之前,我们先来了解下什么是 CopyNr? CopyNr是指 “副本编号” 或 “拷贝编号”,也就是下图中的复制的记录行数,图中的两个步骤复制的记录行数都是0,表示只有一个副本。 2、写日志步骤右......
  • 深入理解PHP8的新特性:如何高效使用异步编程和代码
    PHP8是PHP编程语言的最新主要版本,带来了许多令人兴奋的新特性和改进。其中最突出的特性之一是对异步编程的支持。异步编程允许我们在处理并发任务时提高性能和响应能力。本文将深入探讨PHP8的异步编程特性,并介绍如何高效地使用它们。首先,让我们了解一下什么是异步编程。在传统的......
  • golang 如从一个通道(channel)接收数据时在预期时间没接收到,可以使用select语句和time.A
    在Go语言中,如果希望在从一个通道(channel)接收数据时设置超时,可以使用select语句和time.After函数。以下是一个示例代码,演示了如何实现这个功能:packagemainimport("fmt""time")funcmain(){//创建一个通道ch:=make(chanstring)//启动一......
  • 为什么 spdlog 不在异步函数中打印
    importasynciofrompathlibimportPathimportspdlogasspdimportasyncioimportloggingasyncdefA():asyncio.create_task(B())whileTrue:awaitasyncio.sleep(1)asyncdefB():logger=spd.DailyLogger(name='B',filen......
  • Flask框架内容基础3 -- 使用redis实现异步任务队列
    前面所了解的所有请求都是同步的,那么当面临异步请求时,应该怎么做?调用者:携带参数发送请求API:接收请求并生成一个任务ID,接下来:返回给调用者+放到任务队列中worker:等待redis队列(List),一旦接收到任务,就执行并将结果返回到结果队列(Hash)调用者:等待n秒后,携带任务ID再次发送请求,获......
  • springboot 异步任务
    在主类开始任务packagecom.sugon.dataexchangeplatform;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.scheduling.annotation.EnableAsync;@EnableAsync//开启......
  • Python rocketMq 客户端的同步和异步模式
    同步模式fromrocketmq.clientimportPushConsumer,ConsumeStatusimporttimedefcallback(msg):print(msg.id,msg.body,msg.get_property('property'))returnConsumeStatus.CONSUME_SUCCESSdefstart_consume_message():consumer=PushCon......
  • JavaEE 初阶(11)——多线程9之“阻塞队列”
    目录一.什么是“阻塞队列”二.生产者消费者模型2.1概念2.2 组件 2.3实际应用2.4优点 a.实现“解耦合” b.流量控制——“削峰填谷”2.5代价a. 更多的机器b.通信时间延长三.阻塞队列的实现 3.1简述  3.2ArrayBlockingQueue的使用3.3实现MyA......
  • @Schedule定时任务和异步注解@Async时推荐自定义线程池
    1.原因@Schedule定时任务和异步注解@Async使用的默认线程池时, 池中允许的最大线程数和最大任务等待队列都是Integer.MAX_VALUE. 2.解决2.1、可以手动异步编排,交给某个线程池来执行。首先我们先向Spring中注入一个我们自己编写的线程池,参数自己设置即可,我这里比较随意。@C......
  • 【JavaEE】阻塞队列
    目录一.阻塞队列(BlockingQueue)1.什么是阻塞队列2.特性二.生产者消费者模型1.什么是生产者消费者模型?2.生产者消费模型的好处2.1解耦合  2.2削峰填谷三.如何在java中使用阻塞队列 四.模拟实现阻塞队列1.加锁2.阻塞等待实现3.解决interrupt唤醒waitting问题4......