深入Asyncio(二)从线程到协程
线程的真相
多线程并不是一无是处,在实际问题中,要权衡优劣势来选择多线程、多进程或是协程。协程为多线程的某些问题提供了一种解决方案,所以学习协程首先要对线程有一定了解。
多线程优点
- 代码可读性
多线程代码即使是并发执行的,但依然可以线性阅读,可读性高。 - 共享内存
在多核CPU中仍然共享内存数据,这对解决某些问题很重要,避免了数据通信。 - 很容易对现有代码进行改造
有很多多线程编程的实例,也有很多阻塞程序依赖多线程的代码参考。
在Python中,由于GIL的存在,并行执行依然是不可能的(CPython解释器)。GIL带来的一个影响就是,所有的线程只能用到一个CPU核心,这几乎断绝了多线程并行的优势。
使用多线程的最佳方式就是使用concurrent.futures库的ThreadPoolExecutor(),将所有的数据丢到submit()方法中。
from concurrent.futures import ThreadPoolExecutor as Executor
def worker(data):
pass
with Executor(max_workers=10) as exe:
future = exe.submit(worker, data)
要关闭线程则执行Executor.shutdown(wait=True),它允许等待1-2秒来等线程执行完毕。对于上述例子,要尽可能地在worker()函数中不使用全局变量。
多线程的背景
为了文章的完整性,即使读者早就有所了解,在这里还是要补充:
1. 多线程编程中出现的bug是最难修复的bug,根据经验,设计一款新软件可能不容易出现这些问题,但对于一些现存软件的维护,即使专家也没办法解决这些问题;
2. 线程是资源密集型的,需要额外的系统资源来创建,如为每个线程预分配内存栈同时会提前消耗进程的虚拟内存,可以通过threading.stack_size([size])修改栈大小,但对于函数递归嵌套调用的栈深度有影响;
3. 在高并发环境中,由于上下文切换成本,会对吞吐量造成影响;
4. 多线程不够灵活,所有的线程共享CPU时间,而不管线程是否准备工作。
总之,多线程编程让代码bug难查,并且对于高并发场景并不高效。
例子:机器人与餐具
假设你开了一家餐馆,你的雇员都是机器人,现在就雇员与餐具构建一个简单的程序。
# Robot
import threading
from queue import Queue
class ThreadBot(threading.Thread): # 线程子类,继承start/join等方法
def __init__(self):
super().__init__(target=self.manage_table) # 目标函数,下面定义
self.cutlery = Cutlery(knives=0, forks=0) # 每个机器人携带的餐具
self.tasks = Queue() # 机器人接收的任务被添加到任务队列
def manage_table(self):
while True: # 机器人只接受三种工作
task = self.tasks.get()
if task == 'prepare table':
kitchen.give(to=self.cutlery, knives=4, forks=4)
elif task == 'clear table':
self.cutlery.give(to=kitchen, knives=4, forks=4)
elif task == 'shutdown':
return
# Cutlery
from attr import attrs, attrib # 开源库,不影响线程或协程,使实例属性的初始化更轻松
@attrs
class Cutlery:
knives = attrib(default=0)
forks = attrib(default=0)
def give(self, to: 'Cutlery', knives=0, forks=0): # 用于与其它实例交互
self.change(-knives, -forks)
to.change(knives, forks)
def change(self, knives, forks):
self.knives += knives
self.forks += forks
kitchen = Cutlery(knives=100, forks=100)
bots = [ThreadBot() for i in range(10)] # 创建了10个线程机器人
import sys
for bot in bots:
for i in range(int(sys.argv[1])): # 从命令行获取桌子的数量,然后给每个机器人安排所有桌子的任务
bot.tasks.put('prepare table')
bot.tasks.put('clear table')
bot.tasks.put('shutdown')
print(f'Kitchen inventory before service: {kitchen}')
for bot in bots: bot.start()
for bot in bots: bot.join()
print(f'Kitchen inventory after service: {kitchen}')
期望是经过程序运行后,所有的刀叉都应该回到厨房并且数量与初始一样。
λ python test.py 100
Kitchen inventory before service: Cutlery(knives=100, forks=100)
Kitchen inventory after service: Cutlery(knives=100, forks=100)
λ python test.py 10000
Kitchen inventory before service: Cutlery(knives=100, forks=100)
Kitchen inventory after service: Cutlery(knives=104, forks=80)
可以看到,在提供10000张桌子后,结果出现了严重的错误,实际上即使尝试多次都不乐观。我们知道这些机器人构造良好,也不会出现错误,那么是什么地方错了呢?
回忆下场景:代码简单易读,逻辑没错,甚至用100张桌子进行了测试,但10000张桌子测试就失败了,并且错误每次都不一样。
其实这是典型的竞态条件bug,错误出现在这一段代码中:
def change(self, knives, forks):
self.knives += knives
self.forks += forks
自加在C解释器中运行并不是原子的,它被分为几步:
1. 读取原变量(self.knives)值到临时变量;
2. 将knives值加到临时变量;
3. 将终值赋值给原变量。
抢占式多任务(多线程)会打乱上述步骤,可通过加锁的方式修复bug:
def change(self, knives, forks):
with self.lock:
self.knives += knives
self.forks += forks
但加锁需要了解多线程代码中,什么地方会发生数据共享,如果是个人写的程序比较好控制,但如果有第三方的代码夹杂进来,就会很头痛了。
光看源码很难找出有竞态条件,这主要是因为源码里没有指出何时何处切换线程,即使指出也没用,因为切换是由操作系统决定的,它可能发生在任何时间任何位置。
解决问题的一个办法就是让机器人不处理餐具,而是交由某一个单独的线程去处理。
不过在协程中,我们可以显式地知道上下文在何时切换,因为await关键字很显眼。
import asyncio
class CoroBot: # 单线程管理多个机器人实例
def __init__(self):
self.cutlery = Cutlery(knives=0, forks=0)
self.tasks = asyncio.Queue() # 使用异步队列
async def manage_table(self):
while True:
task = await self.tasks.get() # 关键点,协程唯一可以切换的位置
if task == 'prepare table':
kitchen.give(to=self.cutlery, knives=4, forks=4)
elif task == 'clear table':
self.cutlery.give(to=kitchen, knives=4, forks=4)
elif task == 'shutdown':
return
from attr import attrs, attrib
@attrs
class Cutlery:
knives = attrib(default=0)
forks = attrib(default=0)
def give(self, to: 'Cutlery', knives=0, forks=0):
self.change(-knives, -forks)
to.change(knives, forks)
def change(self, knives, forks):
self.knives += knives
self.forks += forks
kitchen = Cutlery(knives=100, forks=100)
bots = [CoroBot() for i in range(10)] # 创建了10个协程机器人,但它们是由一个线程管理的
import sys
for bot in bots:
for i in range(int(sys.argv[1])):
bot.tasks.put_nowait('prepare table') # 异步写入队列
bot.tasks.put_nowait('clear table')
bot.tasks.put_nowait('shutdown')
print(f'Kitchen inventory before service: {kitchen}')
loop = asyncio.get_event_loop()
tasks = [loop.create_task(bot.manage_table()) for bot in bots]
task_group = asyncio.gather(*tasks)
loop.run_until_complete(task_group)
print(f'Kitchen inventory after service: {kitchen}')
由于只有一个位置提供切换协程,因此在运行过程中不存在竞态条件,结果也是明显的,无论多少测试都能通过。
深入Asyncio(二)从线程到协程的更多相关文章
- 进程、线程、协程和GIL(二)
上一篇博客讲了进程.线程.协程和GIL的基本概念,这篇我们来说说在以下三点: 1> python中使用threading库来创建线程的两种方式 2> 使用Event对消来判断线程是否已启动 ...
- python并发编程之进程、线程、协程的调度原理(六)
进程.线程和协程的调度和运行原理总结. 系列文章 python并发编程之threading线程(一) python并发编程之multiprocessing进程(二) python并发编程之asynci ...
- 11.python3标准库--使用进程、线程和协程提供并发性
''' python提供了一些复杂的工具用于管理使用进程和线程的并发操作. 通过应用这些计数,使用这些模块并发地运行作业的各个部分,即便是一些相当简单的程序也可以更快的运行 subprocess提供了 ...
- Python学习之路--进程,线程,协程
进程.与线程区别 cpu运行原理 python GIL全局解释器锁 线程 语法 join 线程锁之Lock\Rlock\信号量 将线程变为守护进程 Event事件 queue队列 生产者消费者模型 Q ...
- Python—进程、线程、协程
一.线程 线程是操作系统能够进行运算调度的最小单位.它被包含在进程之中,是进程中的实际运作单位.一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务 方法: ...
- Queue、进程、线程、协程
参考博客地址 http://www.cnblogs.com/alex3714/articles/5230609.html 1.python GIL全局解释器锁 python调用的操作系统的原生线程,当 ...
- 11.python之线程,协程,进程,
一,进程与线程 1.什么是线程 线程是操作系统能够进行运算调度的最小单位.它被包含在进程之中,是进程中的实际运作单位.一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行 ...
- python中socket、进程、线程、协程、池的创建方式和应用场景
进程 场景 利用多核.高计算型的程序.启动数量有限 进程是计算机中最小的资源分配单位 进程和线程是包含关系 每个进程中都至少有一条线程 可以利用多核,数据隔离 创建 销毁 切换 时间开销都比较大 随着 ...
- python自动化开发-[第十天]-线程、协程、socketserver
今日概要 1.线程 2.协程 3.socketserver 4.基于udp的socket(见第八节) 一.线程 1.threading模块 第一种方法:实例化 import threading imp ...
随机推荐
- C#Url传递中文参数时解决方法
原文发布时间为:2008-11-05 -- 来源于本人的百度文章 [由搬家工具导入] 1.设置web.config文件。<system.web> <globalization req ...
- GC+JVM
1.内存管理模型 ①以对象的方式管理内存,每个对象占据内存中连续的一段,分配在堆中.对象引用可以指向堆中的其他对象.非基本数据类型的对象等价于数据引用. ②基于栈和堆的内存管理都是动态分配,即在运行时 ...
- django Modelform 使用
前言: 为什么要用form去验证呢? 我们提交的是form表单,在看前端源码时如果检查到POST URL及我们提交的字段,如果没有验证我们是否可以直接POST数据到URL,后台并没有进行校验,直接处理 ...
- 《手把手教你学C语言》学习笔记(3)---变量
编程目的是为了解决问题,编程本质是用计算机的思维操作数据,操作就是算法,数据主要是数据类型,也可以说量,其中分为常量和变量,常量主要是指在量的生命周期内无法改变其值:变量主要是指在量的生命周期内可以随 ...
- 解决dvajs使用BrowserHistory路由模式后仍然会出现hash(哈希)
在dvajs中,如果你在初始化dva对象的时候不作任何处理,那么你就会发现即使你是用了BrowserRouter来作为Router url中也是会出现#/.解决方法也很简单: 使用前先手动安装下 hi ...
- Educational Codeforces Round 34 D. Almost Difference【模拟/stl-map/ long double】
D. Almost Difference time limit per test 2 seconds memory limit per test 256 megabytes input standar ...
- 微信公众号开发C#系列-12、微信前端开发利器:WeUI
1.前言 通过前面系列文章的学习与讲解,相信大家已经对微信的开发有了一个全新的认识.后端基本能够基于盛派的第三方sdk搞定大部分事宜,剩下的就是前端了.关于手机端的浏览器的兼容性问题相信一直是开发者们 ...
- OS | 死锁
死锁的四个条件 互斥 占用等待 非剥夺 循环等待 死锁的解决方案 死锁预防 间接预防:防止前三个条件中的任何一个的发生 直接预防:防止循环等待的发生 死锁避免 进程启动拒绝:不启动任何一个可能发生死锁 ...
- Apache Openwhisk学习(一)
一.背景 最近中途参与的一个项目是和Serverless.Faas相关的,项目的整体架构和实现都参考了开源项目openwhisk,因此,同事们在编码时都会参考openwhisk的源码.因为以前从没有接 ...
- 聊聊、Zookeeper 客户端 Curator
[Curator] 和 ZkClient 一样,Curator 也是开源客户端,Curator 是 Netflix 公司开源的一套框架. <dependency> <groupI ...