锁(线程)/线程安全 + 线程池(ThreadPoolExecutor) + threading.local + 生产者消费者模型
锁(线程) / 线程安全
线程安全
1. 什么是线程安全?
线程安全是 python 的内置功能, 在多线程操作时,内部会让所有的线程排队处理.
线程安全, 列表/字典/队列 都是线程安全的.
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()
2. 线程不安全的时候怎么办?
线程不安全 + 锁 ==> 线程排队处理(编程安全的了)
3. 为什么要加锁?
a. 非线程安全情况下 加锁会变得安全
b. 加锁还可以控制一段代码,保证代码处理数据的完整性
4. 线程越多越好吗?
不是,线程越多,上下文管理越复杂(来回切换),会降低效率
锁
所有4中情况
1. 锁 Lock
1次放1个 / 一个一个的放
threading.Lock()
需求:
a. 创建100个线程,在列表中追加8
b. 创建100个线程
v = []
锁
- 把自己的添加到列表中。
- 在读取列表的最后一个。 # 添加进去读取的最后一个有可能不是自己添加的,因为添加的过程中,别的线程也可以添加,为了防止混乱要加锁,可以保证数据的完整性
解锁
具体代码示例:
import threading
import time
v = []
lock = threading.Lock() #### 创建一个锁对象 lock
def func(arg):
lock.acquire() #### 加锁
v.append(arg) # 数据追加都列表
time.sleep(0.01)
m = v[-1] # 取最后一个值
print(arg,m)
lock.release() #### 解锁
for i in range(10):
t =threading.Thread(target=func,args=(i,)) # 循环创建了10个线程 来执行func函数
t.start()
2. 锁 RLock
1次放1个 / 一个一个的放
threading.RLock()
.acquire() # 加锁
.release() # 解锁
RLock 和 Lock 的作用是一样的,都是一个一个放,用法也类似 Lock.
区别:
Lock 同步锁: 如果连续两次 .acquire() 加锁, 会变成死锁 解不开. 必须是加一次锁 解一次锁
RLock 递归锁: 可以连续两次加锁,再解两次
3. 锁 BoundedSemaphpre (也叫信号量)
一次放n个 / n个n个的放 n为固定值 参数
threading.BoundedSemaphore(n) # 创建 锁 对象
.acquire() # 加锁
.release() # 解锁
import time
import threading
lock = threading.BoundedSemaphore(3) # 创建锁lock 3个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()
4. 锁 Condition
1次放n个, n为动态值, 可以变化,
BoundedSemaphpre是每次都放 n 个
lock = threading.Condition() # 创建 lock 锁
lock.wait() # 加锁 夯住,等待另外的进程来释放锁
lock.notify(3) # 确定释放线程的个数
等价于, lock.wait_for(func) # 其中func为函数,当线程满足func时,便放行
方式一:
给定被同时释放的线程的个数, 也就是执行这几个线程.
如果.notify(3) 就是执行3个线程, 执行完了也不会接着再执行3个,需要重新给定个数,给定以后,从未执行的线程中,继续执行相应的个数.
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()
方式二:
满足 f1 函数条件的 线程可以被释放
import time import threading lock = threading.Condition() def f1(): print('来执行函数了') input(">>>") # ct = threading.current_thread() # 获取当前线程 # ct.getName() return True # 满足return True 就可以往下走 def func(arg): print('线程进来了') lock.wait_for(f1) # f1 为True print(arg) time.sleep(1) for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start()
5. 锁 Event
1次释放所有的,
threading.Event()
用 wait() 来阻止所有的线程,
用 set() 来解除阻止 释放所有的.
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,)) # 创建了10个进程每个进程都在wait()处停止了 t.start() input(">>>>") lock.set() # 绿灯 解除阻止 所有线程都可以通行了 lock.clear() # 再次变红灯 清除lock.set() 下次创建线程需要 lock.set() # 如果没有clear()的话, 下次创建的线程,可以被执行 for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start() input(">>>>") lock.set() # 前面有clear() 又变成了红灯 这里需要再次解除阻止 变绿灯
总结:
Lock (同步锁) 1个1个的放
RLock(递归锁) 1个1个的放
BoundedSemaphore(n) n个n个的放 (固定个数的放)
Contion 1次释放n个,下次输入的值,会接着放未执行的线程
1次释放满足函数条件的线程
Event 1次释放所有的
线程池
1. 什么是线程池?
线程池可以控制最多创建的线程的个数.
如果线程池中有5个线程,那么最多有5个线程可以同时被CPU调度.
如果一个程序需要创建10个线程来完成任务,线程池只设置了5,那么会先有5个线程来工作,当其中有线程结束的时候,才会创建新的线程来继续工作,总之要保证,能够被cpu同时调度的线程数不能超过设置的5. (这种原理类似于下载时,设置同时下载数为3.)
2. 如何控制线程的个数, 线程池的编写?
用线程池控制: 需要导入模块
from concurre.futures import ThreadPoolExecutor
pool = ThreadPoolExecutor(5) # 设置5
pool.submit(函数名,参数1,参数2)
from concurrent.futures import ThreadPoolExecutor import time def task(a1,a2): time.sleep(1) print(a1,a2) # 创建了一个线程池(最多5个线程) pool = ThreadPoolExecutor(5) for i in range(10): # 去线程池中申请一个线程,让线程执行task函数。 pool.submit(task,i,8) # (函数名,参数1,参数2)
3. 线程和线程池的比较
以后创建线程的时候,一定要用线程池,可以控制线程的个数
# # ######################## 线程 ########################### import time import threading def task(arg): time.sleep(50) 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) pool = ThreadPoolExecutor(20) # 线程池 设置20 较好 while True: num = input('>>>') pool.submit(task,num)
threading.local
1. threading.local()
作用:内部自动为每个线程维护一个空间(字典),用于当前存取属于自己的值。
保证线程之间的数据隔离。
这样做,资源相对会浪费.
{
线程ID: {...}
线程ID: {...}
线程ID: {...}
线程ID: {...}
}
注意: threading.local() 在py2中不存在, 在 py3 中存在
# 示例
import time import threading v = threading.local() def func(arg): # 内部会为当前线程创建一个空间用于存储:phone=自己的值 v.phone = arg time.sleep(1) print(v.phone,arg) # 去当前线程自己空间取值 for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start()
2. threadinglocal原理
threading.get_ident() # 获取当前的线程的ident
import time import threading DATA_DICT = {} # 创建一个字典用于存放数据 def func(arg): ident = threading.get_ident() # 获取当前线程ident 作为key DATA_DICT[ident] = arg time.sleep(1) print(DATA_DICT) print(DATA_DICT[ident],arg) for i in range(10): t =threading.Thread(target=func,args=(i,)) t.start()
因为在打印之前要停留1秒,此时每个线程都已经将自己ident 和参数值 添加到了字典内,所以打印的是完整的字典.

# {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9} # 0 0 # {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9} # 4 4 # {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9} # 2 2 # {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9} # {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9} # 3 3 # {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9} # 5 5 # 1 1 # {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9} # 8 8 # {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9} # 9 9 # {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9} # 6 6 # {20444: 0, 10340: 1, 15368: 2, 15404: 3, 19096: 4, 15312: 5, 17896: 6, 10492: 7, 17432: 8, 16584: 9} # 7 7
2. threadinglocal 原理 (可选, 了解)
""" 以后: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()
生产者和消费者模型 (队列)
1. 该模型有三部件
生产者 : 制造任务,制造的任务保存在队列中
队列 : 先进先出
消费者 : 处理任务,从队列中取出任务处理
2. 生产者和消费者模型解决了什么问题?
解决了不用的等待的问题. 例如 订票原理
示例:
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,)) # 创建3个线程 即3个厨师生产包子 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,)) # 创建2个线程 即2个顾客来吃包子 t.start()