一、多线程定义
进程是由若干线程组成的,一个进程至少有一个线程,叫主线程。 多线程类似于同时执行多个不同程序,多线程运行有如下优点:
- 使用线程可以把占据长时间的程序中的任务放到后台去处理,不会出现界面卡顿的情况。
- 用户界面更加友好,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。
- 程序的运行速度可能加快。
- 在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下我们可以释放一些珍贵的资源如内存占用等等。
线程在执行过程中与进程还是有区别的。每个独立的进程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,多个线程共享进程的资源,比如内存,文件句柄等。
每个线程都有他自己的一组CPU寄存器,称为线程的上下文,该上下文反映了线程上次运行该线程的CPU寄存器的状态。
指令指针和堆栈指针寄存器是线程上下文中两个最重要的寄存器,线程总是在进程得到上下文中运行的,这些地址都用于标志拥有线程的进程地址空间中的内存。
- 线程可以被抢占(中断)。
- 在其他线程正在运行时,线程可以暂时搁置(也称为睡眠) -- 这就是线程的退让。
Python的线程虽然是真正的线程,但解释器执行代码时,有一个GIL锁:Global Interpreter Lock,任何Python线程执行前,必须先获得GIL锁,然后,每执行100条计步(ticks)(也可以认为是虚拟机指令或字节码),解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。
GIL是Python解释器设计的历史遗留问题,通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。不过Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。
线程的生命周期:
- 新建:使用线程的第一步就是创建线程,创建后的线程只是进入可执行的状态,也就是Runnable
- Runnable:进入此状态的线程还并未开始运行,一旦CPU分配时间片给这个线程后,该线程才正式的开始运行
- Running:线程正式开始运行,在运行过程中线程可能会进入阻塞的状态,即Blocked
- Blocked:在该状态下,线程暂停运行,解除阻塞后,线程会进入Runnable状态,等待CPU再次分配时间片给它
- 结束:线程方法执行完毕或者因为异常终止返回
线程从Running进入Blocked状态,通常有三种情况:
- 睡眠:线程主动调用sleep()或join()方法后.
- 等待:线程中调用wait()方法,此时需要有其他线程通过notify()方法来唤醒
- 同步:线程中获取线程锁,但是因为资源已经被其他线程占用时.
二、Python用法
由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。需要注意的Python多用于处理数据,所以多线程用的地方并不像其他高级语言(如Java,C++)一样多,因为GIL的限制,更多的情况下用多进程,多线程更适用于IO操作。
Python3 通过两个标准库 _thread (python2中是thread模块)和 threading 提供对线程的支持。_thread 提供了低级别的、原始的线程以及一个简单的锁,它相比于 threading 模块的功能还是比较有限的。
(1) 设置GIL
(2) 切换到一个线程去运行
(3) 运行(CPU调度过程):
a. 指定数量的字节码的指令,或者
b. 线程主动让出控制(可以调用time.sleep(0))
(4) 把线程设置为睡眠状态
(5) 解锁GIL
(6) 再次重复以上所有步骤
验证GIL:
在一个Python进程中,GIL只有一个。
为了验证确实是 GIL 的问题,我们可以用不同的解释器再执行一次。这里使用 pypy(有 GIL)和 jython (无 GIL)作测试:
# PyPy, fib
Time elapsed with 1 branch(es): 0.868052 sec(s)
Time elapsed with 2 branch(es): 1.706454 sec(s)
Time elapsed with 3 branch(es): 2.594260 sec(s)
Time elapsed with 4 branch(es): 3.449946 sec(s)
# Jython, fib
Time elapsed with 1 branch(es): 2.984000 sec(s)
Time elapsed with 2 branch(es): 3.058000 sec(s)
Time elapsed with 3 branch(es): 4.404000 sec(s)
Time elapsed with 4 branch(es): 5.357000 sec(s)
从结果可以看出,用 pypy 执行时,时间开销和线程数也是几乎成正比的;而 jython 的时间开销则是以较为缓慢的速度增长的。jython 由于下面还有一层 JVM,单线程的执行速度很慢,但在线程数达到 4 时,时间开销只有单线程的两倍不到,仅仅稍逊于 cpython 的 4 线程运行结果(5.10 secs)。由此可见,GIL 确实是造成伪并行现象的主要因素。为了解决这个问题,多线程无法实现,需要用到多进程,在其他文章有所介绍。
GIL也有好处:
- 可以增加单线程程序的运行速度(不再需要对所有数据结构分别获取或释放锁)
- 容易和大部分非线程安全的 C 库进行集成
- 容易实现(使用单独的 GIL 锁要比实现无锁,或者细粒度锁的解释器更容易)
2.1 定义
Python中使用线程有两种方式:函数或者用类来包装线程对象。
Python的标准库提供了两个模块:thread
和threading
,thread
是低级模块,threading
是高级模块,对thread
进行了封装。绝大多数情况下,我们只需要使用threading
这个高级模块。
(1)函数式:调用thread模块中的start_new_thread()函数来产生新线程。
基本语法:
thread.start_new_thread ( function, args[, kwargs] )
参数说明:
- function - 线程函数。
- args - 传递给线程函数的参数,必须是个tuple类型。保证不可变类似于Java要求是final。
- kwargs - 可选参数。
start_new_thread()要求一定要有前两个参数,即使运行的函数不要参数,也要传一个空的元组。
示例:
# 为线程定义一个函数
def print_time( threadName, delay):
count = 0
while count < 5:
time.sleep(delay)
count += 1
print "%s: %s" % ( threadName, time.ctime(time.time()) )
# 创建两个线程
try:
thread.start_new_thread( print_time, ("Thread-1", 2, ) )
thread.start_new_thread( print_time, ("Thread-2", 4, ) )
except:
print "Error: unable to start thread"
while 1:
pass
Thread-1: Thu Jan 22 15:42:17 2009
Thread-1: Thu Jan 22 15:42:19 2009
Thread-2: Thu Jan 22 15:42:19 2009
Thread-1: Thu Jan 22 15:42:21 2009
Thread-2: Thu Jan 22 15:42:23 2009
Thread-1: Thu Jan 22 15:42:23 2009
Thread-1: Thu Jan 22 15:42:25 2009
Thread-2: Thu Jan 22 15:42:27 2009
Thread-2: Thu Jan 22 15:42:31 2009
Thread-2: Thu Jan 22 15:42:35 2009
(2)使用threading模块创建线程。
1)直接从threading.Thread继承,然后重写__init__方法和run方法,用法类似于Java的Thread类:
class myThread (threading.Thread): #继承父类threading.Thread
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self): #把要执行的代码写到run函数里面 线程在创建后会直接运行run函数
print "Starting " + self.name
print_time(self.name, self.counter, 5)
print "Exiting " + self.name
def print_time(threadName, delay, counter):
while counter:
if exitFlag:
(threading.Thread).exit()
time.sleep(delay)
print "%s: %s" % (threadName, time.ctime(time.time()))
counter -= 1
# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 开启线程
thread1.start()
thread2.start()
print "Exiting Main Thread"
Starting Thread-1
Starting Thread-2
Exiting Main Thread
Thread-1: Thu Mar 21 09:10:03 2013
Thread-1: Thu Mar 21 09:10:04 2013
Thread-2: Thu Mar 21 09:10:04 2013
Thread-1: Thu Mar 21 09:10:05 2013
Thread-1: Thu Mar 21 09:10:06 2013
Thread-2: Thu Mar 21 09:10:06 2013
Thread-1: Thu Mar 21 09:10:07 2013
Exiting Thread-1
Thread-2: Thu Mar 21 09:10:08 2013
Thread-2: Thu Mar 21 09:10:10 2013
Thread-2: Thu Mar 21 09:10:12 2013
Exiting Thread-2
最常用:2)继承一个类,对Java来说很正常也很常用,但是对于Python来说有点浪费,Python更喜欢函数式编程。所以另一种方式是启动一个线程就是把一个函数传入并创建Thread
实例,然后调用start()
开始执行:
实例化一个Thread调用Thread()方法与调用thread.start_new_thread()之间的最大区别是:新的线程不会立即开始。在你创建线程对象,但不想马上开始运行线程的时候,这是一个很有用的同步特性。
实际上首先Thread类会检测传入的target是否是None,如果是则执行内部的run()方法;如果不是None,说明传入了一个目标执行函数target,执行target即可。
args: 线程执行方法接收的参数,该属性是一个元组,如果只有一个参数也需要在末尾加逗号。
# 新线程执行的代码:
def loop():
print 'thread %s is running...' % threading.current_thread().name
n = 0
while n < 5:
n = n + 1
print 'thread %s >>> %s' % (threading.current_thread().name, n)
time.sleep(1)
print 'thread %s ended.' % threading.current_thread().name
print 'thread %s is running...' % threading.current_thread().name
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join() # 为了线程执行结束打印主线程名。等待调用线程执行结束。
print 'thread %s ended.' % threading.current_thread().name
thread MainThread is running...
thread LoopThread is running...
thread LoopThread >>> 1
thread LoopThread >>> 2
thread LoopThread >>> 3
thread LoopThread >>> 4
thread LoopThread >>> 5
thread LoopThread ended.
thread MainThread ended.
3) 创建一个Thread实例,传给它一个可调用的类对象
这是第二个方法,与传一个函数很相似,但它是传一个可调用的类的实例供线程启动的时候执行,这是多线程编程的一个更为面向对象的方法。相对于一个或几个函数来说,类对象可以保存更多的信息,这种方法更为灵活。
许多的python 对象都是我们所说的可调用的,即是任何能通过函数操作符“()”来调用的对象(见《python核心编程》第14章)。类的对象也是可以调用的,当被调用时会自动调用对象的内建方法__call__(),因此这种新建线程的方法就是给线程指定一个__call__方法被重载了的对象。
loops = [4,2] #睡眠时间
class ThreadFunc(object):
def __init__(self, func, args, name=''):
self.name=name
self.func=func
self.args=args
def __call__(self): # 实际的函数执行体,执行逻辑
apply(self.func, self.args)
def loop(nloop, nsec):
print "Start loop", nloop, 'at:', ctime()
sleep(nsec)
print 'Loop', nloop, 'done at:', ctime()
def main():
print 'Starting at:', ctime()
threads=[]
nloops = range(len(loops)) #列表[0,1]
for i in nloops:
#调用ThreadFunc类实例化的对象,创建所有线程
t = threading.Thread(
target=ThreadFunc(loop, (i,loops[i]), loop.__name__)
)
threads.append(t)
#开始线程
for i in nloops:
threads[i].start()
#等待所有结束线程
for i in nloops:
threads[i].join()
print 'All end:', ctime()
通过with语句使用线程锁
所有的线程锁都有一个加锁和释放锁的动作,非常类似文件的打开和关闭。在加锁后,如果线程执行过程中出现异常或者错误,没有正常的释放锁,那么其他的线程会造到致命性的影响。通过with上下文管理器,可以确保锁被正常释放。其格式如下:
with some_lock:
# 执行任务...
这相当于:
some_lock.acquire()
try:
# 执行任务..
finally:
some_lock.release()
2.2 线程模块
Python通过两个标准库thread和threading提供对线程的支持。
2.2.1 thread模块
thread提供了低级别的、原始的线程以及一个简单的锁。
2.2.2 threading模块
由于任何进程默认就会启动一个线程,我们把该线程称为主线程,主线程又可以启动新的线程,Python的threading
模块有个current_thread()
函数,它永远返回当前线程的实例。主线程实例的名字叫MainThread
,子线程的名字在创建时指定。名字仅仅在打印时用来显示,完全没有其他意义,如果不起名字Python就自动给线程命名为Thread-1
,Thread-2
……。
thread模块不支持守护线程。当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出。这就引入了守护线程的概念。Threading模块支持守护线程,它们工作流程如下:
守护线程一般是一个等待客户请求的服务器,如果没有客户提出请求,它就在那等着。如果你设定一个线程为守护线程,就表示你在说这个线程是不重要的,在进程退出时,不用等待这个线程退出,正如网络编程中服务器线程运行在一个无限循环中,一般不会退出的。
如果你的主线程要退出的时候,不用等待那些子线程完成,那就设定这些线程的daemon属性。即,线程开始(调用thread.start())之前,调用setDaemon()函数设定线程的daemon标准(thread.setDaemon(True))就表示这个线程“不重要”。如果你想要等待子线程完成再退出,那就什么都不用做,或者显示地调用thread.setDaemon(False)以保证其daemon标志位False。你可以调用thread.isDaemon()函数来判断其daemon标志的值。
新的子线程会继承其父线程的daemon标志,整个Python会在所有的非守护线程退出后才会结束,即进程中没有非守护线程存在的时候才结束。
thread模块基本被废弃了,现在多用threading模块来创建和管理子线程,有两种方式来创建线程:一种是继承Thread类,并重写它的run()方法;另一种是在实例化threading.Thread
对象的时候,将线程要执行的任务函数作为参数传入线程。
Thread类声明如下:
threading.Thread(self, group=None, target=None, name=None,
args=(), kwargs=None, *, daemon=None)
- 参数group是预留的,用于将来扩展;
- 参数target是一个可调用对象,在线程启动后执行;
- 参数name是线程的名字。默认值为“Thread-N“,N是一个数字。
- 参数args和kwargs分别表示调用target时的参数列表和关键字参数。
threading 模块提供的其他方法:
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount(): 返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
除了使用方法外,线程模块同样提供了Thread类来处理线程,Thread类提供了以下方法:
- run(): 表示线程活动的方法,线程被cpu调度后自动执行的方法。
- start():启动线程活动,等待CPU调度。
- join([time]): 阻塞进程直到线程执行完毕,进程(也就是所有线程)等待至其他线程终止。这阻塞调用线程直至线程的join() 方法被调用终止-正常退出或者抛出未处理的异常-或者是可选的超时发生。.阻塞主线程(挡住,无法执行join以后的语句),专注执行多线程。多线程多join的情况下,依次执行各线程的join方法,前头一个结束了才能执行后面一个。无参数,则等待到该线程结束,才开始执行下一个线程的join。
调用Thread.join将会使主调线程堵塞,直到被调用线程运行结束或超时。参数timeout是一个数值类型,表示超时时间,如果未提供该参数,那么主调线程将一直堵塞到被调线程结束
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
2.3 线程结束问题
(1)线程的结束一般依靠线程函数的自然结束,一般不是循环操作;
(2)可以线程中任务结束后,在线程函数中调用thread.exit(),他抛出SystemExit exception,达到退出线程的目的;
(3)利用一个全局变量flag来控制是否在线程函数中调用thread.exit()。
(4)实际上在真正的编程中大部分情况下可能是循环操作,比如网络编程中循环监听端口。一般用一个全局变量flag控制循环执行,函数外设置flag为false结束线程。
当一个线程结束计算,它就退出了。线程可以调用thread.exit()之类的退出函数,也可以使用Python退出进程的标准方法,如sys.exit()或抛出一个SystemExit异常等。不过,不可以直接杀掉Kill一个线程。
2.4 线程同步
多线程开发中最难的问题不是如何使用,而是如何写出正确高效的代码,要写出正确而高效的代码必须要理解两个很重要的概念:同步和通信。
所谓的通信指的是线程之间如何交换消息,而同步则用于控制不同线程之间操作发生的相对顺序。简单点说同步就是控制多个线程访问代码的顺序,通信就是线程之间如何传递消息。在python中实现同步的最简单的方案就是使用锁机制,实现通信最简单的方案就是Event。
2.4.1 threading模块join函数
示例:A 线程正在运行,当B线程进行Join操作后,A线程会被阻断,进入等待队列。B线程执行,当B线程执行完毕后,B线程的资源收回,A线程进去执行队列。A线程继续进行执行。
总结:也就是调用的线程先执行,其他线程等待,执行结束后再执行其他线程。
1、python 默认参数创建线程后,不管主线程是否执行完毕,都会等待子线程执行完毕才一起退出。
2、如果创建线程,并且设置了daemon为true,即thread.setDaemon(True), 则主线程执行完毕后自动退出,不会等待子线程的执行结果。而且随着主线程退出,子线程也消亡。设置子线程为守护线程。默认是False,非守护线程。
3、join方法的作用是阻塞,等待子线程结束,join方法有一个参数是timeout,即如果主线程等待timeout,子线程还没有结束,则主线程强制结束子线程。
4、如果线程daemon属性为False, 则join里的timeout参数无效。主线程会一直等待子线程结束。
5、如果线程daemon属性为True, 则join里的timeout参数是有效的, 主线程会等待timeout时间后,结束子线程。此处有一个坑,即如果同时有N个子线程join(timeout),那么实际上主线程会等待的超时时间最长为 N * timeout, 因为每个子线程的超时开始时刻是上一个子线程超时结束的时刻。
在主线程A中调用了B.setDaemon(),这个的意思是,把子线程B设置为主线程A的守护线程,这时候,要是主线程A执行结束了,就不管子线程B是否完成,一并和主线程A退出。这就是setDaemon方法的含义,这基本和join是相反的。此外,还有个要特别注意的:必须在start() 方法调用之前设置,如果不设置为守护线程,程序会被无限挂起。
顾名思义是守护主线程的子线程,所以主线程执行完毕,守护线程也没必要继续执行,直接退出即可。
示例:
def doThreadTest():
print 'start thread time:', time.strftime('%H:%M:%S')
time.sleep(10)
print 'stop thread time:', time.strftime('%H:%M:%S')
threads = []
for i in range(3):
thread1 = threading.Thread(target=doThreadTest)
thread1.setDaemon(True)
threads.append(thread1)
for t in threads:
t.start()
for t in threads:
t.join(1)
print 'stop main thread'
start thread time: 19:31:15
start thread time: 19:31:15
start thread time: 19:31:15
stop main thread
如果把thread1.setDaemon(True) 注释掉, 运行结果为:
start thread time: 19:32:30
start thread time: 19:32:30
start thread time: 19:32:30
stop main thread
stop thread time: 19:32:40
stop thread time: 19:32:40
stop thread time: 19:32:40
如果去掉join的参数:
start thread time: 19:34:21
start thread time: 19:34:21
start thread time: 19:34:21
stop thread time: 19:34:26
stop thread time: 19:34:26
stop thread time: 19:34:26
stop main thread
为了防止脏数据而使用join()的方法,其实是让多线程变成了单线程,属于因噎废食的做法,正确的做法是使用线程锁。Python在threading模块中定义了几种线程锁类。
2.4.2 线程锁
threading模块所提供的锁类型:
- Lock:互斥锁
- RLock:可重入锁,使单一进程再次获得已持有的锁(递归锁)
- Condition:条件锁,使得一个线程等待另一个线程满足特定条件,比如改变状态或某个值。
- Semaphore:信号锁。为线程间共享的有限资源提供一个”计数器”,如果没有可用资源则会被阻塞。
- Event:事件锁,任意数量的线程等待某个事件的发生,在该事件发生后所有线程被激活
- Timer:一种计时器
- Barrier:Python3.2新增的“阻碍”类,必须达到指定数量的线程后才可以继续执行。
如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。通常锁还与try/finally及with一起用,为了保证锁一定会被释放,如果发生exception,锁不释放就会出现问题。也叫原语锁、简单锁、互斥锁、互斥量、二值信号量。
多线程和多进程最大的不同在于,多进程中,同一个变量,各自有一份拷贝存在于每个进程中,互不影响,而多线程中,所有变量都由所有线程共享所在进程的资源和数据,所以,任何一个变量都可以被任何一个线程修改,因此,线程之间共享数据最大的危险在于多个线程同时改一个变量,把内容给改乱了。
Condition是在Lock/RLock的基础上再次包装而成,而Semaphore的原理和操作系统的PV操作一致。
使用Thread对象的Lock和Rlock可以实现简单的线程同步,这两个对象都有acquire方法(请求锁)和release(释放锁)方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到acquire和release方法之间。如下:
class myThread (threading.Thread):
def __init__(self, threadID, name, counter):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.counter = counter
def run(self):
print "Starting " + self.name
# 获得锁,成功获得锁定后返回True
# 可选的timeout参数不填时将一直阻塞直到获得锁定
# 否则超时后将返回False
threadLock.acquire()
print_time(self.name, self.counter, 3)
# 释放锁
threadLock.release()
def print_time(threadName, delay, counter):
while counter:
time.sleep(delay)
print "%s: %s" % (threadName, time.ctime(time.time()))
counter -= 1
threadLock = threading.Lock()
threads = []
# 创建新线程
thread1 = myThread(1, "Thread-1", 1)
thread2 = myThread(2, "Thread-2", 2)
# 开启新线程
thread1.start()
thread2.start()
# 添加线程到线程列表
threads.append(thread1)
threads.append(thread2)
# 等待所有线程完成
for t in threads:
t.join()
print "Exiting Main Thread"
多线程的优势在于可以同时运行多个任务(至少感觉起来是这样,实际不是,是并发,不是并行)。但是当线程需要共享数据时,可能存在数据不同步的问题。为了避免这种情况,引入了锁的概念。
锁有两种状态——锁定和未锁定。每当一个线程比如"set"要访问共享数据时,必须先获得锁定;如果已经有别的线程比如"print"获得锁定了,那么就让线程"set"暂停,也就是同步阻塞;等到线程"print"访问完毕,释放锁以后,再让线程"set"继续。
经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。类似于Java的synchronizedde用法。
Lock (互斥锁Mutex)。互斥锁是一种独占锁,同一时刻只有一个线程可以访问共享的数据。使用很简单,初始化锁对象,然后将锁当做参数传递给任务函数或者利用全局对象传递参数,在任务中加锁,使用后释放锁。
重入锁(Re-Entrant Lock)。 可重入锁对象。使单线程可以再次获得已经获得了的锁(递归锁定)。RLock允许在同一线程中被多次acquire。而Lock却不允许这种情况。否则会出现死循环,程序不知道解哪一把锁。注意:如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的锁。
RLock的使用方法和Lock一模一样,只不过它支持重入锁。该锁对象内部维护着一个Lock和一个counter对象。counter对象记录了acquire的次数,使得资源可以被多次require。最后,当所有RLock被release后,其他线程才能获取资源。在同一个线程中,RLock.acquire()
可以被多次调用,利用该特性,可以解决部分死锁问题。
也就是同一个线程可以获得多个锁,利用计数器来实现,然后逐步清除即可。锁没有释放,也可以继续请求锁,不会阻塞。但是RLock是线程级别的,在哪个线程acquire的,就需要在这个线程release,其它线程无法release。也就是说RLock无法跨线程。需要跨线程就得使用Lock。从而避免引起脏数据问题。
2.4.3 信号量
Threading 模块包含对其他功能的支持。例如,可以创建信号量(Semaphore),这是计算机科学中最古老的同步原语之一。基本上,一个信号量管理一个内置的计数器。当你调用 acquire 时计数器就会递减,相反当你调用 release 时就会递增。根据其设计,计数器的值无法小于零,所以如果正好在计数器为零时调用 acquire 方法,该方法将阻塞线程。通常使用信号量时都会初始化一个大于零的值,如 semaphore = threading.Semaphore(2)。
示例:
def run(n, se):
se.acquire()
print("run the thread: %s" % n)
time.sleep(1)
se.release()
# 设置允许5个线程同时运行
semaphore = threading.BoundedSemaphore(5)
for i in range(20):
t = threading.Thread(target=run, args=(i,semaphore))
t.start()
运行后,可以看到5个一批的线程被放行。
mutex 互斥锁 是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
2.4.4 Event事件
另一个非常有用的同步工具就是事件(Event)。它允许你使用信号(signal)实现线程通信。Python提供了Event对象用于线程间通信,它是由线程设置的信号标志,如果信号标志为真,则其他线程等待直到信号清除。
Event对象实现了简单的线程通信机制,它提供了设置信号,清除信号,等待等用于实现线程间的通信。
event = threading.Event() 创建一个event
事件线程锁的运行机制:全局定义了一个Flag,如果Flag的值为False,那么当程序执行wait()方法时就会阻塞,如果Flag值为True,线程不再阻塞。这种锁,类似交通红绿灯(默认是红灯),它属于在红灯的时候一次性阻挡所有线程,在绿灯的时候,一次性放行所有排队中的线程。
1 设置信号
event.set()
使用Event的se()t方法可以设置Event对象内部的信号标志为真。Event对象提供了isSet()方法来判断其内部信号标志的状态。
当使用event对象的set()方法后,isSet()方法返回真
2 清除信号
event.clear()
使用Event对象的clear()方法可以清除Event对象内部的信号标志,即将其设为假,当使用Event的clear方法后,isSet()方法返回假。
3 等待
event.wait()
Event对象wait的方法只有在内部信号为真的时候才会很快的执行并完成返回。当Event对象的内部信号标志为假时,
则wait方法一直等待到其为真时才返回。也就是说必须set新号标志位真。
示例:
def do(event):
print('start')
event.wait()
print('execute')
event_obj = threading.Event()
for i in range(10):
t = threading.Thread(target=do, args=(event_obj,))
t.start()
event_obj.clear()
inp = input('输入内容:')
if inp == 'true':
event_obj.set()
start
start
start
start
start
start
start
start
start
start
输入内容:'true'
execute
execute
execute
execute
execute
execute
execute
execute
execute
execute
2.4.5 条件Condition
Condition称作条件锁,依然是通过acquire()/release()加锁解锁。
wait([timeout])方法将使线程进入Condition的等待池等待通知,并释放锁。使用前线程必须已获得锁定,否则将抛出异常。
notify()方法将从等待池挑选一个线程并通知,收到通知的线程将自动调用acquire()尝试获得锁定(进入锁定池),其他线程仍然在等待池中。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。
notifyAll()方法将通知等待池中所有的线程,这些线程都将进入锁定池尝试获得锁定。调用这个方法不会释放锁定。使用前线程必须已获得锁定,否则将抛出异常。
2.4.6 Barrier
最后,在 Python 3.2 中加入了 Barrier 对象。Barrier 是管理线程池中的同步原语,在线程池中多条线程需要相互等待对方。如果要传递 barrier,每一条线程都要调用 wait() 方法,在其他线程调用该方法之前线程将会阻塞。全部调用之后将会同时释放所有线程。
2.4.7 定时器Timer
定时器Timer类是threading模块中的一个小工具,用于指定n秒后执行某操作。
from threading import Timer
def hello():
print("hello, world")
# 表示1秒后执行hello函数
t = Timer(1, hello)
t.start()
2.4.8 创建线程本地数据
我们知道进程之间数据互不干扰,而进程内的线程共享进程数据、资源和地址空间等。有些场景下,我们希望每个线程,都有自己独立的数据,他们使用同一个变量,但是在每个线程内的数据都是独立的互不干扰的,类似于进程中的数据。
我们可以使用threading.local() 相当于创建线程局部变量来实现:
import threading
L = threading.local()
L.num = 1
# 此时操作的是我们当前主线程的threading.local()对象,输出结果为1
print(L.num)
def f():
print(L.num)
# 创建一个子线程,去调用f(),看能否访问主线程中定义的L.num
t = threading.Thread(target=f)
t.start()
# 结果提示我们:
# AttributeError: '_thread._local' object has no attribute 'num'
对上面的稍作修改:
import threading
L = threading.local()
L.num = 1
# 此时操作的是我们当前主线程的threading.local()对象,输出结果为1
print(L.num)
def f():
L.num = 5 # 重新赋值,相当于新定义的变量
# 这里可以成功的输出5
print(L.num)
# 创建一个子线程,去调用f(),看能否访问主线程中定义的L.num
t = threading.Thread(target=f)
t.start()
# 主线程中的L.num依然是1,没有发生任何改变
print(L.num)
程序运行结果为:
1
5
1
由此可见,threading.local()创建的对象中的属性,是对于每个线程独立存在的,它们相互之间无法干扰,我们称它为线程本地数据。
全局变量L
就是一个ThreadLocal
对象,每个Thread
对它都可以读写student
属性,但互不影响。你可以把L
看成全局变量,但每个属性如L.num
都是线程的局部变量,可以任意读写而互不干扰,也不用管理锁的问题,ThreadLocal
内部会处理。
可以理解为全局变量L
是一个dict
,不但可以用L.num
,还可以绑定其他变量,如L.num1
等等。
ThreadLocal
最常用的地方就是为每个线程绑定一个数据库连接,HTTP请求,用户身份信息等,这样一个线程的所有调用到的处理函数都可以非常方便地访问这些资源。
2.5 线程通信
2.5.1 线程优先级队列( Queue)
Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO(先入先出)队列Queue,LIFO(后入先出)队列LifoQueue,和优先级队列PriorityQueue。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。用于信息和资源共享。
凡是符合该种结构的多线程通信过程我们称之为生产者-消费者模型(应届生面试常问)。
Queue模块中的常用方法:
- Queue.qsize() 返回队列的大小
- Queue.empty() 如果队列为空,返回True,反之False
- Queue.full() 如果队列满了,返回True,反之False
- Queue.full 与 maxsize 大小对应
- Queue.get([block[, timeout]])获取队列,timeout等待时间
- Queue.get_nowait() 相当Queue.get(False)
- Queue.put(item) 写入队列,timeout等待时间
- Queue.put_nowait(item) 相当Queue.put(item, False)
- Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号
- Queue.join() 实际上意味着等到队列为空,再执行别的操作
exitFlag = 0
class myThread (threading.Thread):
def __init__(self, threadID, name, q):
threading.Thread.__init__(self)
self.threadID = threadID
self.name = name
self.q = q
def run(self):
print "Starting " + self.name
process_data(self.name, self.q)
print "Exiting " + self.name
def process_data(threadName, q):
while not exitFlag:
queueLock.acquire()
if not workQueue.empty():
data = q.get()
queueLock.release()
print "%s processing %s" % (threadName, data)
else:
queueLock.release()
time.sleep(1)
threadList = ["Thread-1", "Thread-2", "Thread-3"]
nameList = ["One", "Two", "Three", "Four", "Five"]
queueLock = threading.Lock()
workQueue = Queue.Queue(10)
threads = []
threadID = 1
# 创建新线程
for tName in threadList:
thread = myThread(threadID, tName, workQueue)
thread.start()
threads.append(thread)
threadID += 1
# 填充队列
queueLock.acquire()
for word in nameList:
workQueue.put(word)
queueLock.release()
# 等待队列清空
while not workQueue.empty():
pass
# 通知线程是时候退出
exitFlag = 1
# 等待所有线程完成
for t in threads:
t.join()
print "Exiting Main Thread"
Starting Thread-1
Starting Thread-2
Starting Thread-3
Thread-1 processing One
Thread-2 processing Two
Thread-3 processing Three
Thread-1 processing Four
Thread-2 processing Five
Exiting Thread-3
Exiting Thread-1
Exiting Thread-2
Exiting Main Thread
其实Event等更像是线程通信。
三、总结
(1)thread模块创建多线程有一种方法。threading模块创建多线程有两种方法:继承Thread类和直接利用Thread类。
(2)锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。
(3)默认子线程非守护线程,也就是各个子线程和主线程同时执行,各自执行互不干扰。最后等待子线程执行完成,主线程才退出(此时主线程有可能任务执行完,但是没有退出)。而join函数的不同点是,在运行join函数的地方主线程阻塞,后面的任务不执行,等待子线程执行完成之后,主线程继续执行然后退出(如果所有子线程都执行完毕,否则在主线程执行完任务之后还需要等待其他子线程执行完毕后才能退出)。
(4)锁可以去with等语句共用,比如 with lock,即可实现锁的请求与释放。
(5)当我们需要编写并发爬虫等IO密集型的程序时,应该选用多线程或者协程(亲测差距不是特别明显);当我们需要科学计算,设计CPU密集型程序,应该选用多进程。当然以上结论的前提是,不做分布式,只在一台服务器上测试。