线程 *知识点扩充
1. 基于 TCP 协议实现 socket 通信 多线程并发 效果(简易版本)
# 客户端 import socket client = socket.socket() client.connect(('127.0.0.1', 8083)) while True: client.send(b'hello') data = client.recv(1024) print(data.decode('utf-8'))
# 服务端
TCP服务端实现并发
1.将不同的功能尽量拆分成不同的函数
拆分出来的功能可以被多个地方使用
1.将连接循环和通信循环拆分成不同的函数
2.将通信循环做成多线程
import socket from threading import Thread def run(conn, i): while True: try: data = conn.recv(1024) print('第%s条数据:' % i, data.decode('utf-8')) conn.send(data.upper()) except ConnectionResetError as e: print(e) break conn.close() server = socket.socket() server.bind(('127.0.0.1', 8083)) server.listen(5) while True: conn, addr = server.accept() for i in range(10): t = Thread(target=run, args=(conn, i)) t.start()
2. 全局解释器锁 GIL(global interpreter lock)
GIL本质也是一把互斥锁:将并发变成串行牺牲效率,保证数据的安全
作用:阻止同一个进程下的多个线程的同时执行
也就意味着 同一个进程内多个线程无法实现并行但是可以实现并发
python的多线程没法利用多核优势 是不是就是没有用了?
研究python的多线程是否有用需要分情况讨论:(肯定是有用的)
假设有四个任务(每个任务都需要10s)
都是计算密集型:
单核情况下
开线程更省资源
多核情况下
开进程 10+s
开线程 40+s
都是 IO 密集型 :
单核情况下
开线程更节省资源
多核情况下
开线程更节省资源
PS:python 解释器有很多种,最常见的就是 CPython 解释器 在CPython解释器才有GIL的概念,不是python的特点
GIL 的存在是因为 CPython 解释器的 内存管理的线程不是安全 的
内存管理机制:
引用计数:值与变量的绑定关系的个数
标记清除:当内存快要满的时候 会自动停止程序的运行 检测所有的变量与值的绑定关系
给没有绑定关系的值打上标记,最后一次性清除
分代回收:(垃圾回收机制也是需要消耗资源的,而正常一个程序的运行内部会使用到很多变量与值
并且有一部分类似于常量,减少垃圾回收消耗的时间,应该对变量与值的绑定关系做一个分类)
新生代(5S) >>> 青春代(10s) >>> 老年代(20s)
垃圾回收机制扫描一定次数发现关系还在,会将该对关系移至下一代
随着代数的递增 扫描频率是降低的
如:多个线程同时处理同一进程下的一份数据,假如没有全局解释器锁,容易造成数据的错乱
# 计算密集型 # from multiprocessing import Process # from threading import Thread # import os,time # def work(): # res=0 # for i in range(100000000): # res*=i # # # if __name__ == '__main__': # l=[] # print(os.cpu_count()) # 本机为6核 # start=time.time() # for i in range(6): # # p=Process(target=work) #耗时 4.732933044433594 # p=Thread(target=work) #耗时 22.83087730407715 # l.append(p) # p.start() # for p in l: # p.join() # stop=time.time() # print('run time is %s' %(stop-start))
# IO密集型 from multiprocessing import Process from threading import Thread import threading import os,time def work(): time.sleep(2) if __name__ == '__main__': l=[] print(os.cpu_count()) #本机为6核 start=time.time() for i in range(4000): p=Process(target=work) #耗时9.001083612442017s多,大部分时间耗费在创建进程上 # p=Thread(target=work) #耗时2.051966667175293s多 l.append(p) p.start() for p in l: p.join() stop=time.time() print('run time is %s' %(stop-start))
3. 全局解释器 GIL & 普通互斥锁
我们从 全局解释器锁 未加锁但是让程序出现IO操作情况 以及 加锁 情况来比较
# 全局解释器锁 def run(i): global n re_n = n n = re_n - 1 print(f'第{i}个线程修改了n,现在 n = {n}') if __name__ == '__main__': for i in range(10): t = Thread(target=run, args=(i,)) t.start() # 由于有全局解释器锁的存在,多线程操作数据正常 ''' # 第0个线程修改了n,现在n = 99 # 第1个线程修改了n,现在n = 98 # 第2个线程修改了n,现在n = 97 # 第3个线程修改了n,现在n = 96 # 第4个线程修改了n,现在n = 95 # 第5个线程修改了n,现在n = 94 # 第6个线程修改了n,现在n = 93 # 第7个线程修改了n,现在n = 92 # 第8个线程修改了n,现在n = 91 # 第9个线程修改了n,现在n = 90 # # Process finished with exit code 0 '''
# 不加锁但是 让程序出现IO操作 from threading import Thread import time n = 100 def run(i): global n re_n = n time.sleep(1) # IO操作时候回自动释放GIL n = re_n - 1 print(f'第{i}个线程修改了n,现在 n = {n}') if __name__ == '__main__': for i in range(10): t = Thread(target=run, args=(i,)) t.start() # 由于多线程程序遇到了IO操作,使得数据错乱 ''' # 第1个线程修改了n,现在 n = 99 # 第0个线程修改了n,现在 n = 99 # 第6个线程修改了n,现在 n = 99 # 第4个线程修改了n,现在 n = 99 # 第2个线程修改了n,现在 n = 99 # 第3个线程修改了n,现在 n = 99 # 第7个线程修改了n,现在 n = 99 # 第5个线程修改了n,现在 n = 99 # 第8个线程修改了n,现在 n = 99 # 第9个线程修改了n,现在 n = 99 # # Process finished with exit code 0 '''
# 加锁 from threading import Thread, Lock import time mutex = Lock() # 生成一把锁 n = 100 def run(i): mutex.acquire() # 抢锁 global n # 局部修改全局 re_n = n time.sleep(1) # IO操作时候回自动释放GIL,但是由于自己加了一把锁,所以还是在修改数据时候并发变成串行 n = re_n - 1 print(f'第{i}个线程修改了n,现在 n = {n}') mutex.release() # 释放锁 if __name__ == '__main__': for i in range(10): t = Thread(target=run, args=(i,)) t.start() # 加锁情况下,数据得到了保障,结果正常 此时同时有 GIL 和 锁 ''' 第0个线程修改了n,现在 n = 99 第1个线程修改了n,现在 n = 98 第2个线程修改了n,现在 n = 97 第3个线程修改了n,现在 n = 96 第4个线程修改了n,现在 n = 95 第5个线程修改了n,现在 n = 94 第6个线程修改了n,现在 n = 93 第7个线程修改了n,现在 n = 92 第8个线程修改了n,现在 n = 91 第9个线程修改了n,现在 n = 90 Process finished with exit code 0 '''
4. 死锁现象(了解知识点)
from threading import Thread, Lock import time mutex1 = Lock() mutex2 = Lock() def run(i): run1(i) run2(i) def run1(i): mutex1.acquire() print(f''' 第 {i} 个线程 第一次 抢到了 mutex1 锁''') mutex2.acquire() print(f'第 {i} 个线程 第一次 抢到了 mutex2 锁') mutex2.release() mutex1.release() def run2(i): mutex2.acquire() print(f''' 第 {i} 个线程 第二次 抢到了 mutex2 锁''') time.sleep(1) mutex1.acquire() print(f'第 {i} 个线程 第二次 抢到了 mutex1 锁') mutex1.release() mutex2.release() if __name__ == '__main__': for i in range(5): t = Thread(target=run, args=(i,)) t.start() # 结果 ''' 第 0 个线程 第一次 抢到了 mutex1 锁 第 0 个线程 第一次 抢到了 mutex2 锁 第 0 个线程 第二次 抢到了 mutex2 锁 第 1 个线程 第一次 抢到了 mutex1 锁 '''
# 上述结果可以看到,由于第0个线程在第二次抢锁时候,刚抢完锁1,准备抢锁2时候,发现锁2已经被线程1给抢走了,只能继续停在原地,代码执行不动,也就是死锁
# RLock(递归锁) """ Rlock可以被第一个抢到锁的人连续的acquire和release 每acquire一次锁身上的计数加1 每release一次锁身上的计数减1 只要锁的计数不为0 其他人都不能抢 """ from threading import Thread, RLock import time mutex1 = mutex2 = RLock() # Rlock可以被第一个抢到锁的人连续的acquire和release # 此时 mutex1 和 mutex2 是同一把锁 def run(i): run1(i) run2(i) def run1(i): mutex1.acquire() print(f''' 第 {i} 个线程 第一次 抢到了 mutex1 锁''') mutex2.acquire() print(f'第 {i} 个线程 第一次 抢到了 mutex2 锁') mutex2.release() mutex1.release() def run2(i): mutex2.acquire() print(f''' 第 {i} 个线程 第二次 抢到了 mutex2 锁''') time.sleep(1) mutex1.acquire() print(f'第 {i} 个线程 第二次 抢到了 mutex1 锁') mutex1.release() mutex2.release() if __name__ == '__main__': for i in range(5): t = Thread(target=run, args=(i,)) t.start() # 结果 第 0 个线程 第一次 抢到了 mutex1 锁 第 0 个线程 第一次 抢到了 mutex2 锁 第 0 个线程 第二次 抢到了 mutex2 锁 第 0 个线程 第二次 抢到了 mutex1 锁 第 1 个线程 第一次 抢到了 mutex1 锁 第 1 个线程 第一次 抢到了 mutex2 锁 第 1 个线程 第二次 抢到了 mutex2 锁 第 1 个线程 第二次 抢到了 mutex1 锁 第 3 个线程 第一次 抢到了 mutex1 锁 第 3 个线程 第一次 抢到了 mutex2 锁 第 3 个线程 第二次 抢到了 mutex2 锁 第 3 个线程 第二次 抢到了 mutex1 锁 第 2 个线程 第一次 抢到了 mutex1 锁 第 2 个线程 第一次 抢到了 mutex2 锁 第 2 个线程 第二次 抢到了 mutex2 锁 第 2 个线程 第二次 抢到了 mutex1 锁 第 4 个线程 第一次 抢到了 mutex1 锁 第 4 个线程 第一次 抢到了 mutex2 锁 第 4 个线程 第二次 抢到了 mutex2 锁 第 4 个线程 第二次 抢到了 mutex1 锁 Process finished with exit code 0
5. 信号量(了解知识点)
from threading import Semaphore, Thread import time, random sm = Semaphore(5) # 5表示容量个数 def run(i): sm.acquire() # 能同时接纳5个 print(f'{i}占了一个位置') time.sleep(random.randint(1, 3)) sm.release() # 释放一个空出来一个,可以继续接纳 print(f'{i}释放了一个位置') if __name__ == '__main__': for i in range(10): t = Thread(target=run, args=(i,)) t.start() # 结果 0占了一个位置 1占了一个位置 2占了一个位置 3占了一个位置 4占了一个位置 3释放了一个位置 5占了一个位置 1释放了一个位置 6占了一个位置 0释放了一个位置 7占了一个位置 2释放了一个位置 8占了一个位置 4释放了一个位置 6释放了一个位置 9占了一个位置 5释放了一个位置 9释放了一个位置 8释放了一个位置 7释放了一个位置 Process finished with exit code 0
6. event 事件(了解知识点)
from threading import Event, Thread import time e = Event() # 生成一个Event对象 def run(): print(' 红灯....') time.sleep(3) e.set() # 发信号 print(' 绿灯... ') t = Thread(target=run) t.start() def run1(i): print(f'公交 {i + 1} 路停车等待....') e.wait() # 等待信号 print(f'公交 {i + 1} 路开始百公里加速....') for i in range(10): t = Thread(target=run1, args=(i,)) t.start() # 结果 ''' 红灯.... 公交 1 路停车等待.... 公交 2 路停车等待.... 公交 3 路停车等待.... 公交 4 路停车等待.... 公交 5 路停车等待.... 公交 6 路停车等待.... 公交 7 路停车等待.... 公交 8 路停车等待.... 公交 9 路停车等待.... 公交 10 路停车等待.... 绿灯... 公交 1 路开始百公里加速.... 公交 4 路开始百公里加速.... 公交 5 路开始百公里加速.... 公交 8 路开始百公里加速.... 公交 9 路开始百公里加速.... 公交 2 路开始百公里加速.... 公交 10 路开始百公里加速.... 公交 6 路开始百公里加速.... 公交 3 路开始百公里加速.... 公交 7 路开始百公里加速.... Process finished with exit code 0 '''
7. 线程 q(了解知识点)
from queue import Queue, LifoQueue, PriorityQueue q1 = Queue() # 先进先出队列 q1.put(2) # 先放 q1.put(1) q1.put(3) print(q1.get()) # 2 q2 = LifoQueue() # 后进先出队列 last in first out q2.put(2) q2.put(3) q2.put(5) # 后放 print(q2.get()) # 5 q3 = PriorityQueue() # 优先级队列,一个元祖参数,元祖内第一个参数表示优先级,越小越优先,支持负数;元组内第二个参数为要放入的数据 q3.put((10, 4)) q3.put((20, 5)) q3.put((-10, 6)) # 优先级最高 q3.put((0, 7)) print(q3.get()) # (-10, 6) print(q3.get()[1]) # 6 元祖是有序的,支持索引取值