锁/threadinglocal的应用及原理/线程池/生产者消费者模型
一.关于上篇博客的补充
1.Python的GIL锁
Python内置的一个全局解释器锁,锁的作用就是保证同一时刻一个进程中只有一个线程可以被CPU调度
2.为什么要有这把锁?
答:Python语言的创始人在开发这门语言时,目的是快速把语言开发出来,如果加上GIL锁(C语言加锁),切换时按照100条字节指令来进行线程间的切换
3.进程与线程的区别:
线程:CPU工作的最小单元
进程:为线程提供一个资源共享的空间
一个进程中默认是有一个主线程
二.多线程锁,也就是threading模块
引入threading模块
threading用于提供线程相关的操作,线程是应用程序中工作的最小单元。python当前版本的多线程库没有实现优先级、线程组,线程也不能被停止、暂停、恢复、中断。
threading模块提供的类:
Thread,Lock,RLock,Condition,[Bounded]Semaphore,Event,Time,Local
threading模块提供的常用方法:
threading.currentThread():返回当前的线程变量
threading.enumerate():返回一个包含正在运行的线程的list.正在运行指线程启动后,结束前,不包括启动前和终止后的线程
threading.activeCount():返回正在运行的线程数量,与len(threading.enumerate())有相同的结果
threading模块提供的常量:
threading.TIMEOUT_MAX设置threading全局超过时间
1.线程锁:Lock/Rlock
由于线程之间随机调度:某线程可能在执行N条后,CPU接着执行其他线程.为了多个线程同时操作一个内存中的资源时不产生混乱,我们使用锁
(1).Lock(指令锁)是可用的最低级的同不指令.Lock处于锁定状态时,不被特定的线程拥有.
Lock包含两种状态--锁定和非锁定,以及两个基本的方法.
可以认为Lock有一个锁定池,当线程请求锁定时,将线程至于池中,直到获得锁定出池,池中的线程处于状态图中的同步阻塞状态
(2).RLock(可重入锁)是一个可以被同一个线程请求多次的同步指令.RLock使用了"拥有的线程"和"递归等级"的概念,处于锁定状态时,RLock被某个线程拥有.拥有RLock的线程可以再次调用acquire(),释放锁时需要调用release()相同次数
可以认为RLock包含一个锁定池和一个初识值为0的计数器,每次成功调用acquire()/release(),计数器将+1/-1,为0时锁处于未锁定状态
简而言之:Lock属于全局,RLock属于线程
构造方法:Lock(),RLock();推荐使用RLock()
实例方法:
acquire([timeout]): 尝试获得锁定。使线程进入同步阻塞状态。
release(): 释放锁。使用前线程必须已获得锁定,否则将抛出异常。
(3).习题:创建10个线程Lock方法
v = [] : 锁
把自己的添加列表中
再读取列表中的最后一个
解锁
线程安全,多线程操作时,内部会让所有线程排队处理.如:list/dict/Queue

import threading v = [] def func(arg): v.append(arg) # 线程安全 print(v) for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start() 结果: [0] [0, 1] [0, 1, 2] [0, 1, 2, 3] [0, 1, 2, 3, 4] [0, 1, 2, 3, 4, 5] [0, 1, 2, 3, 4, 5, 6] [0, 1, 2, 3, 4, 5, 6, 7] [0, 1, 2, 3, 4, 5, 6, 7, 8] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
线程不安全 + 人 => 排队处理

import threading import time v = [] lock = threading.Lock() def func(arg): lock.acquire() v.append(arg) time.sleep(0.01) # print(v) m = v[-1] print(arg,m) lock.release() for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start() 结果: 结果: 加锁 不加锁 0 0 2 9 1 1 0 9 2 2 1 9 3 3 7 9 4 4 4 9 5 5 3 9 6 6 5 9 7 7 6 9 8 8 9 9 9 9 8 9
(4).锁:RLock(1次放1个)

import threading import time v = [] lock = threading.RLock() def func(arg): lock.acquire() lock.acquire() v.append(arg) time.sleep(0.01) m = v[-1] print(arg,m) lock.release() lock.release() for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start() 结果: 0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9
(5).Lock对比RLock
import threading
lock = threading.Lock() #Lock对象
lock.acquire()
lock.acquire() #产生了死锁。
lock.release()
lock.release()
print lock.acquire()
import threading
rLock = threading.RLock() #RLock对象
rLock.acquire()
rLock.acquire() #在同一线程内,程序不会堵塞。
rLock.release()
rLock.release()
2.信号量(Semaphore)
互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。

