Python低层多线程接口_thread模块的用法和特性
作者:V1ncent Chen
一、进程和线程的区别
并行(多任务处理)是现代操作系统的基本特性,一个程序可以同时处理很多任务而不被阻塞。多任务处理的基本方式有两种:进程分支和线程派生。
进程分支就是从当前进程复制一个程序副本,程序中的内存副本,文件描述符等都会被复制,子进程改变一个全局对象只是修改本地的副本,不会影响到父进程。常用在启动独立的程序。
线程派生和进程分支类似,但是子线程依然在原进程中运行,所有的子线程都会共享进程中的全局对象,相对于进程分支,少了一个"复制"的操作,因此更加轻量化,且由于共享进程对象,相当于自带了线程间的通信机制(进程间的通信则需要借助管道,套接字,信号等外部工具)。常用在处理一些轻量级任务。
但这些共享对象的访问可能出现冲突,_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()可以实现等待子进程退出。
到此这篇关于Python低层多线程接口_thread模块的用法和特性的文章就介绍到这了,更多相关Python低层多线程接口内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!