为什么会有协程安全问题?

更多:并发异步编程之争:协程(asyncio)到底需不需要加锁?

1、案例

因为所有的协程都是在同一个线程里,所以即使最开始在await前拿到的counter=0,但只要同一线程里counter发生了变化,切换到新协程时,它拿的就是counter的最新值。

import asyncio
import time

# 一个简单的协程,模拟对共享数据的修改
async def increment(shared_data):
    task = asyncio.current_task()  # 获取当前正在执行的 Task 对象
    print(f"当前运行的Task name: {task.get_name()}")
    print(f"传来的shared_data={shared_data}")
    for _ in range(100):
        await asyncio.sleep(2)  
        # time.sleep(2)  
        task1 = asyncio.current_task()  # 获取当前正在执行的 Task 对象
        print(f"等待结束,Task name: {task1.get_name()}")
        shared_data['counter'] += 1
        print(f"累加后shared_data={shared_data}")

async def main():
    # 创建一个字典来存储共享数据
    shared_data = {'counter': 0}

    # 创建多个任务来修改共享数据
    tasks = [asyncio.create_task(increment(shared_data)) for _ in range(5)]

    # 等待所有任务完成
    await asyncio.gather(*tasks)

    # 打印最终的计数结果
    print(f"Expected counter value: 500")
    print(f"Actual counter value: {shared_data['counter']}")

# 运行事件循环
asyncio.run(main())

模拟同步操作:time.sleep(2)

同步阻塞,不会出现协程的让权了,但同一时刻仍然只有一个协程在执行,发现下一个协程必须等上一次协程全部跑完,所以输出是:
task1:counter从0递增到100
task2:counter从100递增到200
task3:counter从200递增到300

最终结果是500。

模拟异步操作: await asyncio.sleep(2)

发现最终的结果是500,但是中间状态下,每个task真正执行“shared_data[‘counter’] += 1”这行代码时的counter值并不都是从0开始!!!

输出结果举例:
task1 抢到执行权,counter = 0+1=1
task3 抢到执行权,counter = 1+1=2
task2 抢到执行权,counter = 2+1=3
task4 抢到执行权,counter = 3+1=4
task5 抢到执行权,counter = 4+1=5
task3 抢到执行权,counter = 5+1=6

模拟异步下加锁

发现异步加锁后,和同步阻塞的效果是一样的…

import asyncio
import time

# 一个简单的协程,模拟对共享数据的修改
async def increment(shared_data):
    task = asyncio.current_task()  # 获取当前正在执行的 Task 对象
    print(f"当前运行的Task name: {task.get_name()}")
    print(f"传来的shared_data={shared_data}")
    async with lock:
        for _ in range(100):
            await asyncio.sleep(2)  
            task1 = asyncio.current_task()  # 获取当前正在执行的 Task 对象
            print(f"等待结束,Task name: {task1.get_name()}")
            shared_data['counter'] += 1
            print(f"累加后shared_data={shared_data}")

async def main():
    # 创建一个字典来存储共享数据
    shared_data = {'counter': 0}

    # 创建多个任务来修改共享数据
    tasks = [asyncio.create_task(increment(shared_data)) for _ in range(5)]

    # 等待所有任务完成
    await asyncio.gather(*tasks)

    # 打印最终的计数结果
    print(f"Expected counter value: 500")
    print(f"Actual counter value: {shared_data['counter']}")

lock = asyncio.Lock()
# 运行事件循环
asyncio.run(main())

结论

  • 如果只关注最终的“500”是否正确,那协程在异步阻塞下是安全的;
  • 如果关注不管启动服务多少次,taski执行第j次时拿到的counter值,并根据这个中间状态做其他判断,那就是不安全的!!!
    比如,本例中,在模拟异步操作时,task3第一次抢到执行权得到counter=2,等到第二次抢到执行权得到counter=6。
    如果重新运行当前xx.py文件,那task3每次抢到执行权时的counter值都可能是不一样的!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值