Python中的线程
线程同步
概念
线程同步,线程间协同,通过某种技术,让一个线程访问某些数据时,其他线程不能访问这些数据,直到该线程完成对数据的操作.
临界区(Critical Section) 互斥量(Mutex) 信号量(Semaphore) 时间(Event)
Event事件
Event事件,是线程间通信机制中最简单的实现,使用一个内部标记的flag,通过flag的True或False的变化来进行操作.
名称 | 含义 |
---|---|
set() | 标记设置为True |
clear() | 标记设置为False |
is_set() | 标记是否为True |
wait(timeout=None) | 设置等标记为True的时长,None为无限等待.等到返回True,未等到超时返回False,不设置默认无限等 |
总结
使用同一个Event对象的标记flag
谁wait就是等到flag变为true,或等到超时返回False,不限制等待个数
wait的使用
Event的wait优于time.sleep,它会更快的切换到其他进程,提高并发效率.
Lock(互斥锁,互斥量)
锁,凡是存在共享资源争抢的地方都可以使用锁,从而保证只有一个使用者可以完全使用这个资源.
一旦线程获得锁,其他试图获取锁的线程将被阻塞
名称 | 含义 |
---|---|
acquire(blocking=True,timeout=-1) | 默认阻塞,阻塞可以设置超时时间.非阻塞时,timeout禁止设置.成功获取锁,返回True,否则返回False |
release() | 释放锁,可以从任何线程调用释放.已上锁的锁,会被重置为unlocked未上锁的锁上调用,抛RuntimeError异常 |
加锁,解锁
一般来说,有加锁就需要解锁,一旦加锁后解锁前,还要执行一些代码,就可能抛异常,锁无法释放,但是当前线程就可能因为这个异常被终止,产生死锁.一般为逻辑问题.
常用语句:
- 使用try,,,finally语句保证锁的释放
- with上下文管理,锁对象支持上下文管理
注意事项:
- 少用.使用了锁,多线程访问被锁的资源时,就成了串行,要么排队执行,要么争抢执行
- 加锁时间越短越好,不需要就立即释放锁
- 一定要避免死锁
递归锁RLock(可重入锁)
递归锁,是线程相关锁,个人认为,锁的是一个线程
线程A可获得重复锁,并可多次成功获取,不会阻塞.最后要在线程A中做和acquire次数相同的release.
当锁未释放完,其他线程获取锁会被阻塞,知道当前持有锁的线程释放完锁
Condition
构造方法:Condition(lock= None),可以掺入一个Lock或者RLock锁对象,默认RLock
名称 | 含义 |
---|---|
acquire(*args) | 获取锁 |
wait() | 等待或超时 |
notify(n= 1) | 唤醒之多指定数目个数的等待线程,没有等待的线程就没有任何操作 |
notify_all() | 唤醒所有等待的线程 |
总结
Condition用于生成者消费者模型中,解决生产者消费者速度匹配的问题.
采用通知机制,效率极高
使用方法:
使用Condition,必须先acquire,用完了要release,默认使用RLock.最好的方式是使用with上下文
消费者wait,等待通知
生产者生产好消息,对消费者发送通知,可以使用notify或者notify_all方法.
Barrier栅栏(路闸)
wait超时
semaphore信号量
和Lock很想,信号量对象内部维护一个倒计数器,每一次acquire都会减1,当acquire方法发现技术为0就阻塞请求的线程,知道其他线程对信号量release后,计数大于0,恢复阻塞的线程.
名称 | 含义 |
---|---|
Semaphore(value=1) | 构造方法.value小于0,抛ValueError异常 |
acquire() | 获取信号量,计数器减1,成功返回True |
release() | 释放信号量,计数器加1 |
计数器永远不会低于0,因为acquire的时候,发现是0,都会被阻塞.
from threading import Semaphore,Lock
from threading import current_thread
from threading import Thread
import time
sm = Semaphore(5)#5个马桶
def task():
sm.acquire()
print(f'{current_thread().name} work')
time.sleep(1)
sm.release()
for line in range(20):
t.Thread(target=task)
t.start
信号量和锁
锁,值允许同一个时间一个线程独占资源,它是特殊的信号量,即信号量计数器初值为1.
信号量,可以多个线程访问共享资源,但这个共享资源数量有限.
数据结构和GIL
Queue
标准库queue模块,提供
FIFO的队列:先进先出
LIFO的队列:后进先出
优先队列:根据参数内,数字的大小进行分级,数值越小,优先级越高(字符串会有自己的算法,不建议使用)
Queue是线程安全的,适用于多线程间安全的交换数据.内部使用了Lock,Condition
import queue
q = queue.Queue()
q.put(1)
q.put(2)
q.put(3)
print(q.get()) #1
q = queue.LifoQueue()
q.put(1)
q.put(2)
q.put(3)
print(q.get()) # 3
GIL全局解释器锁(Global Interpreter Lock)
CPython在解释器进程级别有把锁.叫GIL全局解释器锁.
GIL保证CPython进程中,只有一个线程执行字节码,甚至多核CPU情况下,也会当一个进程A中的线程执行时,阻塞其他CPU上A进程的线程.相当于,CPython中永远没有真正微观实际意义上的多线程.
CPython中,IO密集型,使用多线程;CPU密集型,使用多继承,绕开GIL
Ruby也有GIL,如果高并发的话,用erlang或者go
Python中绝大多数内置数据结构读写都是原子操作.(即不会被优先级更高的线程打断,线程安全)
由于GIL存在,Python的内置数据类型在多线程编程的时候就变程了安全的了,但是实际上他们本身不是线程安全类型.
原子操作:原子操作,就是不能被更高等级中断抢夺优先的操作。由于操作系统大部分时间处于开中断状态,所以,一个程序在执行的时候可能被优先级更高的线程中断。而有些操作是不能被中断的,不然会出现无法还原的后果,这时候,这些操作就需要原子操作。就是不能被中断的操作。