Python异步编程并发比较之循环、进程、线程、协程

发布时间 2023-10-06 18:19:59作者: 金色旭光

image

服务端

现在有一个api接口 http://127.0.0.1:18081/hello 批量请求该接口,该接口中有一个5s的阻塞。使用循环,多进程,多线程,协程等四种方式,一共请求10次,比较总的请求耗时。

import time
from flask import Flask

app = Flask(__name__)


@app.route('/hello')
def hello_world():
    time.sleep(5)
    return "hello world"


if __name__ == '__main__':
    app.run(port=8090, host="0.0.0.0")

四种请求方法

请求函数

请求接口使用最常见好用的http请求包requests,三种请求方法使用同一个函数。
函数如下:

def blocking_way():
    res = requests.get("http://172.16.9.124:8090/hello")
    return res.content

循环

循环调用请求函数10次

# 同步
def sync_way():
    res = []
    for i in range(10):
        res.append(blocking_way())
    return len(res)

start = time.time()
res = sync_way()
print(res)
end = time.time()
print("**********sync************")
print(end-start)

结果:
50.0388023853302

多进程

开启10个进程并发请求函数

# 多进程
def process_way():
    workers = 10
    with futures.ProcessPoolExecutor(workers) as executor:
        futs = {executor.submit(blocking_way) for i in range(10)}

    return len([fut.result() for fut in futs])

start = time.time()
res = process_way()
end = time.time()
print("**************process***********")
print(end-start)

结果:
5.066945791244507

多线程

开启10个线程并发请求函数

# 多线程
def thread_way():
    worker = 10
    with futures.ThreadPoolExecutor(worker) as executor:
        futs = {executor.submit(blocking_way) for i in range(10)}

    return len([fut.result() for fut in futs])

start = time.time()
res = thread_way()
end = time.time()
print("**************threading***********")
print(end-start)

结果:
5.034665822982788

协程

开启10个协程

import aiohttp
import asyncio


async def fetch(url):
    async with aiohttp.ClientSession(loop=loop) as session:
        async with session.get(url) as response:
            response = await response.read()
            return response


if __name__ == "__main__":
    import time
    start = time.time()
    url = "http://127.0.0.1:8090/hello"
    loop = asyncio.get_event_loop()
    tasks = [fetch(url) for i in range(10)]
    res = loop.run_until_complete(asyncio.gather(*tasks))
    end = time.time()
    print(end-start)

结果:
5.018295049667358

耗时比较

并发类型 耗时 单位秒
循环 50.0388023853302
多进程 5.066945791244507
多线程 5.034665822982788
协程 5.018295049667358

分析

同步
每一次请求会阻塞5s,因为10个请求是按照顺序执行,所有一共阻塞50s左右

多进程
开启10个进程,每一个进程完成一次请求,请求之间是互相隔离的,10个请求不存在阻塞。理论上来说10个请求相当于1个请求,所以也就相当于1次请求的时间5s左右

多线程
多线程是一个进程中的并发,也就是说10次请求是在一个进程中完成的。由于GIL锁的存在,一个Python进程中,只允许有一个线程处于运行状态。

为什么线程结果还是如预期,耗时缩减到了十分之一?
因为python线程的调度机制。python遇到阻塞时当前线程会释放GIL,让别的线程有执行机会。所以一个线程执行到 requests.get 时让出GIL,下一个线程执行,这个过程就不存在阻塞。
当第一个让出GIL锁的线程下一次被调度到就有可能已经完成接口请求,下面就是执行剩下的逻辑。整个执行过程主要是阻塞的时间,业务逻辑耗时非常少,所以从10个请求整体来看是非阻塞的。

为什么进程的时间略多于线程呢?
因为进程切换时的上下文切换花费时间高于线程。
进程在上下文切换是需要保存当前进程的寄存器,内存状态,所以耗时比较长。而线程切换耗时较少,所以多线程略快于多进程。

协程
从结果来看,协程似乎是最快的。虽然这里数据量较少,但是从理论分析可以得知这样的结论:协程是用户态的并发,没有cpu调度,协作式的cpu机制比线程的cpu竞争机制要快,因为协程中cpu一直在用户态,没有发生切换,对比线程少了10次切换。

结论

由此可以看出在IO频繁的业务中适合用多线程、协程

对比

类型 特点 优点 缺点
同步 - 同步阻塞的网络交互方式,效率低十分低下
多进程 使用多个cpu核心执行任务 有效减少同步过程的时间阻塞 进程切换开销较大,由于内存资源的限制,一个任务开启的进程数有限
多线程 使用一个cpu核心开启多个线程执行 执行任务更加轻量级,支持数百到数千的数量规模。遇到阻塞任务自动让出GIL,可以有效解决阻塞 GIL让多核cpu同时只能有一个工作。调度策略是抢占式,需要业务控制
协程 一个线程下的并发,没有cpu切换 没有cpu调度,使用系统的事件通知,耗时最少 协程并发需要相应模块的支持,目前模块异步的支持较少