_thread是python标准库中的一个低层多线程API,可以在进程中启动线程来处理任务,并且提供了简单的锁机制来控制共享资源的同步访问。本文就_thread模块的用法和特性做个简单的演示。
文章目录
- 一、进程和线程的区别
- 二、_thread模块的用法
- 2.1 派生线程
- 2.2 同步化访问控制
- 2.3 子线程的退出控制
- 2.3.1 通过sleep等待子线程运行结束
- 2.3.2 通过锁的状态监测子进程结束
- 2.3.3 通过共享变量监测子进程结束
一、进程和线程的区别
并行(多任务处理)是现代操作系统的基本特性,一个程序可以同时处理很多任务而不被阻塞。多任务处理的基本方式有两种:进程分支和线程派生。
进程分支就是从当前进程复制一个程序副本,程序中的内存副本,文件描述符等都会被复制,子进程改变一个全局对象只是修改本地的副本,不会影响到父进程。常用在启动独立的程序。
线程派生和进程分支类似,但是子线程依然在原进程中运行,所有的子线程都会共享进程中的全局对象,相对于进程分支,少了一个"复制"的操作,因此更加轻量化,且由于共享进程对象,相当于自带了线程间的通信机制(进程间的通信则需要借助管道,套接字,信号等外部工具)。常用在处理一些轻量级任务。
但这些共享对象的访问可能出现冲突,_thread也提供了锁机制来同步化对象访问,例如修改一个对象时,先获取锁,完成后再释放,这样就可以保证任意时间点,最多能有1个线程能修改这个共享对象,防止出现混乱。
二、_thread模块的用法
_thread模块中的start_new_thread可以开启一个新的线程并运行指定程序。
2.1 派生线程
当程序启动时,其实它就已经启动了一个线程,这个就是主线程。通过_thread.start_new_thread方法,传入一个函数对象和一个参数元组,就可以开启新线程来执行所传入的函数对象。简单的演示如下:
import _thread as thread
def func(id):
print('我是 {} 号子线程'.format(id))
for i in range(5):
thread.start_new_thread(func,(i,))
上面代码执行示意图如下:for循环执行了5次start_new_thread,这会开启5个线程,每个线程去执行func函数。因为线程是并行的,所以输出的顺序并不是0,1,2,3,4而是混乱的。注意3,4号子线程打印出现了重叠,这是因为它们共享一个标准输出流,而我们没有做同步化访问控制,因而它们同时打印输出。
2.2 同步化访问控制
由于子线程可以共享进程中的资源,这既是一个优势(方便线程间通信),也带来了共享资源访问的问题,如果多个子线程同时修改一个共享资源,那么就容易出现冲突,例如上面的输出重叠。
为了解决这类问题,_thread模块中的allocate_lock方法提供了一个简单的锁机制来控制共享访问,每次获取共享资源时先获取锁,可以保证任何时间点最多只有1个线程可以访问该资源。示例如下:
import _thread as thread
lock = thread.allocate_lock() # 定义一个锁
def func(id):
with lock: # 用上下文管理器自动控制锁的获取和释放
print('我是 {} 号子线程'.format(id))
for i in range(5):
thread.start_new_thread(func,(i,))
with lock会自动管理锁对象的获取和释放,默认线程会一直等待直到锁的获取,如果想要更精细的控制可以使用下面的方式:
lock.acquire() # 获取锁
print('我是 {} 号子线程'.format(id))
lock.release() # 释放锁
lock.acquire()有2个可选的参数:blocking=True代表无法获取锁时等待,blocking=False代表如无法立刻获取锁则返回。timeout=-1代表等待的秒数(-1代表无限等待),此参数只有在blocking设置为True时才能指定。
2.3 子线程的退出控制
_thread派生子线程后,如果主线程运行完毕,而子线程依然在运行中,那么所有子线程就会随着主线程的退出而被终止。
我们在子进程中增加一个sleep来模拟长时间任务,让其运行时长超过主线程。将下面的代码保存到一个thread1.py中,以一个独立进程启动:
import _thread as thread, time
lock = thread.allocate_lock() # 定义一个锁
def func(id):
time.sleep(id) # 子线程会睡眠,运行时长将超过主线程
with lock:
print('我是 {} 号子线程'.format(id))
for i in range(5):
thread.start_new_thread(func,(i,))
print('主线程结束,退出...') # 主线程退出时打印提示
可以看到,主线程打印了退出提示,但子线程却没有任何输出,这是因为主线程运行的时间非常短,当其退出时,所有子线程都终止了。这种情况显然不是我们想看到的。示例图如下:
2.3.1 通过sleep等待子线程运行结束
为了解决上面的问题,一个简单的解决方案可以在主线程中加一个sleep,让其等待一段时间再退出,但这个时间我们只能预估。在上面的代码基础上,增加一个time.sleep(3),让主线程退出前等待3秒,保存为thread2.py,再次执行:
import _thread as thread, time
lock = thread.allocate_lock() # 定义一个锁
def func(id):
time.sleep(id) # 子线程会睡眠,运行时长将超过主线程
with lock:
print('我是 {} 号子线程'.format(id))
for i in range(5):
thread.start_new_thread(func,(i,))
time.sleep(3) # 主线程等待3秒再退出
print('主线程结束,退出...') # 主线程退出时打印提示
可以看到部分子进程运行完毕,但还有部分子进程未完成,因此这种方法不是很准确,虽然你可以给一个足够长的时间来保证所有子进程运行结束,但如果进程长时间不结束,也会占用系统资源。
2.3.2 通过锁的状态监测子进程结束
_thread.allocate_lock除了可以控制共享对象的访问,还可以用来传递全局状态,下面定义了包含5把锁的列表,每个子线程执行完成后会去获取其中对应位置上的锁,在主线程中通过lock.locked()来检查是否所有的锁都被获取,当所有锁都被获取时(代表所有子线程都结束),主线程退出。将下面代码保存到thread3.py中,再次运行:
import _thread as thread, time
lock = thread.allocate_lock() # 定义一个锁
exit_locks = [thread.allocate_lock() for I in range(5)] # 定义一个列表,包含5把锁,对应稍后启动的5个子线程
def func(id):
time.sleep(id) # 子线程会睡眠,运行时长将超过主线程
with lock:
print('我是 {} 号子线程'.format(id))
exit_locks[id].acquire() # 执行完成后获取exit_locks中对应位置的锁
for i in range(5):
thread.start_new_thread(func,(i,))
for lock in exit_locks:
while not lock.locked(): pass # lock.locked()检测锁是否已被获取
print('主线程结束,退出...') # 主线程退出时打印提示
测试时可以发现,主线程会在所有子线程执行完毕后立刻退出,即不会提前导致子线程终止,也不会推迟浪费系统资源。
2.3.3 通过共享变量监测子进程结束
由于子线程可以共享进程中的变量,因此子线程中对共享对象的修改在主线程也可以看到,我们可以将上面的锁替换为简单的变量,可以达到相同的效果,下面使用一个共享列表,通过在子线程中修改变量值传递状态,将下面代码保存为thread4.py并执行:
import _thread as thread, time
lock = thread.allocate_lock() # 定义一个锁
exit_flags = [False]*5 # 定义一个全局共享列表,包含5个布尔变量False
def func(id):
time.sleep(id) # 子线程会睡眠,运行时长将超过主线程
with lock:
print('我是 {} 号子线程'.format(id))
exit_flags[id] = True # 执行完成后将共享列表中对应位置的值改为True
for i in range(5):
thread.start_new_thread(func,(i,))
while False in exit_flags:pass # 检测列表中是否有False,如果全部为Ture,代表所有子线程执行完毕
print('主线程结束,退出...') # 主线程退出时打印提示
可以看到主线程会等待子线程执行完毕后退出,这种方式相比上面可以节约锁分配的资源,看上去也更加简单。
以上即是_thread模块的基本用法。基于_thread模块还有高级的threading模块,_threading模块是基于类和对象的高级接口,并提供了额外的控制工具,例如threading.join()可以实现等待子进程退出。