import time import threading lock = threading.BoundedSemaphore(3) def func(arg): lock.acquire() print(arg) time.sleep(1) lock.release() for i in range(20): t =threading.Thread(target=func,args=(i,)) t.start() 结果: 0 1 2 ...等1秒 3 4 5 ... 6 7 8 ... 9 10 11 ... 12 13 14 ... 15 16 17 ... 18 19
3.条件(Condition)
(1).使得线程等待,只有满足某条件时,才释放n个线程
(2).Condition(条件变量)通常与一个锁关联。需要在多个Contidion中共享一个锁时,可以传递一个Lock/RLock实例给构造方法,否则它将自己生成一个RLock实例。
(3)可以认为,除了Lock带有的锁定池外,Condition还包含一个等待池,池中的线程处于等待阻塞状态,直到另一个线程调用notify()/notifyAll()通知;得到通知后线程进入锁定池等待锁定。
(4).构造方法:
Condition([lock/rlock])
(5).实例方法:
acquire([timeout])/release(): 调用关联的锁的相应方法。
wait([timeout]): 调用这个方法将使线程进入Condition的等待池等待通知,并释放锁。使用前线程必须已获得锁定,否则将抛出异常。
notify(): 调用这个方法将从等待池挑选一个线程并通知,收到通知的线程将自动调用acquire()尝试获得锁定(进入锁定池);其他线程仍然在等待池中。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。
notifyAll(): 调用这个方法将通知等待池中所有的线程,这些线程都将进入锁定池尝试获得锁定。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。

import time import threading lock = threading.Condition() def func(arg): print('线程进来了') lock.acquire() lock.wait() # 加锁 print(arg) time.sleep(1) lock.release() for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start() while True: inp = int(input('>>>')) lock.acquire() lock.notify(inp) lock.release()

import time import threading lock = threading.Condition() def xxxx(): print('来执行函数了') input(">>>") # ct = threading.current_thread() # 获取当前线程 # ct.getName() return True def func(arg): print('线程进来了') lock.wait_for(xxxx) print(arg) time.sleep(1) for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start()
4.Event(事件)
(1).Event(事件)是最简单的线程通信机制之一:一个线程通知事件,其他线程等待事件。Event内置了一个初始为False的标志,当调用set()时设为True,调用clear()时重置为 False。wait()将阻塞线程至等待阻塞状态。
(2).Event其实就是一个简化版的 Condition。Event没有锁,无法使线程进入同步阻塞状态。
(3).构造方法:
Event()
(4).实例方法:
isSet(): 当内置标志为True时返回True。
set(): 将标志设为True,并通知所有处于等待阻塞状态的线程恢复运行状态。
clear(): 将标志设为False。
wait([timeout]): 如果标志为True将立即返回,否则阻塞线程至等待阻塞状态,等待其他线程调用set()

import time import threading lock = threading.Event() def func(arg): print('线程来了') lock.wait() # 加锁:红灯 print(arg) for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start() input(">>>>") lock.set() # 绿灯 time.sleep(1) lock.clear() # 再次变红灯 for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start() input(">>>>") lock.set() 结果: 线程来了 线程来了 线程来了 线程来了 线程来了 线程来了 线程来了 线程来了 线程来了 线程来了 >>>>1 0 1 2 3 7 6 9 4 5 8 ...睡1秒 线程来了 线程来了 线程来了 线程来了 线程来了 线程来了 线程来了 线程来了 线程来了 线程来了 >>>>2 0 2 4 5 6 9 8 3 7 1
5.local类
(1).local是一个小写字母开头的类,用于管理 thread-local(线程局部的)数据。对于同一个local,线程无法访问其他线程设置的属性;线程设置的属性不会被其他线程设置的同名属性替换。
(2).可以把local看成是一个“线程-属性字典”的字典,local封装了从自身使用线程作为 key检索对应的属性字典、再使用属性名作为key检索属性值的细节。

