[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3jMlBs35-1589445214945)(http://prya5mckf.bkt.clouddn.com/blog/20190618/0BtjeHsSij71.jpg?imageslim)]
并行: parallel 互不干扰的同一时刻做几件事情
并发: concurrency 同一时刻有很多事情要做
1> 对列,缓冲区 # 排队 如Queue Lifoqueue PriorityQueue (小顶堆 实现)
2> 争抢 # 互相抢,哪个抢到就执行哪个
3> 预处理 # 提前处理好一些事情
4> 并行 # 多个任务同时进行
5> 提速 # 使用处理速度更快的设备 提高单个CPU性能,或者服务器安装更多的CPU
6> 消息中间件 # 如进地铁一样,设置中间排队 常见消息中间件有: 常见的消息中间件有RabbitMQ、ActiveMQ(Apache)、RocketMQ(阿里Apache)、kafka(Apache)等。
进程: 进程是计算机程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单元,是操作系统机构的基础
进程和程序的基础
程序是源代码编译后的文件,而这些文件存放在磁盘上,当程序被操作系统加载到内存中,就是进程,进程中存放着指令和数据,它也是线程的容器
Linux 进程有父进程,子进程,windows 的进程是平等关系
线程,有时被称为轻量级进程(Lightweight Process,LWP),是程序执行流的最小单元。
一个标准的线程由线程ID,当前指令指针(PC)、寄存器集合和堆栈组成。
在许多系统中,创建一个线程比创建一个进程快10-100倍。
进程、线程的理解
现代操作系统提出进程的概念,每一个进程都认为自己独占所有的计算机硬件资源。
进程就是独立的王国,进程间不可以随便的共享数据。
线程就是省份,同一个进程内的线程可以共享进程的资源,每一个线程拥有自己独立的堆栈。
线程的状态
状态 | 含义 |
---|---|
就绪 | 线程能够运行,但在等待被调度。可能线程刚刚创建启动,或刚刚从阻塞中恢复,或者被其他线程抢占 |
运行 | 线程正在运行 |
阻塞 | 线程等待外部事件发生而无法运行,如I /O操作 |
终止 | 线程完成,或退出,或被取消 |
python中的进程和线程
进程会启动一个解释器进程,线程共享一个解释器进程。
Python的线程开发使用标准库threading
进程靠线程执行代码,至少有一个主线程,其它线程是工作线程。
主线程是第一个启动的线程。
父线程:如果线程A中启动了一个线程B,A就是B的父线程。
子线程:B就是A的子线程。
Thread类
def __init__(self,group= None,target =None,name = None,args = (),kwargs = None,*,demon = None)
参数 | 含义 |
---|---|
target | 线程调用的对像,就是目标函数 |
name | 为线程起一个名字 |
args | 为目标函数传递实参,元组类型 |
kwargs | 为目标函数关键字传参,字典类型 |
demon | 继承父类的demon 为None ,表示等待线程,若为True表示不等待 |
import threading
def fun1():
paaa
t = threading.Thread(target = fun1 ,name = 'fun1')
t.start()# 启动线程
通过threading.Thread创建一个线程对象,target是目标函数,可以使用name为线程指定名称。
但是线程没有启动,需要调用start方法。
线程之所以执行函数,是因为线程中就是要执行代码的,而最简单的封装就是函数,所以还是函数调用。
函数执行完,线程也就退出了。
线程退出
python中没有提供线程退出的方法,线程在下面情况下退出
1,线程函数内语句执行完毕
2,线程函数中抛出未处理异常
线程的传参
t = threading.Thread(target = fun1,name= 'fun1',args = (1,3,3),kwargs = ('x'= 123,'y'= 456))
t.start()
threading的属性和方法
名称 | 含义 |
---|---|
current_thread() | 返回当前线程对像 |
main_thread() | 返回主线程对像 |
active_count() | 当前处于alive状态的线程个数 |
enumerate() | 返回所有活着的线程列表,不包括已终止的线程和未开始的线程 |
get_ident() | 返回当前线程的ID,非0 整数 |
active_count(),enumerate 方法返回的值还包括主线程
名称 | 含义 |
---|---|
name | 只是一个名字,只是个标识,名称可以重名。getName()、setName()获取、设置这个 |
ident | 线程ID,它是非0整数。线程启动后才会有ID,否则为None。线程退出,此ID依旧可以访问.此ID可以重复使用 |
is_alive() | 返回线程是否活着 |
start()方法会调用run()方法,而run()方法可以运行函数。
这两个方法看似功能重复了,这么看来留一个方法就可以了。是这样吗?
start和run的区别
使用start方法启动线程,启动了一个新的线程,名字叫做worker运行。但是使用run方法的,并没有启动新的线程,就是在主线程中调用了一个普通的函数而已。
因此,启动线程请使用start方法,且对于这个线程来说,start方法只能调用一次。(设置_started属性实现)
import threading
import time
def worker():
print(threading.enumerate()) # 增加这一句
for i in range(5):
time.sleep(1)
print('i am working')
print('finished')
class MyThread(threading.Thread):
def start(self):
print('start~~~~')
super().start()
def run(self):
print('run~~~~~~')
super().run()
t = MyThread(target=worker, name='worker')
t.start() # start 方法会开启新的线程来运行函数
# t.run() # 分别执行start或者run方法 ,只是在主线程运行
# 观察run 方法与start()方法的区别
多线程,一个进程中如果有多个线程运行,就是多线程,实现一种并发
import threading
import time
import datetime
def worker():
t = threading.current_thread()
for i in range(5):
time.sleep(1)
print('i am working', t.name, t.ident)
print('finished')
class MyThread(threading.Thread):
def start(self):
print('start~~~~')
super().start()
def run(self):
print('run~~~~~~')
super().run()
t1 = MyThread(target=worker, name='worker1')
t2 = MyThread(target=worker, name='worker2')
# t1.start()
# t2.start()
t1.run() # 等待t1.run 执行结束后才会执行下一句
t2.run()
通过比较run 方法和start 方法可以发现:
run 方法没有开新的线程,这就是普通函数调用,所以执行完t1.run(),然后执行t2.run(),这里就不是多线程。
当使用start方法启动线程后,进程内有多个活动的线程并行的工作,就是多线程。
一个进程中至少有一个线程,并作为程序的入口,这个线程就是主线程。
一个进程至少有一个主线程。
其他线程称为工作线程。
定义: 线程执行一段代码,不会产生不确定的结果,那么这段代码就是线程安全的
logging模块
标准库里面的logging模块,日志处理模块,线程安全的,生成环境代码都使用logging
import threading
import logging
def worker():
for x in range(100):
logging.warning('{} is running.'.format(threading.current_thread().name))
#print('{} is running.\n'.format(threading.current_thread().name), end='')
for x in range(1, 8): # 可以增加线程
name = 'worker{}'.format(x)
t = threading.Thread(target=worker, name=name)
t.start()
fmstr = "%(asctime)s%(thread)s%(threadName)s%(message)s"
logging.basicConfig(level=logging.INFO,format=fmstr) # 配置格式字符串,默认为warring
.......
logging.info("{} is running.".format(threading.current_thread().name)),上面设定了INFO 的格式后,下面就可以使用logging.info 进行打印
# 源码Thread的__init__方法中
if daemon is not None:
self._daemonic = daemon # 用户设定bool值
else:
self._daemonic = current_thread().daemon
线程daemon属性,如果设定就是用户的设置,否则就取当前线程的daemon值。
主线程是non-daemon线程,即daemon = False。
t = threading.Thread(target = worker, name = 'worker',daemon = False)
如果线程中定义了daemon 为True ,那么 主线程在运行时将不会等待其他线程
名称 | 含义 |
---|---|
daemon | 表示线程是否具有daemon 线程,这个值必须在start( )前设置,否则引发RuntimeError异常 |
isDaemon() | 是否是daemon 线程 |
setDaemon | 设置daemon 线程,必须在start ()之前设置 |
import time
import threading
def worker( name ,timeout):
time.sleep(timeout)
print('{}working'.format(name))
t1 = threading.Thread(target=worker,args = ('t1',5),name= 'worker1',daemon=True)
t1.setDaemon(False) # 可以手动给线程设定daemon值
t1.start()
print('start')
t2 = threading.Thread(target=worker,name = 'worker2',args = ('t2',3),daemon=False)
t2.start()
print('Main Thread Exits')
# 交换上面代码中timeout时间的值,可以看出,若将daemon设置为True, 那么主线程运行时将不再等待子线程的运行
从上可以得出,如果除主线程之外还有non-daemon线程的时候,主线程退出时,也不会杀掉所有daemon线程,直到
所有non-daemon线程全部结束,如果还有daemon线程,主线程需要退出(主线程退出也可以理解为最后一个
non-daemon线程也要退出了),会结束所有daemon线程,程序退出。
总结
线程具有一个daemon属性,可以手动设置为True或False,也可以不设置,则取默认值None。
如果不设置daemon,就取当前线程的daemon来设置它。
主线程是non-daemon线程,即daemon = False。
从主线程创建的所有线程的不设置daemon属性,则默认都是daemon = False,也就是non-daemon线程。
Python程序在没有活着的non-daemon线程运行时,程序退出,也就是除主线程之外剩下的只能都是daemon线
程,主线程才能退出,否则主线程就只能等待。
import time
import threading
def worker(name, timeout):
time.sleep(timeout)
print('{} working'.format(name))
t1 = threading.Thread(target=worker, args=('t1', 3), daemon=True)
t1.start()
t1.join()# 设置join,取消join对比一下
print('Main Thread Exits')
使用了join方法后,daemon线程执行完了,主线程才退出
import time
import threading
def worker(name, timeout):
time.sleep(timeout)
print('{} working'.format(name))
t1 = threading.Thread(target=worker, args=('t1',3), daemon=True)
t1.start()
t1.join(2) # 等待daemon线程2 秒
print('+++++++++++')
t1.join(2) # 在等待daemon线程2 秒
print('~~~~~~~~~~~')
print('Main Thread Exits')
#结果如下:
+++++++++++
t1 working
~~~~~~~~~~~
Main Thread Exits
# 可以看出仅仅打印出了一个线程working ,因为第一个join()等待的时间小于线程函数睡眠时间,所以在等待2s后退出等待,主线程继续运行
join(timeout=None),是线程的标准方法之一。
一个线程中调用另一个线程的join方法,调用者将被阻塞,直到被调用线程终止。
一个线程可以被join多次。
timeout参数指定调用者等待多久,没有设置超时,就一直等到被调用线程结束。
调用谁的join方法,就是join谁,就要等谁。
简单来说就是,本来并没有 daemon thread,为了简化程序员的工作,让他们不用去记录和管理那些后台线程,
创造了一个 daemon thread 的概念。这个概念唯一的作用就是,当你把一个线程设置为 daemon,它可以会随主
线程的退出而退出。
主要应用场景有:
1、后台任务。如发送心跳包、监控,这种场景最多。
2、主线程工作才有用的线程。如主线程中维护这公共的资源,主线程已经清理了,准备退出,而工作线程使用这
些资源工作也没有意义了,一起退出最合适。
3、随时可以被终止的线程
如果主线程退出,想所有其它工作线程一起退出,就使用daemon=True来创建工作线程。
比如,开启一个线程定时判断WEB服务是否正常工作,主线程退出,工作线程也没有必须存在了,应该随着主线程
退出一起退出。这种daemon线程一旦创建,就可以忘记它了,只用关心主线程什么时候退出就行了。
daemon线程,简化了程序员手动关闭线程的工作。
如果在non-daemon线程A中,对另一个daemon线程B使用了join方法,这个线程B设置成daemon就没有什么意
义了,因为non-daemon线程A总是要等待B。
如果在一个daemon线程C中,对另一个daemon线程D使用了join方法,只能说明C要等待D,主线程退出,C和D
不管是否结束,也不管它们谁等谁,都要被杀掉。
threading.local类
先看一个例子:
import threading
import time
def worker():
x = 0
for i in range(100):
time.sleep(0.05)
x+=1
print(threading.current_thread(),x,end = '')
print('~~~~~~~~~')
for i in range(10):
threading.Thread(target = worker).start()
上例使用线程,每个线程完成不同的计算任务.
x是局部变量,可以看出每一个线程的x的独立的,互不干扰,
能否改造成使用全局变量完成?
import threading
import time
class A :
def __init__(self):
self.x = 0
global_data =A()
def worker():
global_data.x = 0
for i in range(100):
time.sleep(0.05)
global_data.x +=1
print(threading.current_thread().global_data.x) # 当读取数据时,可能有线程正在调用全局变量,进行加操作,所以最后的结果将不可预期
for i in range(10):
threading.Thread(target =worker,name = 'worker')
上例中虽然使用了全局变量,但是线程之间互相干扰,导致了不期望的结果
能不能即使用全局变量,还能保持每个线程之间使用不同的数据呢?
python提供了threading.local类,将这个实例化得到一个全局变量,但是不同的线程使用这个线程存储的数据其他线程看不见
import threading
import time
X = 'ABC'
global_data = threading.local()
global_data.x = 100
print(global_data,type(global_data),global_data.x)
def worker():
print(X)
print(global_data)
print(global_data.x)
print('in worker')
worker()
print('-'*30)
threading.Thread(target=worker,name = 'worker').start()
从运行结果上来看,另起一个线程打印global_data.x 出差了,AttributeError: '_thread._local' object has no attribute 'x'
但是global_data 打印没有出错,说名能看到global-data, 但是global _data中的x 看不到,这个x 不能跨线程
# 上例可以改为如下代码
import threading
import time
global_data = threading.local()
def worker():
global_data = 0
for i in range(100):
time.sleep(0.05)
global_data +=1
print(threading.current_thread(),global_data)
for i in range(10):
threading.Thread(target =worker,name = 'workr').start()
本质
运行时,threading.local实例处在不同的线程中,就从大字典中找到当前线程相关键值对中的字典,覆盖
threading.local实例的__dict__
。
这样就可以在不同的线程中,安全地使用线程独有的数据,做到了线程间数据隔离,如同本地变量一样安
全。
线程同步,线程间协同,通过某种技术,让一个线程访问某些数据时,其他线程不能访问这些数据,直到该线程完成对数据的操作
Event 事件,是线程间通信机制中最简单的实现,使用一个内部标记Flag ,通过flag 的True,或者False 是变化来进行操作.
名称 | 含义 |
---|---|
set() | 标记设置为True |
clear() | 标记设置为False |
is_set() | 标记是否为True |
wait(timeout= None) | 设计等待标记True的时长,None 为无限等待,等到True ,未等到超时了返回False |
#练习
#老板雇佣了一个工人,让他生产杯子,老板一直等着这个工人,直到生产了10个杯子
from threading import Event, Thread
# import threading
import logging
import time
event = Event()
# event2 = threading.Event
FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)
def worker(event:Event, count=10):
logging.info('I am working for U')
cups = []
while True:
logging.info('make 1 cup')
time.sleep(0.5)
cups.append(1)
if len(cups) >= count:
event.set()
break
logging.info('I finished my job. cups={}'.format(cups))
def boss(event:Event):
logging.info("I'm boss, waiting for U")
# 阻塞等待
event.wait()
logging.info('Good Job.')
b = Thread(target=boss, name='boss', args=(event,))
w = Thread(target=worker, name='worker', args=(event,))
b.start()
w.start()
总结:
使用同一个Event 对象的标记flag
谁wait 谁就是等到flag 变为True, 或者等到超时返回False, 不限制等待的个数
wait 的使用
from threading import Event, Thread
# import threading
import logging
import time
event = Event()
# event2 = threading.Event
FORMAT = '%(asctime)s %(threadName)s %(thread)s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)
def worker(event:Event, interval:int):
while not event.wait(interval): # 等不到返回False,(等到3秒后仍然没有人event.set(),就返回False )
logging.info('do sth')
w = Thread(target=worker, name='worker', args=(event,3))
w.start()
print('+++end++++')
threading.Timer 继承自Thread ,这个类用来定义延迟多久后执行一个函数
class threading.Timer(interval,function,args=None,kwargs = None)
start方法执行之后,Timer 对象会处于等待状态,等待interval秒之后,开始执行function 函数
import threading
import time
import logging
format = "%(asctime)s%(threadName)s %(threadName)s %(thread)s %(message)s"
logging.basicConfig(level=logging.INFO,format=format)
def worker():
logging.info('in work')
time.sleep(2)
t = threading.Timer(4,worker)
t.cancel() # 线程没有起到之前可以取消
t.start()
# t.cancel()线程即使启动,但只要在wait ()等待时间内,都可以取消,本质是调用的Event()函数
while True:
print(threading.enumerate())
time.sleep(1)
# 本质是在cancel类中定义了:
def __init__(self):
....
self.finiseed= Event()
..
def cancel(self):
....
self.finished.set()
上述代码工作县城关早就启动了,只不过是在工作线程中延时等待了4秒才执行worker函数,
Timer 是线程Thread 的子类,Timer 实例内部提供了一个finished 属性,该属性是Event 对象, cancel方法,是在worker 函数执行前对finished 属性set 方法操作,从而跳过了worker 函数执行,达到了取消的效果,
总结:
Timer 是线程Thread 的子类,就是线程类,具有线程的能力和特征,
它的实例是能够延时执行目标函数的线程,在真正执行目标函数之前,都可以cancel 它
cancel 方法本质使用Event 类实现,这并不是说线程提供了取消的方法,线程一旦被创建,不可被取消
Lock
锁,一旦线程获得锁,其他试图获取锁的线程将被阻塞
锁,凡是存在共享资源争抢的地方都可以使用锁,从而保证只有一个使用者可以完全使用这个资源
名称 | 含义 |
---|---|
acquire(block =True,timeout = -1) | 默认阻塞,阻塞可以设置超时时间。非阻塞时,timeout禁止设置。成功获取锁,返回True,否则返回False |
release() | 释放锁。可以从任何线程调用释放。 已上锁的锁,会被重置为unlocked未上锁的锁上调用,抛RuntimeError异常。 |
锁的基本使用:
先看一个例子:
订单要求生产1000个杯子,组织10个工人生产,请忽略老板,关注工人生产杯子
import logging
import threading
import time
event = threading.Event()
fmstr = "%(asctime)s%(thread)s%(threadName)s%(message)s"
logging.basicConfig(level=logging.INFO,format=fmstr)
cups = []
def worker(count):
logging.info("{} if working".format(threading.current_thread().name))
while len(cups)< count: # 存在大问题 最后的数目会超出目标值
time.sleep(0.0001)
cups.append(1)
logging.info('{}finished my job.cups={}'.format(threading.current_thread().name,len(cups)))
for i in range(10):
threading.Thread(target=worker, name='worker{}'.format(i),args=(1000,)).start()
上面可以看出,多线程调度,导致了判断失败,多生产了杯子,可以使用Event 加判断.
有什么好的解决方案呢?
上锁!
import logging
import threading
import time
event = threading.Event()
fmstr = "%(asctime)s%(thread)s%(threadName)s%(message)s"
logging.basicConfig(level=logging.INFO ,format=fmstr) # 配置格式字符串,默认为warring
cups = []
lock = threading.Lock()
def worker(lock ,count):
logging.info("{} if working".format(threading.current_thread().name))
flag = False
while True: # 存在大问题 最后的数目会超出目标值
lock.acquire()
if len(cups)>=count:
flag2 =True
time.sleep(0.0001)
if not flag:
cups.append(1)
if flag:
lock.release()
if flag :
break
logging.info('{}finished my job.cups={}'.format(threading.current_thread().name ,len(cups)))
for i in range(10):
threading.Thread(target=worker, name='worker{}'.format(i) ,args=(lock ,10000,)).start()
加锁,解锁
一般来说,加锁就需要解锁,但是加锁后解锁,还要有一些代码执行,就可能抛异常,一旦出现异常,锁是无法释放,但是当前线程可能因为这个异常被终止了,这也就产生了死锁.
加锁解锁常用语句:
1,使用try …finally 语句保证锁的释放.
2,with 上下文管理,锁对象支持上下文管理
import threading
import time
class Counter:
def __init__(self):
self._value =0
@property
def value(self):
return self._value
def inc(self):
self._value+=1
def dec (self):
self._value-=1
def balabce(c:Counter,loop:int):
for i in range(loop):
for j in range(-50,50):
if j< 0 :
c.dec()
else:
c.inc()
c = Counter()
l1=10# 线程数
l2=100#loop
for i in range(l1):
t = threading.Thread(target=balabce,args=(c,l2))
t.start()
# 达不到平衡,因为在多个线程同时操作时,比如现在有一个线程得到当前数值为1,
# 另外一个线程得到的数值也是1 那么两个线程的值都是加 1 ,之间存在不同步,
# 所以最后的结果不是0
print(c.value)
# with threading.Lock() :
# 存在上下文管理, 有__ enter__ 和__release__
# 不一定能保证一定会执行,有try : .... finally :....,所以用上下文管理会更好
# 对锁使用上下文管理
import threading
import time
class Counter:
def __init__(self):
self._value =0
self._lock = threading.Lock()
@property
def value(self):
with self._lock:
return self._value
def inc(self):
with self._lock:
self._value+=1
def dec (self):
with self._lock :
self._value-=1
def balabce(c:Counter,loop:int):
for i in range(loop):
for j in range(-50,50):
if j< 0 :
c.dec()
else:
c.inc()
c = Counter()
l1=10# 线程数
l2=100#loop
thread_list=[]
for i in range(l1):
t = threading.Thread(target=balabce,args=(c,l2))
t.start()
thread_list.append(t)
for t in thread_list:
t.join() # 不加阻塞的话,下面的读取数据,可能线程还没执行完就去取数据导致数据不准确
print(c.value)
对读取数据部分进行改造:
while True:
time.sleep(1)
if threading.active_count()==1:
print(threading.enumerate())
print(c.value)
break
else:
print(threading.enumerate())
说明:
ptinr(c.value)
这一句在主线程中时,很早就执行了.退出条件是只剩下主线程的时候,这样改造后,代码可以保证最后得到value 值一定是0
锁的应用场景
锁适用于访问和修改同一个共享资源的时候,即读写同一个资源的时候。
如果全部都是读取同一个共享资源需要锁吗?
不需要。因为这时可以认为共享资源是不可变的,每一次读取它都是一样的值,所以不用加锁
使用锁的注意事项:
不使用锁,有了效率,但是结果是错的。
使用了锁,效率低下,但是结果是对的。
所以,我们是为了效率要错误结果呢?还是为了对的结果,让计算机去计算吧
非阻塞锁使用
import threading
import logging
import time
FORMAT = '%(asctime)s %(threadName)s %(thread)-10d %(message)s'
logging.basicConfig(level=logging.INFO, format=FORMAT)
lock = threading.Lock()
def worker(l:threading.Lock):
while True:
flag = l.acquire(False) # 非阻塞,会不断尝试去拿锁
if flag:
logging.info('do something.') # 为了显示效果,没有释放锁
else:
logging.info('try again')
time.sleep(1)
for i in range(5):
threading.Thread(target=worker, name='worker={}'.format(i), args=(lock, )).start()
可重入锁,是线程相关的锁.
线程A获得可重复锁,并可以多次成功获得,不会阻塞,最后要在线程A中做和acquire次数相同的release
import threading
lock = threading.RLock()
print(lock.acquire()) #True
print(lock.acquire(False))#True # 非阻塞锁,直接拿到锁
print(lock.acquire(timeout = 4))#True # 直接拿到锁,不需要等
print(lock.acquire()) #True
RLock 会立即得到4把锁
#比较与lock=threading.Lock()
# print(lock.acquire()) # 第一个会得到锁 返回True
# print(lock.acquire(False)) # 非阻塞锁,得不到锁
# print(lock.acquire(timeout = 4))# 阻塞4秒返回False,因为第一个锁没有释放
# print(lock.acquire()) # 阻塞锁,返回False ,当前线程阻塞.
lock.release()
lock.release()
lock.release()
#lock.release()
def sub(l):
print('enter')
print(l.acquire())# 若主线程拿了4把锁,但是只还了三把锁,这里要是在拿锁便会拿不到
print(threading.current_thread().ident)
print(threading.main_thread().ident)
print(l.acquire(timeout=4))
print('exit')
# l.release()
t = threading.Thread(target=sub,name='sub',args=(lock,))
t.start()
# 在可重入锁中:存在特殊情况
# 当主线程的锁没有释放,子线程是无法在使用锁.
# 当子线程中拿到了锁,在其他线程中也无法释放该锁,否则其他线程无法使用该锁
可重入锁:
Condition
构造方法Condition(lock=None),可以传入一个Lock或RLock对象,默认是RLock。
名称 | 含义 |
---|---|
acquire(*args) | 获取锁 |
wait(self,timeout= None) | 等待或者超时 |
notify(n=1) | 唤醒至多指定数目个数的等待的线程,没有等待的线程就没有任何操作 |
一个工人生产1000个杯子,有2个老板等到他生产完为止
import threading
import time
from threading import Thread
import logging
FORMAT= "%(asctime)s%(thread)s%(threadName)s%(message)s"
logging.basicConfig(format= FORMAT,level = logging.INFO)
cups =[]
cond= threading.Condition()
event = threading.Event()
def boss():
print('I am waiting')
with cond: # 利用上下文管理
cond.wait()# 等待阻塞中
print('Good job')
def worker(count):
print('I am working')
with cond: # 上下文管理
while len(cups)< count:
time.sleep(0.001)
cups.append(1)
print('I finished my job{}'.format(len(cups)))
cond.notify(2) # 通知两个等待者
# cond.notify(1)通知一个等待者
# cond.notify_all# 通知所有等待者
threading.Thread(target=boss,name = 'boss').start()
threading.Thread(target=boss,name = 'boss').start()
threading.Thread(target=worker,name='worker',args = (1000,)).start()
print('====')
Condition 用于生产者消费者模型,为了解决生产者消费者速度匹配问题
在看一个例子,消费者速度大于生产者速度
import random
import logging
import threading
FORMAT= "%(asctime)s %(thread)s %(threadName)s %(message)s"
logging.basicConfig(format= FORMAT,level = logging.INFO)
class Dispacter:
def __init__(self):
self.data= None
self.event = threading.Event()
self.cond = threading.Condition()
def product(self,count = 100):
for i in range(count):
data = random.randint(1,100)
logging.info(data) # 打印下信息
self.data = data
self.event.wait(1) # 模拟生产数据需要耗时 1 秒
# time.sleep(1) # 与上面一样.都是模拟等待一秒
def consume(self):
# print('consume')
while not self.event.wait(0.5):
data = self.data
logging.info('recived{}'.format(data))
d = Dispacter()
p = threading.Thread(target=d.product,name = 'producer')
for i in range(2):
c = threading.Thread(target= d.consume,name ='consumer{}'.format(i))
c.start()
p.start()
这个例子采用了消费者主动消费,消费者浪费了大量时间,主动来查看有没有数据.
能否换成一种通知机制,有数据通知消费者来小费呢?
使用condition
import random
import threading
import logging
import Event
FORMAT= "%(asctime)s %(thread)s %(threadName)s %(message)s"
logging.basicConfig(format= FORMAT,level = logging.INFO)
class Dispacter:
def __init__(self):
self.data= None
self.event = threading.Event()
self.cond = threading.Condition()
def product(self,count = 100):
for i in range(count):
data = random.randint(1,100)
with self.cond:
logging.info(data) # 打印下信息
self.data = data
self.cond.notify_all()
self.event.wait(1) # 模拟生产数据需要耗时 1 秒
# time.sleep(1) # 与上面一样.都是模拟等待一秒
def consume(self):
# print('consume')
while not self.event.is_set(): # 先等待0.5秒,若有设置为set()则返回True ,否则返回False
with self.cond:
self.conde.wait()
data = self.data
logging.info('recived{}'.format(data))
# self.data = None
# self.event.wait(0.5)
d = Dispacter()
p = threading.Thread(target=d.product,name = 'producer')
for i in range(2)
c = threading.Thread(target= d.consume,name ='consumer{}'.format(i))
c.start()
p.start()
多个消费者使用的是cond.notify_all() 若想生产一个产品给一个消费者,那么只需要设置为cond.notify(1)即可,后面便会一次交替执行.
Condition总结:
Condition 用于生产者消费模型中,解决生产者消费速度匹配问题,采用了通知机制,效率非常高
使用方式
使用condition ,必须先acquire ,用完了release . 因为内部使用了锁,默认使用Rlock锁,最好的方式是使用上下文管理
消费者wait ,等待通知
生产者生产好消息,对消费者发通知,可以使用notify 或者notify_all 方法
和lock 很像,信号量对象内部维护一个倒数计数器,每一次acquire 都会减1 ,当acquire方法发现技术为0 就阻塞请求的线程,直到其他线程对信号量release 后,计数大于0恢复阻塞线程
名称 | 含义 |
---|---|
Semaphore(value =1) | 构造方法,value 小于1 ,抛valueError 异常 |
acquire(blocking =True,timeout =None) | 获取信号量,计数器减1 ,获取成功返回True |
release() | 释放信号量,计数器加1 |
计数器永远不会低于0 ,因为acquire 的时候,发现是0 ,都会被阻塞
s = Semaphore(3)
print(s.acquire())
print(s.acquire())
print(s.acquire())
# print(s.acquire()) # 阻塞,只允许获取三把锁,再多便会阻塞
print(s._value) # 有几把锁空闲就返回几,这里返回0
s.release()
s.release()
print(s._value)# 返回2 上面释放了2次
s.release()
s.release()
s.release()
s.release()
print(s._value)# 返回6
#超过一开始给定的值,没还一次,下面可以使用的次数就加一次
# 在不同线程中操作的是同一个对象,在线程中归还是一样的.
在看
import random
import logging
import threading
import time
from threading import Thread ,Semaphore
FORMAT= "%(asctime)s %(thread)s %(threadName)s %(message)s"
logging.basicConfig(format= FORMAT,level = logging.INFO)
def worker(s:Semaphore):
logging.info('in worker thread')
logging.info(s.acquire()) # 线程中获取锁,如果主线程中 s._value 的值等于0 ,那么线程中是不能获取锁的,将会被阻塞
logging.info('worker thread over')
# 信号量
s = Semaphore(3)
logging.info(s.acquire())
print(s._value)
logging.info(s.acquire())
print(s._value)
logging.info(s.acquire())
print(s._value)
Thread(target=worker,name = 'worker',args=(s,)).start()
time.sleep(2)
logging.info(s.acquire(False)) # 非阻塞
logging.info(s.acquire(timeout=3)) # 阻塞3 秒
logging.info('relsese one')
s.release()
release 方法超界问题
假设如果没有acquire 信号量,就release,会怎么样?
结果就是会比设定的值继续往上追加,大于设定值.这就要使用到下面的方法
有界的信号量,不允许使用release超出初始值的范围,否则,抛出ValueError异常。
将上例的信号量改成有界的信号量试一试。
应用举例:
连接池
因为资源有限,且开启一个连接成本高,所以使用连接池
一个简单的连接池
连接池应该有容量,(总数),有一个工厂方法可以获取连接,能够把不用的连接返回,供其他调用者使用
class Conn: # 创建一个连接对象
def __init__(self, name):
self.name = name
class Pool: # 连接池
def __init__(self, count:int):
self.count = count
# 池中提前放着连接备用
self.pool = [self._connect('conn-{}'.format(i)) for i in range(self.count)]
self.lock = threading.Lock()
def _connect(self, conn_name):
# 创建连接的方法,返回一个连接对象
return Conn(conn_name)
def get_conn(self):
with self.lock:# 多线程情况下会发生数据不确定性,所以要加锁
# 从池中拿走一个连接
if len(self.pool) > 0:
return self.pool.pop()
def return_conn(self, conn: Conn):
# 向池中返回一个连接对象
with self.lock:
if len(self.pool)< self.count:
self.pool.append(conn)
真正的连接池的实现比上面的例子要复杂的多,这里只是简单的一个功能的实现。
本例中,get_conn()方法在多线程的时候有线程安全问题。
假设池中正好有一个连接,有可能多个线程判断池的长度是大于0的,当一个线程拿走了连接对象,其他线程再来
pop就会抛异常的。如何解决?
1、加锁,在读写的地方加锁
2、使用信号量Semaphore
对上面的代码进行修改:
import random
import logging
import threading
import time
from threading import Thread ,Semaphore
FORMAT= "%(asctime)s %(thread)s %(threadName)s %(message)s"
logging.basicConfig(format= FORMAT,level = logging.INFO)
class Conn:
def __init__(self, name):
self.name = name
class Pool:
def __init__(self, count:int):
self.count = count
# 池中提前放着连接备用
self.pool = [self._connect('conn-{}'.format(i)) for i in range(self.count)]
self.semaphore = threading.BoundedSemaphore(count)
def _connect(self, conn_name):
# 创建连接的方法,返回一个连接对象
return Conn(conn_name)
def get_conn(self):
# 从池中拿走一个连接
logging.info('get~~~~~~~~~~~~')
self.semaphore.acquire()
logging.info('-------------')
return self.pool.pop()
def return_conn(self, conn: Conn):
logging.info('return~~~~~~~~~~')
# 向池中返回一个连接对象
self.pool.append(conn)
self.semaphore.release() # 要放在append 后面,若放前面,先释放了release ,但是容器内没有数据,其他线程去取数据时会发现是空的,报错
# 初始化连接池
pool = Pool(3)
def worker(pool:Pool):
conn = pool.get_conn()
logging.info(conn)
# 模拟使用了一段时间
time.sleep(random.randint(1, 5))
pool.return_conn(conn)
for i in range(6):
threading.Thread(target=worker, name='worker-{}'.format(i), args=(pool,)).start()
补充:
import uuid
print(uuid.uuid4().hex)
这里返回的是不同的实例的ID
上例中,使用信号量解决资源有限问题
如果池中有资源,请求者获取资源时信号量减1,拿走资源。当请求超过资源数,请求者只能等待。当使用者用完
归还资源后信号量加1,等待线程就可以被唤醒拿走资源。
注意:这个连接池的例子不能用到生成环境,只是为了说明信号量使用的例子,连接池还有很多未完成功能。
问题:
self.append(conn)这一句有哪些问题考虑
1,边界问题分析
return_conn方法可以单独执行,有可能多归还连接,也就是会多release,所以,要用有界信号量
BoundedSemaphore类。
这样用有界信号量修改源代码,保证如果多return_conn就会抛异常。
self.pool.append(conn)
self.semphore.release()
假设一种极端情况,计数器还差1就归还满了,有三个线程A、B、C都执行了第一句,都没有来得及release,这时
候轮到线程A release,正常的release,然后轮到线程C先release,一定出问题,超界了,直接抛异常。
因此信号量,可以保证,一定不能多归还。
如果归还了同一个连接多次怎么办,重复很容易判断。
这个程序还不能判断这些连接是不是原来自己创建的,这不是生成环境用的代码,只是简单演示。
2,正常使用分析
正常使用信号量,都会先获取信号量,然后用完归还。
创建很多线程,都去获取信号量,没有获得信号量的线程都阻塞。能归还的线程都是前面获取到信号量的线程,其
他没有获得线程都阻塞着。非阻塞的线程append后才release,这时候等待的线程被唤醒,才能pop,也就是没有
获取信号量就不能pop,这是安全的。
经过上面的分析,信号量比计算列表长度好,线程安全。
信号量,可以多个线程访问共享资源,但这个共享资源数量有限。
锁,可以看做特殊的信号量,即信号量计数器初值为1。只允许同一个时间一个线程独占资源。
Queue
**<**br/>标准库queue模块,提供FIFO的Queue、LIFO的队列、优先队列。
Queue类是线程安全的,适用于多线程间安全的交换数据。内部使用了Lock和Condition。
为什么讲魔术方法时,说实现容器的大小,不准确?
如果不加锁,是不可能获得准确的大小的,因为你刚读取到了一个大小,还没有取走数据,就有可能被其他线程改
了。
Queue类的size虽然加了锁,但是,依然不能保证立即get、put就能成功,因为读取大小和get、put方法是分开
的。
GIL全局解释器锁
CPython 在解释器进程级别有一把锁,叫做GIL,即全局解释器锁。
GIL 保证CPython进程中,只有一个线程执行字节码。甚至是在多核CPU的情况下,也只允许同时只能有一个CPU
上运行该进程的一个线程。
CPython中
IO密集型,某个线程阻塞,就会调度其他就绪线程;
CPU密集型,当前线程可能会连续的获得GIL,导致其它线程几乎无法使用CPU。
在CPython中由于有GIL存在,IO密集型,使用多线程较为合算;CPU密集型,使用多进程,要绕开GIL。
新版CPython正在努力优化GIL的问题,但不是移除。
如果在意多线程的效率问题,请绕行,选择其它语言erlang、Go等。
Python中绝大多数内置数据结构的读、写操作都是原子操作。
由于GIL的存在,Python的内置数据类型在多线程编程的时候就变成了安全的了,但是实际上它们本身 不是
线程安全类型。