一、死锁与递归锁
1、死锁:当多个进程(线程)交叉争夺多个互斥锁时会产生死锁的现象造成程序的阻塞。
import threading import time mutex_a = threading.Lock() mutex_b = threading.Lock() def task(name): mutex_a.acquire() # 第一名抢到a锁---第二名抢到a锁,此时第一名抢到b锁 print('{}获得了a锁'.format(name)) mutex_b.acquire() # 第一名抢到b锁---第二名拿着a锁尝试抢b锁,此时第一名拿着b锁尝试抢a锁,此处开始阻塞 print('{}获得了b锁'.format(name)) mutex_b.release() # 第一名释放b锁 print('{}释放了b锁'.format(name)) mutex_a.release() # 第一名释放a锁 print('{}释放了a锁'.format(name)) mutex_b.acquire() # 第一名抢到b锁 print('{}获得了b锁'.format(name)) time.sleep(3) mutex_a.acquire() # 第一名拿着b锁尝试抢a锁 print('{}获得了a锁'.format(name)) mutex_a.release() print('{}释放了a锁'.format(name)) mutex_b.release() print('{}释放了b锁'.format(name)) if __name__ == '__main__': for n_name in range(1, 3): t = threading.Thread(target=task, args=(n_name,)) t.start() ''' 阻塞结果为: 1获得了a锁 1获得了b锁 1释放了b锁 1释放了a锁 1获得了b锁 2获得了a锁 ...... '''
2、递归锁:跟互斥锁一样,首个抢到的进程(线程)才可以执行后续代码,但是可以被该进程(线程)多次acquire和release,其内部有个自带的计数器,每acquire/计数器加1,每release/计算器减1,只有等到计数器为0才会真正释放该锁,在此之前其他进程(线程)无法获取该锁。
import threading import time mutex_a = mutex_b = threading.RLock() # 如此a锁和b锁其实是同一把递归锁,共享计数器 def task(name): mutex_a.acquire() # 第一名抢到a锁,递归锁计数器=1 print('{}获得了a锁'.format(name)) mutex_b.acquire() # 第一名抢到b锁,递归锁计数器=2 print('{}获得了b锁'.format(name)) mutex_b.release() # 第一名释放b锁,递归锁计数器=1 print('{}释放了b锁'.format(name)) mutex_a.release() # 第一名抢到b锁,递归锁计数器=0 print('{}释放了a锁'.format(name)) mutex_b.acquire() # 第一名抢到b锁,递归锁计数器=1 print('{}获得了b锁'.format(name)) time.sleep(3) mutex_a.acquire() # 第一名抢到a锁,递归锁计数器=2 print('{}获得了a锁'.format(name)) mutex_a.release() # 第一名释放a锁,递归锁计数器=1 print('{}释放了a锁'.format(name)) mutex_b.release() # 第一名释放b锁,递归锁计数器=0,第二名可以抢到a锁并重复第一名的过程 print('{}释放了b锁'.format(name)) if __name__ == '__main__': for n_name in range(1, 3): t = threading.Thread(target=task, args=(n_name,)) t.start() ''' 结果为: 1获得了a锁 1获得了b锁 1释放了b锁 1释放了a锁 1获得了b锁 1获得了a锁 1释放了a锁 1释放了b锁 2获得了a锁 2获得了b锁 2释放了b锁 2释放了a锁 2获得了b锁 2获得了a锁 2释放了a锁 2释放了b锁 '''
二、信号量:在进程(线程)里也是一种锁,可以理解为可多人同时持有的公锁。
import threading import time import random public_lock = threading.Semaphore(3) # 可同时持有锁的线程数量 def task(name): public_lock.acquire() print('{}获得了公共锁'.format(name)) time.sleep((random.uniform(1, 2))) public_lock.release() print('{}释放了公共锁'.format(name)) if __name__ == '__main__': for n_name in range(1, 7): t = threading.Thread(target=task, args=(n_name,)) t.start() ''' 结果为: 1获得了公共锁 2获得了公共锁 3获得了公共锁 3释放了公共锁 4获得了公共锁 2释放了公共锁 5获得了公共锁 1释放了公共锁 6获得了公共锁 4释放了公共锁 6释放了公共锁 5释放了公共锁 '''
三、Event事件:为了让一些进程(线程)需要等待另一些进程(线程)执行到某步或者执行完毕自己才能执行,可以用event设置信号。
import threading import time event = threading.Event() # 创建一个事件对象 def pre_task(): print('前置任务执行中...') time.sleep(3) event.set() # 结束信号,用于给其他线程获取 print('前置任务执行完毕,发送信号') def then_task(): print('后道任务等待信号中...') event.wait() # 正在等待前置任务给出结束信号,当获取到event.set()后,则可以执行后续代码 print('后道任务获取信号,开始作业') if __name__ == '__main__': pre_t = threading.Thread(target=pre_task) then_t = threading.Thread(target=then_task) pre_t.start() then_t.start() ''' 结果为: 前置任务执行中... 后道任务等待信号中... 前置任务执行完毕,发送信号 后道任务获取信号,开始作业 '''
四、线程的队列应用和队列的其他形式
1、线程的队列应用:虽然同一个进程下的多个线程之间的数据是共享的,但是因为队列自带的互斥锁的机制,线程之间应用队列进行通信可以确保数据安全,具体用法类同于进程。
2、堆栈型队列:后进先出
3、优先级队列:以标记数字加队列成员组成的元组形式入队列,标记数字越小,优先级越高,出队也是对应的元组形式。
import queue q1 = queue.Queue(3) # 普通队列 print(q1.empty()) # 判断队列是否为空,结果为 True q1.put(111) q1.put(222) q1.put(333) print(q1.full()) # 判断队列是否满载,结果为 True print(q1.get()) # 先进先出,结果为 111 print(q1.get(timeout=3)) # 结果为 222,若队列为空,等待3秒后报错 print(q1.get_nowait()) # 结果为 333,若队列为空,直接报错 q2 = queue.LifoQueue(3) # 堆栈型队列 q2.put(444) q2.put(555) q2.put(666) print(q2.get()) # 后进先出,结果为 666 q3 = queue.PriorityQueue(3) # 优先级队列 q3.put((0, 777)) q3.put((-3, 888)) q3.put((3, 999)) print(q3.get()) # 标记数字最小的先出,结果为 (-3, 888)
五、进程池与线程池:拿tcp服务端用开进程(线程)的方式实现了并发为例,但是开进程(线程)都或多或少是需要消耗系统资源的。受限于计算机硬件的现状,不可能无限制地开设进程(线程)。为了能在硬件承受范围内尽可能地提高效率,引入了“池”的概念,利用它可以在计算机硬件安全与程序执行效率之间达到比较好的平衡。
import time import os import concurrent.futures pool = concurrent.futures.ProcessPoolExecutor(5) # 进程的情况,括号内如果不声明数量,会默认数量为 cpu数 # pool = concurrent.futures.ThreadPoolExecutor() # 线程的情况,括号内如果不声明数量,会默认数量为 cpu数*5 # 池子内生成固定数量的进程(线程),此处为5个,来流动执行提交进来的任务,每个进程(线程)同时只执行一个任务,池子关闭之前,这5个不会更改 def task(n): print('{}号进程正在执行{}号任务'.format(os.getpid(), n)) time.sleep(1) return '{}号任务的结果'.format(n) def call_back(task_obj): print(task_obj.result()) if __name__ == '__main__': for x in range(1, 11): # 向池子中投递10个任务 pool.submit(task, x).add_done_callback(call_back) # 此处若不加 add_done_callback()的回调机制,则是同步提交 pool.shutdown() # 待所有任务执行完毕再运行到此处关闭进程池 print('所有任务已全部执行') ''' 结果为: 14856号进程正在执行1号任务 13648号进程正在执行2号任务 13516号进程正在执行3号任务 10564号进程正在执行4号任务 10048号进程正在执行5号任务 14856号进程正在执行6号任务 1号任务的结果 13648号进程正在执行7号任务 2号任务的结果 13516号进程正在执行8号任务 3号任务的结果 10564号进程正在执行9号任务 4号任务的结果 10048号进程正在执行10号任务 5号任务的结果 6号任务的结果 7号任务的结果 8号任务的结果 9号任务的结果 10号任务的结果 所有任务已全部执行 '''
六、协程:不同于进程(明确的资源单位)和线程(明确的执行单位),协程是个虚构的概念,是程序设计的一种手法,具体来讲就是在程序设计时,把一些IO操作进行代码上的切换,用于给CPU造成程序一直在运行而没有IO阻塞的假象,使CPU不会从本程序切换走,进而提升程序的运行效率。
1、利用生成器验证计算密集的情况切换的效率。
# 串行执行密集计算的情况 import time start_time = time.time() for x in range(1, 99999): sum_x = 1 sum_x *= x for y in range(1, 99999): sum_y = 1 sum_y *= y end_time = time.time() print(end_time - start_time) # 结果为0.02秒多
# 利用生成器切换的情况 import time def sum_m(x, sum_x): while 1: x += 1 sum_x *= x yield start_time = time.time() g = sum_m(0, 1) for y in range(1, 99999): sum_y = 1 sum_y *= y g.__next__() end_time = time.time() print(end_time - start_time) # 结果为2秒多
2、利用gevent模块验证IO密集的情况切换的效率。
import time from gevent import spawn from gevent import monkey monkey.patch_all() def task1(): time.sleep(1) def task2(): time.sleep(2) def task3(): time.sleep(3) if __name__ == '__main__': start_time = time.time() g1 = spawn(task1) g2 = spawn(task2) g3 = spawn(task3) g1.join() g2.join() g3.join() end_time = time.time() print(end_time - start_time) # 结果为3秒多,如果是串行,一定是6秒多
3、协程实现tcp服务端的并发。
# tcp服务端 import socket from gevent import spawn from gevent import monkey monkey.patch_all() def build_server(ip, port): server = socket.socket() server.bind((ip, port)) server.listen(5) while 1: print('等待客户端连接...') conn, client_addr = server.accept() spawn(talk, conn) def talk(conn): while 1: try: recv_data = conn.recv(1024) if len(recv_data) == 0: break send_data = '收到:'.encode('utf-8') + recv_data conn.send(send_data) except Exception as e: print(e) break conn.close() if __name__ == '__main__': g = spawn(build_server, '127.0.0.1', 3333) g.join()
# tcp客户端 import threading import socket def build_client(n): client = socket.socket() client.connect(('127.0.0.1', 3333)) send_data = '来自客户端的:{}'.format(str(n)).encode('utf-8') client.send(send_data) recv_data = client.recv(1024) print(recv_data.decode('utf-8')) if __name__ == '__main__': for x in range(333): t = threading.Thread(target=build_client, args=(x,)) t.start()
4、小结:理论上,可以通过多进程===>开线程===>开协程,来实现程序执行效率的最大化。