import time import threading v = threading.local() def func(arg): # 内部会为当前线程创建一个空间用于存储:phone=自己的值 v.phone = arg time.sleep(2) print(v.phone,arg) # 去当前线程自己空间取值 for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start() 结果: ...睡2秒 2 2 1 1 0 0 6 6 4 4 5 5 3 3 8 8 9 9 7 7
(3).threading.local的原理

import time import threading DATA_DICT = {} def func(arg): """ :param arg: 传过来的i值 :return: """ ident = threading.get_ident() # 获得线程代号 print(ident) DATA_DICT[ident] = arg # 将线程代号和传过来的i值添加到字典 time.sleep(1) # 睡一秒 print(DATA_DICT[ident],arg) # 打印线程代号所对应的值和传过来的i值 for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start() 结果: 18356 1388 9440 19216 7300 7888 2500 2100 12404 1000 1 1 0 0 2 2 4 4 3 3 6 6 7 7 5 5 9 9 8 8
(4).threading.local的原理(可选)

""" 以后:Flask框架内部看到源码 上下文管理 """ import time import threading INFO = {} class Local(object): def __getattr__(self, item): ident = threading.get_ident() return INFO[ident][item] def __setattr__(self, key, value): ident = threading.get_ident() if ident in INFO: INFO[ident][key] = value else: INFO[ident] = {key:value} obj = Local() def func(arg): obj.phone = arg # 调用对象的 __setattr__方法(“phone”,1) time.sleep(2) print(obj.phone,arg) for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start() 结果: 3 3 0 0 2 2 1 1 7 7 5 5 4 4 6 6 8 8 9 9
6.线程池
(1).线程池技术为线程创建、销毁的开销问题和系统资源不足问题提供了很好的解决方案。
优点:
(1)可以控制产生线程的数量。通过预先创建一定数量的工作线程并限制其数量,控制线程对象的内存消耗。
(2)降低系统开销和资源消耗。通过对多个请求重用线程,线程创建、销毁的开销被分摊到了多个请求上。另外通过限制线程数量,降低虚拟机在垃圾回收方面的开销。
(3)提高系统响应速度。线程事先已被创建,请求到达时可直接进行处理,消除了因线程创建所带来的延迟,另外多个线程可并发处理。

from concurrent.futures import ThreadPoolExecutor import time def task(a1,a2): time.sleep(2) print(a1,a2) # 创建了一个线程池(最多5个线程) pool = ThreadPoolExecutor(5) for i in range(40): # 去线程池中申请一个线程,让线程执行task函数。 pool.submit(task,i,8) 结果: 每输出5个结果,睡2秒
7.练习

####线程#### import time import threading def task(arg): time.sleep(50) print(arg) while True: num = input('>>>') t = threading.Thread(target=task,args=(num,)) t.start() ####线程池#### import time from concurrent.futures import ThreadPoolExecutor def task(arg): time.sleep(50) print(arg) pool = ThreadPoolExecutor(20) while True: num = input('>>>') pool.submit(task,num)
8.生产者消费者模型
引入queue模块
概念:queue是python标准库中的线程安全的队列(FIFO)实现,提供了一个适用于多线程编程的先进先出的数据结构,即队列,用来在生产者和消费者线程之间的信息传递
(1).业界用的比较广泛,多线程之间进行同步数据的方法,解决线程之间的堵塞,互相不影响
(2).三部件:
生产者
队列:先进先出
扩展:栈,后进先出
消费者
(3).问:生产者消费者模型解决了什么问题?不用一直等待的问题
(4).在一个程序中实现又有生产者又有消费者,生产者不断生产,消费者不断消费,达到并行数据安全完整交互的目的。所以会有消息队列的关键字产生,队列是典型的生产者消费者模型

import time import queue import threading q = queue.Queue() # 线程安全 def producer(id): """ 生产者 :return: """ while True: time.sleep(2) q.put('包子') print('厨师%s 生产了一个包子' %id ) for i in range(1,4): t = threading.Thread(target=producer,args=(i,)) t.start() def consumer(id): """ 消费者 :return: """ while True: time.sleep(1) v1 = q.get() print('顾客 %s 吃了一个包子' % id) for i in range(1,3): t = threading.Thread(target=consumer,args=(i,)) t.start() 结果: 一直不停循环....