并发编程
1 死锁
操作锁,抢锁之后需要释放锁,可能会产生死锁问题,导致程序卡住。
from threading import Thread, Lock
import time
# 使用互斥锁
mutexA = Lock()
mutexB = Lock()
class CustomThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire()
print('{name} 抢到锁A。'.format(name=self.name))
mutexB.acquire()
print('{name} 抢到锁B。'.format(name=self.name))
mutexB.release()
print('{name} 释放锁B。'.format(name=self.name))
mutexA.release()
print('{name} 释放锁A。'.format(name=self.name))
def func2(self):
mutexB.acquire()
print('{name} 抢到锁B。'.format(name=self.name))
time.sleep(2)
mutexA.acquire()
print('{name} 抢到锁A。'.format(name=self.name))
mutexA.release()
print('{name} 释放锁A。'.format(name=self.name))
mutexB.release()
print('{name} 释放锁B。'.format(name=self.name))
if __name__ == '__main__':
for i in range(5):
t = CustomThread()
t.start()
'''
Thread-1 抢到锁A。
Thread-1 抢到锁B。
Thread-1 释放锁B。
Thread-1 释放锁A。
Thread-1 抢到锁B。
Thread-2 抢到锁A。
'''
首先线程1顺利执行func1,之后执行func2抢到锁B,继续执行需要锁A;
线程2通过func1抢到锁A,继续执行需要锁B;
此时两个线程分别拥有锁A和锁B,但需要对方的锁才能继续执行,因此都在等待对方释放锁,
因此程序处于阻塞状态。
2 递归锁
互斥锁抢到锁后在释放这个锁之前,无法再次抢这个锁。
递归锁(Reentrant Lock)
第一个抢到递归锁的线程/进程可以进行连续地抢锁(acquire)和释放锁(release),但是抢到递归锁的进程/线程完全释放这把锁之前,其它进程/线程无法抢到这把锁。
递归锁内部存在一个计数器,抢到锁后计数器加1,释放锁后计数器减1,只要计数器不为0,其它进程/线程都无法抢到这把锁。
将上面例子中的互斥锁换成递归锁,解决了死锁问题。
from threading import Thread, RLock
import time
# 使用递归锁
mutexA = mutexB = RLock()
class CustomThread(Thread):
def run(self):
self.func1()
self.func2()
def func1(self):
mutexA.acquire()
print('{name} 抢到锁A。'.format(name=self.name))
mutexB.acquire()
print('{name} 抢到锁B。'.format(name=self.name))
mutexB.release()
print('{name} 释放锁B。'.format(name=self.name))
mutexA.release()
print('{name} 释放锁A。'.format(name=self.name))
def func2(self):
mutexB.acquire()
print('{name} 抢到锁B。'.format(name=self.name))
time.sleep(2)
mutexA.acquire()
print('{name} 抢到锁A。'.format(name=self.name))
mutexA.release()
print('{name} 释放锁A。'.format(name=self.name))
mutexB.release()
print('{name} 释放锁B。'.format(name=self.name))
if __name__ == '__main__':
for i in range(5):
t = CustomThread()
t.start()
3 信号量
信号量在不同的领域/阶段表示不同的技术点。
在并发编程中,信号量指的是锁。
from threading import Thread, Semaphore
import time
import random
sm = Semaphore(5) # 参数代表锁的数量
def task(name):
sm.acquire()
print('{name} 抢到锁。'.format(name=name))
time.sleep(random.randint(1, 3))
sm.release()
print('{name} 释放锁。'.format(name=name))
if __name__ == '__main__':
for i in range(20):
t = Thread(target=task, args=('任务{i}'.format(i=i),))
t.start()
4 Event事件
一些进程/线程必须等待其它进程/线程发出信号后才能开始运行。
from threading import Event
e = Event()
e.set()
e.wait()
from threading import Thread, Event
import time
e = Event() # 事件对象
def get_signals():
print('红色')
time.sleep(1)
print('绿色')
e.set() # 发送消息
def run(name):
print('{name}停止。'.format(name=name))
e.wait() # 等待消息
print('{name}开始。'.format(name=name))
if __name__ == '__main__':
t = Thread(target=get_signals)
t.start()
for i in range(10):
t = Thread(target=run, args=('任务{i}'.format(i=i),))
t.start()
5 线程q
同一个进程下的多个线程数据共享。
多个线程操作数据可能导致数据不安全,使用队列是因为 队列 = 管道 + 锁,使用队列是为了保证数据安全。
- Queue
先进先出Queue
import queue
q = queue.Queue(3)
q.put(1)
q.get()
q.get_nowait()
q.get(timeout=3)
q.full()
q.empty()
- LifoQueue
后进先出Queue (Last In First Out)
import queue
q = queue.LifoQueue(3)
- PriorityQueue
优先级Queue
优先级数字越小,优先级越高。
import queue
q = queue.PriorityQueue(3)
q.put((2, '2')) # 参数为元组(priority number, value)
q.put((1, '1'))
q.put((3, '3'))
# 优先级数字越小,越先被取出。
print(q.get()) # (1, '1')
print(q.get()) # (2, '2')
print(q.get()) # (3, '3')
6 进程池与线程池
6.1 什么是池?
池是在保证计算机硬件安全的情况下最大限度地利用计算机,它降低了程序的运行效率,但能保证计算机硬件的安全,防止压力过大造成系统崩溃,从而保证程序的正常运行。
6.2 线程池
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
# 参数为线程池容量,默认为计算机CPU核数的5倍
# 参数设置为5,说明线程池创建完毕后,内部固定存在5个线程,而且这5个线程不会出现重复创建和销毁。
# 这样做能够减少再次创建/销毁线程产生的系统开销。
pool = ThreadPoolExecutor(5)
def task(n):
print(n)
time.sleep(2)
return n ** n
pool.submit(task, 0)
# 向线程池中提交任务。
# 返回值是Future类的对象。
# 任务的提交方式:同步和异步
# 同步:任务提交后等待任务的返回结果,等待期间不处理任何事;
# 异步:任务提交后不等待任务的返回结果,直接处理其它任务,返回结果由异步回调机制处理。
# 线程池的任务提交方式属于异步提交。
t_list = []
for i in range(20):
t = pool.submit(task, i)
print(t) # Future类的对象
# 调用对象的result()方法,得到的是任务的结果。
# result()使程序的运行由并发变成串行,提交方式变为同步提交
# print(res.result())
t_list.append(t)
# 关闭线程池,等待线程池中所有任务运行完毕。
pool.shutdown()
# 保证异步的提交方式
for each_t in t_list:
print(each_t.result())
6.3 进程池
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
import os
# 参数为进程池容量,默认为计算机CPU核数
pool = ProcessPoolExecutor()
def task(n):
print(n, os.getpid())
time.sleep(2)
return n ** n
def call_back(n):
print('callback: {n}'.format(n=n))
print(n.result())
if __name__ == '__main__':
p_list = []
for i in range(20):
# 通过异步回调机制处理异步提交产生的结果
pool.submit(task, i).add_done_callback(call_back)
6.4 总结
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
# 1 生成线程/进程池对象
thread_pool = ThreadPoolExecutor() # 线程池容量默认是CPU核数的5倍
process_pool = ProcessPoolExecutor() # 进程池容量默认是CPU核数
# 2 提交任务,返回Future类的对象
# 调用对象的add_done_callback方法进行异步回调处理。
# 异步回调函数call_back会在任务函数task执行完毕后自动触发执行,对任务函数task的结果进程处理。
pool_obj.submit(task, i).add_done_callback(call_back)
7 协程
7.1 介绍
进程:资源单位
线程:执行单位
协程是为了在单线程下实现并发的效果。
协程需要做到:切换 + 保存状态
通过程序代码监听所有的IO操作,出现IO操作时通过程序代码进行切换,使得CPU觉得这样的程序没有IO操作,一直在运行,从而最大化地利用CPU等系统资源,提升程序的运行效率。
例如TCP的服务端,accept()和recv()都属于IO操作,通过程序代码实现accept()和recv()之间的快速切换。
但是不断执行 切换 + 保存状态 操作也可能会降低程序的执行效率。
对于计算密集型,会降低效率;
对于IO密集型,可以提升效率。
7.2 gevent模块
import time
# from gevent import spawn
# gevent本身无法监听常见的io操作,需要额外导入猴子补丁
# from gevent import monkey
# monkey.patch_all()
# 简写
from gevent import monkey; monkey.patch_all()
from gevent import spawn
def func1():
print(11)
time.sleep(2)
print(12)
def func2():
print(21)
time.sleep(3)
print(22)
start_time = time.time()
# 监听,异步提交
g1 = spawn(func1)
g2 = spawn(func2)
# 等待被监测的任务执行完毕后再继续执行
g1.join()
g2.join()
print(time.time() - start_time)
'''
11
21
12
22
3.01
'''
7.3 TCP服务端(基于协程)
使用协程实现TCP服务端的并发,accept()和recv()都属于IO操作,通过程序代码在accept()和recv()之间实现不断切换。
import socket
from gevent import monkey; monkey.patch_all()
from gevent import spawn
def communicate(conn):
while True:
try:
data = conn.recv(1024)
if len(data) == 0:
break
conn.send(data.upper())
except ConnectionResetError as e:
print(e)
break
conn.close()
def server(ip_addr, port_num):
server = socket.socket()
server.bind((ip_addr, port_num))
server.listen(5)
while True:
conn, addr = server.accept()
spawn(communicate, conn)
if __name__ == '__main__':
g1 = spawn(server, '127.0.0.1', 8080)
g1.join()