概念
串行, 并行和并发
- 串行(serial): 一个CPU上, 按顺序完成多个任务
- 并行(parallelism): 指的是任务数小于等于CPU核数, 即任务同时执行
- 并发(concurrency): 一个CPU采用时间片管理方式, 交替的处理多个任务, 一般是任务数量多于CPU 核数, 通过操作系统的各种任务调度算法, 实现用多个任务同时执行(实际上存在部分任务不在执行, 只是因为切换任务的速度比较快, 看上去像同时执行)
进程, 线程和协程
-
线程是程序执行的最小单位, 而进程是操作系统分配资源的最小单位
-
一个进程由一个或多个线程组成, 线程是一个进程中代码的不同执行路线
-
进程之间相互独立, 但同一进程下的各个线程之间共享程序的内部空间(包括代码段, 数据集, 堆等)及一些进程级的资源(如打开文件和信号), 某进程中的线程在其他进程不可见
-
调度和切换: 线程上下文切换比进程上下文切换要快得多
-
进程(Process): 拥有自己独立的堆和栈, 既不共享堆, 也不共享栈, 进程由操作系统调度, 进程切换需要的资源最大, 效率低
-
线程(Thread): 拥有自己独立的栈和共享的堆, 共享堆, 不共享栈, 标准线程由操作系统调度; 在不考虑GIL的情况下, 线程切换需要的资源一般, 效率一般
-
协程(Coroutine): 拥有自己独立的栈和共享的堆, 共享堆, 不共享栈, 协程由程序里的协程的代码里实现调度; 协程切换任务需要的资源小, 效率高
进程
-
进程(Process)是一个具有一定独立功能的程序关于某个数据集合的一次运行活动
现代操作系统例如MaxOS, Linux, Windows等, 都是支持"多任务"的操作系统
多任务的解释: 操作系统可以同时运行多个任务, 打个比方: 你可以一边逛淘宝, 一边微信聊天, 一边听音乐
对于操作系统来说, 一个任务就是一个进程, 比如打开一个浏览器就是启动一个浏览器进程, 打开两个记事本就是启动两个记事本进程
线程
-
线程(Thread)是操作系统能够进行运算调度的最小单位, 它被包含在进程之中, 是进程中的实际运作单位
进程不等于只做一件事情, 例如微信, 它可以同时做视频聊天, 打字聊天, 逛朋友圈等事情
在一个进程内部, 要同时处理多个事情, 就需要同时运行多个子任务
于是我们把进程内的子任务称为"线程"
并发编程解决方案:
多任务实现的方式:
- 多进程模式
- 多线程模式
- 多进程+多线程模式
协程
协程(Coroutine), 也叫纤程(Fiber), 是一种在线程中比线程更加轻量级的存在, 可以由编写程序来进行管理
当出现IO阻塞时, CPU一直等待IO返回, 处于空转状态, 这种情况下, 可以利用协程执行其他任务
当IO返回结果时, 再回来处理数据, 充分利用IO等待的时间, 提高了效率
同步和异步通信
同步和异步强调的是消息通信机制(synchronous communication/asynchronous communication)
同步(synchronous): A调用B, 等待B返回结果后, A继续执行
异步(asynchronous): A调用B, A继续执行, 不等待B返回结果; B结果出来后, 通知A, A再做处理
线程
线程的创建方式(方法包装)
Python的标准库提供了两个模块: _thread和threading, _thread是低级模块, threading是高级模块, 对_thread进行了封装, 绝大多数情况下, 一般使用threading这个高级模块比较多
线程的创建有两种方式:
- 方法包装
- 类包装
线程的执行统一通过start()方法
代码演示:
from threading import Thread
from time import sleep
def func(name):
print(f"线程{name}, 启动线程...")
for i in range(3):
print(f"线程{name}--{i}")
sleep(2)
print(f"线程{name}, 结束线程...")
if __name__ == '__main__':
print("主线程开始ing...")
"""创建线程"""
t1 = Thread(target=func, args=("t1",))
t2 = Thread(target=func, args=("t2",))
"""启动线程"""
t1.start()
t2.start()
print("主线程结束ing...")
"""线程之间相互独立, 互不干扰(守护线程除外)"""
#############################################
# 主线程开始ing...
# 线程t1, 启动线程...
# 线程t1--0
# 线程t2, 启动线程...
# 线程t2--0
# 主线程结束ing...
# 线程t2--1线程t1--1
#
# 线程t1--2
# 线程t2--2
# 线程t2, 结束线程...
# 线程t1, 结束线程...
线程的创建方式(类包装)
代码演示:
from threading import Thread
from time import sleep
class MyThread(Thread):
def __init__(self, name):
Thread.__init__(self)
self.name = name
def run(self):
print(f"线程{self.name}, 启动线程...")
for i in range(3):
print(f"线程{self.name}--{i}")
sleep(2)
print(f"线程{self.name}, 结束线程...")
if __name__ == '__main__':
print("主线程开始ing...")
"""创建线程"""
t1 = MyThread("t1")
t2 = MyThread("t2")
"""启动线程"""
t1.start()
t2.start()
print("主线程结束ing...")
##############################
# 主线程开始ing...
# 线程t1, 启动线程...
# 线程t1--0
# 线程t2, 启动线程...
# 线程t2--0
# 主线程结束ing...
# 线程t2--1
# 线程t1--1
# 线程t2--2
# 线程t1--2
# 线程t1, 结束线程...
# 线程t2, 结束线程...
join()函数
之前的代码, 主线程不会等待子线程结束
如果需要等待子线程结束后, 再结束主线程, 可使用join()方法
from threading import Thread
from time import sleep
def func(name):
print(f"线程{name}, 启动线程...")
for i in range(3):
print(f"线程{name}--{i}")
sleep(2)
print(f"线程{name}, 结束线程...")
if __name__ == '__main__':
print("主线程开始ing...")
"""创建线程"""
t1 = Thread(target=func, args=("t1",))
t2 = Thread(target=func, args=("t2",))
"""启动线程"""
t1.start()
t2.start()
"""主线程会等待t1,t2结束线程后, 再往下继续执行"""
t1.join()
t2.join()
print("主线程结束ing...")
################################
# 主线程开始ing...
# 线程t1, 启动线程...
# 线程t1--0
# 线程t2, 启动线程...
# 线程t2--0
# 线程t2--1
# 线程t1--1
# 线程t1--2
# 线程t2--2
# 线程t2, 结束线程...
# 线程t1, 结束线程...
# 主线程结束ing...
守护线程
在行为上还有一种叫守护线程, 主要特征是它的生命周期, 主线程死亡, 它也随之死亡
在Python中, 线程通过setDaemon(True|False)来设置是否为守护线程
守护线程的作用: 守护线程作用是为其他线程提供便利服务, 守护线程最典型的应用就是GC(垃圾回收机制)
from threading import Thread
from time import sleep
class MyThread(Thread):
def __init__(self, name):
Thread.__init__(self)
self.name = name
def run(self):
print(f"线程{self.name}--启动线程...")
for i in range(3):
print(f"线程{self.name}--{i}")
sleep(2)
print(f"线程{self.name}, 结束线程...")
if __name__ == '__main__':
print("主线程开始ing...")
"""创建线程"""
t1 = MyThread("t1")
"""设置守护线程为True"""
t1.daemon = True
"""启动线程"""
t1.start()
# t1.setDaemon(True)已废弃方法
print("主线程结束ing...")
###############################
# 主线程开始ing...
# 线程t1--启动线程...
# 主线程结束ing...
全局锁GIL问题
引入
在Python中, 无论是多少核, 在CPython解释器中永远都是假象的, 这是Python设计之初开发的一个缺陷
Python中的线程是"含有水分的线程"
Python GIL(Global Interpreter Lock)
Python代码的执行由Python虚拟机(解释器主循环, CPython)来控制的, Python在设计之初就考虑到要在解释器的主循环中, 同时有一个线程在执行, 即在任意时刻, 只有一个线程在解释器中运行, 在Python虚拟机的访问由全局解释器锁(GIL)来控制, 正是这个锁能保证同一时刻只有一个线程在运行
GIL并不是Python的特性, 它是在实现Python解释器(CPython)时所引入的一个概念, 同样一段代码可以通过CPython, PyPy, Psyco等不同的Python运行环境来执行, 就没有GIL的问题, 然而CPython时大部分环境下默认的Python执行环境, 所以在很多人的概念里CPython就是Python, 也就自然而然的把GIL特点归结于Python语言的缺陷了
线程同步和互斥锁
同一个资源, 多线程都想使用, 怎么办? 排队解决
线程同步的概念
处理多线程问题时, 多个线程访问同一个对象, 并且某些线程还想修改这个对象, 这个时候需要用到"线程同步"
线程同步其实就是一种等待机制, 多个需要同时访问该对象的线程进入这个对象的等待池形成队列, 等待前面的线程使用完毕后, 下一个线程再使用
from threading import Thread
from time import sleep
# 未使用线程同步和互斥锁的情况
class Account:
def __init__(self, name, money):
self.name = name
self.money = money
class Drawing(Thread):
def __init__(self, drawing_num, account):
Thread.__init__(self)
self.drawing_num = drawing_num
self.account = account
self.expense_total = 0
def run(self):
if self.drawing_num > self.account.money:
return
sleep(1) # 判断可以取钱, 则阻塞, 这一句代码是为了测试发生冲突问题
self.account.money -= self.drawing_num
self.expense_total += self.drawing_num
print(f"账户: {self.account.name}, 余额: {self.account.money}")
print(f"账户: {self.account.name}, 合计取出: {self.expense_total}")
if __name__ == '__main__':
my_account = Account("usr001", 100)
first_drawing = Drawing(80, my_account) # 定义一个取钱的线程1
second_drawing = Drawing(80, my_account) # 定义一个取钱的线程2
first_drawing.start()
second_drawing.start()
####################################
# 账户: usr001, 余额: 20
# 账户: usr001, 余额: -60
# 账户: usr001, 合计取出: 80账户: usr001, 合计取出: 80
锁机制的特点
- 必须使用同一个对象
- 互斥锁的作用是保证同一时刻只能有一个线程去操作共享数据, 保证共享数据不会出现错误问题
- 使用互斥锁的好处是确保某段关键代码只能由一个线程从头到尾完整地执行
- 使用互斥锁会影响代码的执行效率
- 同时持有多把锁, 容易出现死锁的情况
互斥锁是什么
互斥锁: 对共享数据进行锁定, 保证同一时刻只能有一个线程去操作
注意: 互斥锁是多个线程一起去抢的, 抢到的线程先执行, 没有抢到的线程需要等待, 等互斥锁使用完释放后, 其他等待的线程再接着去抢这个锁
threading模块中定义了Lock变量, 这个变量本质上是一个函数, 通过调用这个函数可以获取一把互斥锁
from threading import Thread, Lock
from time import sleep
# 使用互斥锁的情况
class Account:
def __init__(self, name, money):
self.name = name
self.money = money
class Drawing(Thread):
def __init__(self, drawing_num, account):
Thread.__init__(self)
self.drawing_num = drawing_num
self.account = account
self.expense_total = 0
def run(self):
lock.acquire()
if self.drawing_num > self.account.money:
print("账户余额不足")
return
sleep(1) # 判断可以取钱, 则阻塞, 这一句代码是为了测试发生冲突问题
self.account.money -= self.drawing_num
self.expense_total += self.drawing_num
lock.release()
print(f"账户: {self.account.name}, 余额: {self.account.money}")
print(f"账户: {self.account.name}, 合计取出: {self.expense_total}")
if __name__ == '__main__':
my_account = Account("usr001", 100)
lock = Lock() # 创建互斥锁
first_drawing = Drawing(80, my_account) # 定义一个取钱的线程1
second_drawing = Drawing(80, my_account) # 定义一个取钱的线程2
first_drawing.start()
second_drawing.start()
####################################
# 账户: usr001, 余额: 20
# 账户: usr001, 合计取出: 80
# 账户余额不足
acquire和release方法之间的代码同一时刻只能有一个线程去操作
如果在调用acquire方法的时候, 其他线程已经使用了这个互斥锁, 那么此时acquire方法会阻塞, 直到这个互斥锁释放后才能再次上锁
线程死锁
在多线程程序中, 死锁问题很大一部分是由于一个线程同时获取多个锁造成的, 举例说明: 有两个人都要做饭, 都需要锅和菜刀才能炒菜
代码演示
from threading import Thread, Lock
from time import sleep
# 死锁演示
def func1():
lock1.acquire()
print("拿到第一个锁")
sleep(2)
lock2.acquire()
print("拿到第二个锁")
lock1.release()
print("释放第一个锁")
lock2.release()
print("释放第二个锁")
def func2():
lock2.acquire()
print("拿到第二个锁")
sleep(2)
lock1.acquire()
print("拿到第一个锁")
lock2.release()
print("释放第二个锁")
lock1.release()
print("释放第一个锁")
if __name__ == '__main__':
lock1 = Lock()
lock2 = Lock()
thread1 = Thread(target=func1)
thread2 = Thread(target=func2)
thread1.start()
thread2.start()
############################
#拿到第一个锁
#拿到第二个锁
解决方法:
死锁是由于"同步块需要持有多个锁造成"的, 要解决这个问题, 思路很简单, 就是: 同一个代码块, 不要同时持有两个对象锁
信号量
信号量控制同时访问资源的数量, 信号量和锁相似, 锁同一时间只允许一个对象(进程)通过, 信号量同一时间允许多个对象(进程)通过
应用场景
- 在读写文件的时候, 一般只能只有一个线程在写, 而读可以有多个线程同时进行, 如果需要限制同时读文件的线程个数, 这时候就可以使用到信号量(如果是互斥锁的话, 就是限制同一时刻只能有一个线程读取文件)
- 爬虫爬取数据
底层原理
信号量底层是一个内置的计数器, 每当资源获取时(调用acquire)计数器-1, 资源释放时(调用acquire)计数器+1
代码演示
import time
from threading import Thread, Semaphore
# 信号量-代码演示
def demo(name, se):
se.acquire()
print(f"###{name}开始线程")
time.sleep(3)
print(f"***{name}结束线程")
se.release()
if __name__ == '__main__':
se = Semaphore(2) # 信号量对象
for i in range(5):
t = Thread(target=demo, args=(f"thread{i}", se))
t.start()
#########################
# ###thread0开始线程
# ###thread1开始线程
# ***thread1结束线程***thread0结束线程
#
# ###thread2开始线程
# ###thread3开始线程
# ***thread2结束线程
# ###thread4开始线程***thread3结束线程
#
# ***thread4结束线程
事件(Event)
事件Event主要用于唤醒正在阻塞等待状态的线程
原理
Event对象包含一个由线程设置的信号标志, 它允许线程等待某个事件的发生, 在初始情况下, event对象中的信号标志被设置为假, 如果有线程等待一个event对象, 而这个event对象的标志为假, 那么这个线程将会被一直阻塞直至该标志为真, 一个线程如果将一个event对象的信号标志设置为真, 它将唤醒所有等待这个event对象的线程, 如果一个线程等待一个已经被设置为真的event对象, 那么它将忽略这个事件, 继续执行
Event()可以创建一个事件管理标志, 该标志(event)默认为False, event对象主要有四种方法可以调用
| 方法名 | 说明 |
|---|---|
| event.wait(timeout=None) | 调用该方法的线程会被阻塞, 如果设置了timeout参数, 超时后, 线程会停止阻塞继续执行 |
| event.set() | 将event的标志设置为True, 调用wait方法的所有线程将被唤醒 |
| event.clear() | 将event的标志设置为False, 调用wait方法的所有线程将被阻塞 |
| event.is_set() | 判断event的标志是否为True |
代码演示:
import time
from threading import Thread, Event
def event_wait(name):
# 等待事件, 进入事件阻塞状态
print(f"{name}线程启动")
print(f"{name}进入事件等待状态")
time.sleep(1)
event.wait()
# 收到通知, 结束事件阻塞状态, 继续运行
print(f"{name}收到主线程的通知")
print(f"{name}结束阻塞状态, 继续运行")
if __name__ == '__main__':
event = Event()
# 创建线程
thread1 = Thread(target=event_wait, args=("thread1",))
thread2 = Thread(target=event_wait, args=("thread2",))
# 启动线程
thread1.start()
thread2.start()
time.sleep(5)
# 发送事件通知
print(">>>主线程通知其他线程中...")
event.set()
########################
# thread1线程启动
# thread1进入事件等待状态
# thread2线程启动
# thread2进入事件等待状态
# >>>主线程通知其他线程中...
# thread2收到主线程的通知
# thread2结束阻塞状态, 继续运行
# thread1收到主线程的通知
# thread1结束阻塞状态, 继续运行
生产者和消费者模式
多线程并发协作模式
- 生产者指的是负责生产数据的模块(可以是方法/对象/线程/进程)
- 消费者指的是负责处理数据的模块(可以是方法/对象/线程/进程)
- 缓冲区是的是消费者不能直接使用生产者的数据, 他们之间存在一个缓冲区, 生产者会把生产的数据放入缓冲区, 消费者从缓冲区拿到需要处理的数据
缓冲区是实现并发的核心, 缓冲区的设置有3个好处
-
实现线程的并发协作
有了缓冲区, 生产者线程只需要往缓冲区里面放置数据, 不需要了解消费者的数据处理情况, 同样, 消费者只需要从缓冲区拿处理处理即可, 也不需要了解生产者的数据生产情况, 这样从逻辑上实现了生产者线程和消费者线程的分离
-
解耦了生产者和消费者
生产者不需要跟消费者直接打交道
-
解决忙闲不均, 提高效率
生产者生产数据慢时, 缓冲区仍有数据, 不影响消费者处理数据; 消费者处理数据慢时, 不影响生产者往缓冲区放入数据
缓冲区和queue对象
从一个线程向另一个线程发送数据最安全的方式可能就是使用Queue库中的队列了, 创建一个被多个线程共享的Queue对象, 这些线程通过使用put()和get()操作来向队列中添加或者删除元素, Queue对象已经包含了必要的锁, 所以你可以通过它在多个线程之间安全共享数据
代码演示:
from time import sleep
from threading import Thread
from queue import Queue
def producer():
item = 0
while True:
if my_queue.qsize() < 5:
item += 1
print(f"生产1条数据{item}")
my_queue.put(f"数据{item}")
else:
print("缓冲区已满, 数据生成已暂停")
break
sleep(2)
def consumer():
while True:
print(f"处理{my_queue.get()}中...")
sleep(2)
print(">>>处理成功")
sleep(2)
if __name__ == '__main__':
my_queue = Queue()
thread_pro = Thread(target=producer)
thread_con1 = Thread(target=consumer)
thread_con2 = Thread(target=consumer)
thread_pro.start()
thread_con1.start()
thread_con2.start()
进程
进程的概念
进程(Process): 拥有自己独立的堆和栈, 既不共享堆, 也不共享栈, 进程由操作系统调度, 进程切换需要的资源很大, 效率低, 对于操作系统来说, 一个任务就是一个进程, 比如打开一个浏览器就是启动一个浏览器进程, 打开两个记事本就是启动两个记事本进程
进程的优缺点
- 优点:
- 可以使用计算机多核, 进行任务的并发执行, 提高执行效率
- 运行不受其他进程的影响, 创建方便
- 空间独立, 数据安全
- 缺点:
- 进程的创建和删除消耗的系统资源比较多
进程的创建
Python的标准库提供的模块: multiprocessing
进程的创建可以通过两种方式:
- 方法包装
- 类包装
创建进程后, 使用start()启动进程
代码演示(方法包装):
from multiprocessing import Process
import os
from time import sleep
# 方法包装-多进程实现
def process_create(item):
print(f"<进程{item}, start...>")
print(">>>当前进程ID:", os.getpid())
print(">>>当前父进程ID:", os.getppid())
sleep(3)
print(f"<进程{item}, end...>")
if __name__ == '__main__':
print("当前主进程ID:", os.getpid())
# 创建进程
process_1 = Process(target=process_create, args=("process_1",))
# 启动进程
process_1.start()
process_2 = Process(target=process_create, args=("process_2",))
process_2.start()
############################
# 当前主进程ID: 9936
# <进程process_1, start...>
# >>>当前进程ID: 10156
# <进程process_2, start...>
# >>>当前进程ID: 6692
# >>>当前父进程ID: 9936
# >>>当前父进程ID: 9936
# <进程process_1, end...>
# <进程process_2, end...>
进程的创建方式(Process类)
和使用Thread类创建子线程的方式非常类似, 使用Process类实例化对象, 其本质是调用该类的构造方法创建新进程, Process类的构造方法格式如下:
def __init__(self, group=None, target=None, name=None, args=(), kwargs=())
其中:
- group: 该参数未进行实现, 不需要传参
- target: 为新建进程执行执行任务, 也就是指定一个函数
- name: 为新建进程设置名称
- args: 为target参数指定的参数传递非关键字参数
- kwargs: 为target参数指定的参数传递关键字参数
代码演示:
from multiprocessing import Process
from time import sleep
class MyProcess(Process):
def __init__(self, name):
Process.__init__(self)
self.name = name
def run(self):
print(f">>>进程:{self.name} starting...")
sleep(3)
print(f">>>进程:{self.name} ending...")
if __name__ == '__main__':
process_1 = MyProcess("process_1")
process_1.start()
process_2 = MyProcess("process_2")
process_2.start()
#########################
# >>>进程:process_1 starting...
# >>>进程:process_2 starting...
# >>>进程:process_1 ending...
# >>>进程:process_2 ending...
Queue实现进程间通信
进程间的通信需要使用到multiprocessing模块中的Queue类
简单的理解Queue实现进程间通信的方式, 就是使用了操作系统开辟了一个队列空间, 各个进程可以把数据放入该队列中, 同时也可以把队列中需要的数据取走
代码演示:
from multiprocessing import Process, Queue
from time import sleep
class TestQueue(Process):
def __init__(self, name, item_queue):
Process.__init__(self)
self.name = name
self.item_queue = item_queue
def run(self):
print(f"{self.name} starting...")
print(f"get new_data:{self.item_queue.get()}")
sleep(3)
self.item_queue.put(f"put new_data:{self.name}")
print(f"{self.name} ending...")
if __name__ == '__main__':
my_queue = Queue()
item_list = []
for i in range(3):
my_queue.put(f"item{i}")
for i in range(3):
item_process = TestQueue(name=f"process_{i}", item_queue=my_queue)
item_list.append(item_process)
for i in item_list:
i.start()
for i in item_list:
i.join()
print(">>>", my_queue.get())
print(">>>", my_queue.get())
print(">>>", my_queue.get())
############################
# process_0 starting...
# get new_data:item0
# process_2 starting...
# get new_data:item1
# process_1 starting...
# get new_data:item2
# process_0 ending...process_1 ending...
# process_2 ending...
#
# >>> put new_data:process_0
# >>> put new_data:process_1
# >>> put new_data:process_2
Pipe实现进程间通信
multiprocessing模块中的Pipe类可以实现进程间的通信
Pipe方法返回(conn1, conn2)代表一个管道的两个端
Pipe方法有duplex参数, 如果duplex参数为True(默认值), 那么这个参数是全双工模式, 代表conn1和conn2均可收发, 如果duplex参数为False, 那么conn1只负责接收消息, conn2只负责发送消息, send和recv方法分别是发送和接收消息的方法
import multiprocessing
from multiprocessing import Process, Pipe
from time import sleep
def conn1_func(conn):
send_info = "Hello World!"
print(f"current_process_id: {multiprocessing.current_process().pid} >>> send data: {send_info}")
conn.send(send_info)
sleep(2)
print(f"current_process_id: {multiprocessing.current_process().pid} >>> receive data: {conn.recv()}")
def coon2_func(conn):
send_info = "Python!"
print(f"current_process_id: {multiprocessing.current_process().pid} >>> send data: {send_info}")
conn.send(send_info)
sleep(2)
print(f"current_process_id: {multiprocessing.current_process().pid} >>> receive data: {conn.recv()}")
if __name__ == '__main__':
# 创建管道
conn1, conn2 = Pipe(duplex=True)
process_1 = Process(target=conn1_func, args=(conn1,))
process_2 = Process(target=coon2_func, args=(conn2,))
process_1.start()
process_2.start()
###########################
# current_process_id: 14868 >>> send data: Hello World!
# current_process_id: 2148 >>> send data: Python!
# current_process_id: 14868 >>> receive data: Python!
# current_process_id: 2148 >>> receive data: Hello World!
Manager管理器
管理器提供了一种创建共享数据的方法, 从而可以在不同进程中共享
from multiprocessing import Process, Manager
def manager_func(m_list, m_dict):
m_list.append("Python")
m_dict["name"] = "witcher"
if __name__ == '__main__':
with Manager() as mgr:
mgr_list = mgr.list()
mgr_dict = mgr.dict()
mgr_list.append("Hello, World")
process_1 = Process(target=manager_func, args=(mgr_list, mgr_dict,))
process_1.start()
process_1.join()
print(mgr_list)
print(mgr_dict)
# ['Hello, World', 'Python']
# {'name': 'witcher'}
进程池(Pool)
Python提供了更好的管理多个进程的方式, 就是使用进程池(from multiprocessing import Pool)
进程池可以提供指定数量的进程给用户使用, 即当有新的请求提交到进程池中时, 如果池未满, 则会创建一个新的进程用来执行该请求, 反之, 如果池中的进程数已经达到规定最大值, 那么该请求就会等待, 只要池中有进程空闲下来, 该进程就能得到执行
使用进程池的优点:
- 提高效率, 节省开辟进程和开辟内存空间的时间及销毁进程的时间
- 节省内存空间
| 类/方法 | 功能 | 参数 |
|---|---|---|
| Pool(processes) | 创建进程池对象 | processes表示进程池中有多少进程 |
| pool.apply_async(func, args, kwds) | 异步执行, 将事件放入进程池队列 | func: 事件函数, args: 以元组形式给func传参, kwds: 以字典形式给func传参; 返回值: 返回一个代表进程池事件的对象, 通过返回值的get方法可以得到事件函数的返回值 |
| pool.apply(func, args, kwds) | 同步执行, 将事件放入进程池队列 | 同上 |
| pool.close() | 关闭进程池 | |
| pool.join() | 回收进程池 | |
| pool.map(func, iter) | 类似python的map函数, 将要做的事件放入进程池 | func: 要执行的函数, iter: 迭代对象 |
代码演示1:
import multiprocessing
from multiprocessing import Pool
from time import sleep
def pool_func(name):
print(f"current_process_id: {multiprocessing.current_process().pid}, name is {name}")
sleep(2)
return name
def callback_func(item):
print(f"<<<{item}>>>")
if __name__ == '__main__':
pools = Pool(3)
# callback为回调函数
pools.apply_async(func=pool_func, args=("process_1",), callback=callback_func)
pools.apply_async(func=pool_func, args=("process_2",))
pools.apply_async(func=pool_func, args=("process_3",))
pools.apply_async(func=pool_func, args=("process_4",))
pools.apply_async(func=pool_func, args=("process_5",))
pools.close()
pools.join()
##############################################
# current_process_id: 648, name is process_1
# current_process_id: 18108, name is process_2
# current_process_id: 26052, name is process_3
# <<<process_1>>>
# current_process_id: 648, name is process_5
# current_process_id: 26052, name is process_4
代码演示2:
import multiprocessing
from multiprocessing import Pool
from time import sleep
def pool_func(name):
print(f"current_process_id: {multiprocessing.current_process().pid}, name is {name}")
sleep(2)
return name
if __name__ == '__main__':
with Pool(3) as pools:
p_list = pools.map(func=pool_func, iterable=("process_1", "process_2", "process_3", "process_4", "process_5"))
for item in p_list:
print(item)
##############################
# current_process_id: 11588, name is process_1
# current_process_id: 7416, name is process_2
# current_process_id: 15844, name is process_3
# current_process_id: 11588, name is process_4
# current_process_id: 15844, name is process_5
# process_1
# process_2
# process_3
# process_4
# process_5
协程
协程(Coroutine), 也叫纤程(Fiber)
协程, 全程是"协同程序", 用来实现任务协作, 是一种在线程中比线程更加轻量级的存在, 可以通过编写程序来进行管理
当出现IO阻塞时, CPU一直等待IO返回, 处于空转状态, 这种情况下, 可以利用协程执行其他任务
当IO返回结果时, 再回来处理数据, 充分利用IO等待的时间, 提高了效率
协程的概念
协程的核心
- 每个协程有自己的执行栈, 可以保存自己的执行现场
- 可以由用户程序根据需要来创建协程(比如, 遇到IO阻塞操作)
- 协程主动让出(yield)执行权时, 会保存执行现场(保存中断时的寄存器上下文和栈), 然后切换到其他协程
- 协程恢复(resume)执行时, 根据之前保存的执行现场恢复到中断前的状态, 继续执行, 如此往复, 通过协程实现了轻量级的用户态调度的多任务模型
协程的优点
-
由于自身带有上下文和栈, 无需线程上下文切换的开销, 属于程序级别的切换, 操作系统完全感知不到, 因而更加轻量级
-
无需原子操作的锁定及同步的开销
-
方便切换控制流, 简化编程模型
-
单线程内可以实现并发的效果, 最大限度地利用cpu, 且可扩展性高, 成本低
asynicio协程时写爬虫比较好的方式, 比多线程和多进程都好, 开辟新的线程和进程时非常耗时的
协程的缺点
- 无法利用多核资源, 协程的本质是个单线程, 不能同时将单个CPU的多个核使用上, 协程需要和进程配合才能运行在多CPU上
使用yield实现协程(已淘汰, 了解即可)
python协程的发展历程
- 最初的生成器变形yield/send
- 引入@asyncio.coroutine和yield from
- Python3.5版本后, 引入async/await关键字
代码演示:
from time import time, sleep
def func_1():
for i in range(3):
print(f"A: 第{i}次打印")
yield # 只要方法包含了yield, 就变成一个生成器
sleep(1)
def func_2():
g = func_1() # func_1是一个生成器, func()不会直接调用, 需要通过next()
print(type(g))
for i in range(3):
print(f"B: 第{i}次打印")
next(g) # 继续执行func_1的代码
sleep(1)
if __name__ == '__main__':
# 通过yield实现两个任务的切换和保存状态
start_time = time()
func_2()
end_time = time()
print(f"耗时{end_time - start_time}")
##########################
# <class 'generator'>
# B: 第0次打印
# A: 第0次打印
# B: 第1次打印
# A: 第1次打印
# B: 第2次打印
# A: 第2次打印
# 耗时5.0404956340789795
asyncio实现协程(重点)
- 正常的函数执行时是不会中断的, 所以需要写一个中断的函数, 需要加上async
- async用来声明一个函数是异步函数, 异步函数的特点是能在函数执行过程中挂起, 去执行其他异步函数, 等到挂起条件消失后再回来执行
- await用来声明程序挂起, 比如异步程序执行到某一步需要等待的时间较长, 可以就此挂起, 执行其他的异步程序
- asyncio是python3.5之后的协程模块, 是python实现并发重要的包, 这个包使用事件循环驱动实现并发
代码演示:
import asyncio
from time import time
# async表示方法是异步的
async def func_1():
for i in range(3):
print(f"A: 第{i}次打印")
# await异步执行func_1方法
await asyncio.sleep(1)
return "func_1执行完毕"
async def func_2():
for i in range(3):
print(f"B: 第{i}次打印")
await asyncio.sleep(1)
return "func_2执行完毕"
async def main():
res = await asyncio.gather(func_1(), func_2())
print(res)
if __name__ == '__main__':
start_time = time()
asyncio.run(main())
end_time = time()
print(f"耗时{end_time - start_time}")
##################
# A: 第0次打印
# B: 第0次打印
# A: 第1次打印
# B: 第1次打印
# A: 第2次打印
# B: 第2次打印
# 耗时3.041104555130005