递归锁、信号量、GIL锁、基于多线程的socket通信和进程池线程池
递归锁
死锁现象:是指两个或两个以上的进程和线程因抢夺计算机资源而产生的一种互相等待的现象
from threading import Thread
from threading import Lock
import time
lock_A = Lock()
lock_B = Lock()
class MyThread(Thread):def run(self):self.f1()self.f2()def f1(self):lock_A.acquire()print(f"{self.name}拿到了A锁")lock_B.acquire()print(f"{self.name}拿到了B锁")lock_B.release()lock_A.release()def f2(self):lock_B.acquire()print(f"{self.name}拿到了B锁")time.sleep(0.1)lock_A.acquire()print(f"{self.name}拿到了A锁")lock_A.release()lock_B.release()
if __name__ == '__main__':for i in range(3):t = MyThread()t.start()
# 结果:
Thread-1拿到了A锁
Thread-1拿到了B锁
Thread-1拿到了B锁
Thread-2拿到了A锁
递归锁:
递归锁有一个计数的功能, 原数字为0,上一次锁,计数+1,释放一次锁,计数-1,
只要递归锁上面的数字不为零,其他线程就不能抢锁.
from threading import Thread
from threading import RLock
import time
lock_A = lock_B = RLock()
class MyThread(Thread):def run(self):self.f1()self.f2()def f1(self):lock_A.acquire()print(f"{self.name}拿到了A锁")lock_B.acquire()print(f"{self.name}拿到了B锁")lock_B.release()lock_A.release()def f2(self):lock_B.acquire()print(f"{self.name}拿到了B锁")time.sleep(0.1)lock_A.acquire()print(f"{self.name}拿到了A锁")lock_A.release()lock_B.release()
if __name__ == '__main__':for i in range(3):t = MyThread()t.start()
信号量
也是一种锁, 控制并发数量
from threading import Thread,Semaphore,current_thread
import time
import random
sem = Semaphore(5)
def task():sem.acquire()print(f"{current_thread().name}厕所ing")time.sleep(random.randint(1,3))print(f"{current_thread().name}厕所ed")sem.release()
if __name__ == '__main__':for i in range(20):t = Thread(target=task,)t.start()
GIL锁
GIL锁的定义:
全局解释锁,就是一把互斥锁,将并发变成串行,同一时刻只能有一个线程使用解释器资源,牺牲效率,保证解释器的数据安全。
py文件在内存中的执行过程:
- 当执行py文件时,会在内存中开启一个进程
- 进程中不光包括py文件还有python解释器,py文件中的线程会将代码交给解释器,
- 解释器将python代码转化为C语言能识别的字节码,然后再交给解释器中的虚拟机将字节码转化为二进制码最后交给CPU执行
当线程1先拿到GIL锁时线程2、线程3就只能等待,当线程1在CPU执行遇到阻塞或执行一段时间后,线程1会被挂起,同时GIL锁会被释放,此时线程2或线程3就会拿到锁进入解释器,同样,当在CPU执行遇到阻塞或执行一段时间后被挂起,同时GIL锁会被释放,此时最后一个线程就会进入解释器。
从上面可以看出,当遇到单个进程中含有多个线程时,由于GIL锁的存在,Cpython并不能利用多核进行并行处理,但可以在单核实现并发。
但不同进程之间的多线程是可以利用多核的。
GIL锁的两个作用:
1、保证解释器里面的数据的安全;
2、强行加锁,减轻开发负担
问题:单进程的多线程不能利用多核
如何判断什么情况使用多线程并发与多进程并发
对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用
当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,所以应该相对的去看一个程序到底是计算密集型还是I/O密集型,如下:
#分析:
我们有四个任务需要处理,处理方式肯定是要达到并发的效果,解决方案可以是:
方案一:开启四个进程
方案二:一个进程下,开启四个线程#单核情况下,分析结果: 如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销,方案二胜如果四个任务是I/O密集型,方案一创建进程的开销大,且进程的切换速度远不如线程,方案二胜#多核情况下,分析结果:如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行,可以利用多核,方案一胜如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜#结论:现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
总结:多核前提下,如果任务IO密集型,使用多线程并发;如果任务计算密集型,使用多进程并发。
验证Cpython的并发效率
- 计算密集型: 单个进程的多线程并发 vs 多个进程的并发并行
from threading import Thread
from multiprocessing import Process
import time
import randomif __name__ == '__main__':# 多进程的并发,并行start_time = time.time()l1 = []for i in range(4):p = Process(target=task,)l1.append(p)p.start()for p in l1:p.join()print(f'执行效率:{time.time()- start_time}') # 2.5342209339141846
from threading import Thread
from multiprocessing import Process
import time
import randomif __name__ == '__main__':
# 多线程的并发start_time = time.time()l1 = []for i in range(4):p = Thread(target=task,)l1.append(p)p.start()for p in l1:p.join()print(f'执行效率:{time.time()- start_time}') # 5.262923240661621
总结: 计算密集型: 多进程的并发并行效率高.
- IO密集型: 单个进程的多线程并发 vs 多个进程的并发并行
from threading import Thread
from multiprocessing import Process
import time
import randomdef task():count = 0time.sleep(random.randint(1,3))count += 1if __name__ == '__main__':# 多进程的并发,并行start_time = time.time()l1 = []for i in range(50):p = Process(target=task,)l1.append(p)p.start()for p in l1:p.join()print(f'执行效率:{time.time()- start_time}') # 7.145753383636475
from threading import Thread
from multiprocessing import Process
import time
import randomdef task():count = 0time.sleep(random.randint(1,3))count += 1if __name__ == '__main__':start_time = time.time()l1 = []for i in range(50):p = Thread(target=task,)l1.append(p)p.start()for p in l1:p.join()print(f'执行效率:{time.time()- start_time}') # 3.0278055667877197
总结:对于IO密集型: 单个进程的多线程的并发效率高.
基于多线程的socket通信
客户端:
import socketclient = socket.socket()client.connect(('127.0.0.1',8848))while 1:try:to_server_data = input('>>>').strip()client.send(to_server_data.encode('utf-8'))from_server_data = client.recv(1024)print(f'来自服务端的消息: {from_server_data.decode("utf-8")}')except Exception:break
client.close()
服务端:
import socket
from threading import Threaddef communicate(conn,addr):while 1:try:from_client_data = conn.recv(1024)print(f'来自客户端{addr[1]}的消息: {from_client_data.decode("utf-8")}')to_client_data = input('>>>').strip()conn.send(to_client_data.encode('utf-8'))except Exception:breakconn.close()def _accept():server = socket.socket()server.bind(('127.0.0.1', 8848))server.listen(5)while 1:conn, addr = server.accept()t = Thread(target=communicate,args=(conn,addr))t.start()if __name__ == '__main__':_accept()
进程池线程池
线程池: 一个容器,这个容器限制住你开启线程的数量,比如4个,第一次肯定只能并发的处理4个任务,只要有任务完成,线程马上就会接下一个任务.
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor
import os
import time
import random# print(os.cpu_count()) 获取cpu数量
def task(n):print(f'{os.getpid()} 接客')time.sleep(random.randint(1,3))# 开启进程池 (并行(并行+并发))
if __name__ == '__main__':p = ProcessPoolExecutor() # 进程池,默认不写,开启数量为cpu数量for i in range(20):p.submit(task,i)# 开启线程池 (并发)t = ThreadPoolExecutor() # 默认不写, cpu个数*5 线程数# t = ThreadPoolExecutor(100) # 100个线程for i in range(20):t.submit(task,i)