AI技术百科
8.3、Python threading模块用法精讲
相对于 thread 包,threading 包提供了更多的功能。该包的用法基本分成两步:
第一步是构造一个 threading.Thread 实例对象,这时该对象对应的线程就处于“新建”状态;
第二步是操作该对象,如调用 start() 来将该线程转换到“就绪”状态。
创建线程实例对象
我们可以创建基于现有的 threading.Thread 类的实例对象,主要需要提供入口函数和对应的参数。入口函数仍复用前面的函数,代码如下:
格式化复制
def thread_entry(id): cnt = 0 while cnt < 10: print('Thread:(%d) Time:%s' % (id, time.ctime())) time.sleep(1) cnt = cnt + 1
下面来创建该实例,代码如下:
>>> import threading >>> thread1 = threading.Thread(target=thread_entry, name="thread 1", args=(1,)) >>> thread1.isAlive() # 线程是否在运行 False >>> thread1.name # 线程名称 'thread 1'
在创建线程实例时可以设定入口函数、入口函数的参数、线程名等。
而在 Python 3 中多了一个标志,即 daemon,表示该线程是否为后台线程。
在 Python 3 中,默认创建的线程不是 daemon 进程。在所有非 daemon 线程结束时,进程结束。进程结束时,所有 daemon
线程会强制退出。这说明 daemon 线程会自动随进程一起结束,而非 daemon 线程则会阻止进程的结束,或者说进程会等待所有非 daemon
线程的结束。
下面来演示 daemon 属性的用法。创建一个 daemon 线程,在其完成任务之前,主线程退出。这时可以看到该 daemon 线程会自动退出。代码如下:
import sys, time import threading # 引入线程库 def thread_entry(): # 线程入口函数 left_round = 10 # 一共循环10轮 print(' Child Thread: Start Running') while left_round > 0: print('Child Thread: Running, left round = %d' % left_round) time.sleep(0.5) left_round = left_round – 1 print("Child Thread Quit") # 线程退出 def start_threads(): # 启动线程 thread1 = threading.Thread(target=thread_entry, daemon=True) thread1.start() # 启动线程,使之处于“就绪”状态 time.sleep(0.8) print("Active Thread Number = %d" % threading.active_count()) time.sleep(1.8) print("Main Thread Quit") # 主线程退出 if __name__=='__main__': start_threads()
运行情况如下:
$ python3 demo2.py
Thread: Start Running
Child Thread: Running, left round = 10
Child Thread: Running, left round = 9
Active Thread Number = 2 # 现在有2个线程了
Child Thread: Running, left round = 8
Child Thread: Running, left round = 7
Child Thread: Running, left round = 6
Child Thread: Running, left round = 5
Main Thread Quit # 主线程结束,导致进程结束
可以看到主线程退出后进程就退出了。如果将 daemon 参数设为 False,则该新创建的线程就不是 daemon 线程了,这时即使主线程退出,进程也不会退出,直到所有的非 daemon 线程都退出。
将上例中的 start_threads() 函数修改如下,其他代码没有任何改变:
def start_threads(): # 创建非daemon线程 thread1 = threading.Thread(target=thread_entry, daemon=False) thread1.start() time.sleep(0.8) print("Active Thread Number = %d" % threading.active_count()) time.sleep(1.8) print("Main Thread Quit")
再次运行该程序,结果如下:
$ python3 demo3.py
Child Thread: Start Running
Child Thread: Running, left round = 10
Child Thread: Running, left round = 9
Active Thread Number = 2
Child Thread: Running, left round = 8
Child Thread: Running, left round = 7
Child Thread: Running, left round = 6
Child Thread: Running, left round = 5
Main Thread Quit # 主线程退出,但是进程没有退出
Child Thread: Running, left round = 4
Child Thread: Running, left round = 3
Child Thread: Running, left round = 2
Child Thread: Running, left round = 1
Child Thread Quit # 所有的非daemon线程结束,进程结束
在 Python 2 ,创建线程时是不能指定 daemon 属性的,但是可以在创建后修改属性,方式如下:
Thread实例对象.set setDaemon(False)
在 Python 2 中,新创建的非主线程默认都是 daemon 线程,即会随着进程退出而强制退出。下面是 Python 2
中的一个例子,在该例子中,创建线程后将其设置为非 daemon 线程,这样进程便会一直等待该线程退出才退出,不会强制该线程退出。主要修改在
start_threads() 部分,修改后的内容如下:
def start_threads(): thread1 = threading.Thread(target=thread_entry) thread1.setDaemon(False) # 修改为非daemon线程 thread1.start() time.sleep(0.8) print("Active Thread Number = %d" % threading.active_count()) time.sleep(1.8) print("Main Thread Quit")
运行情况如下:
$ python demo4.py
Child Thread: Start Running
Child Thread: Running, left round = 10
Child Thread: Running, left round = 9
Active Thread Number = 2
Child Thread: Running, left round = 8
Child Thread: Running, left round = 7
Child Thread: Running, left round = 6
Child Thread: Running, left round = 5
Main Thread Quit # 主线程退出,但是进程还没有退出
Child Thread: Running, left round = 4 # 新建的线程仍然在执行
Child Thread: Running, left round = 3
Child Thread: Running, left round = 2
Child Thread: Running, left round = 1
Child Thread Quit # 新建的线程退出,进程退出
派生自己的线程类
上面是通过创建 threading.Thread 的实例对象来创建线程的。其实也可以创建自己的线程类,然后使用自己的线程类来创建实例对象以达到创建线程的目的。当然自己创建的线程类要派生自 threading.Thread。
import sys, time import threading # 引入线程库 class CustomThread(threading.Thread): # 派生自己的线程类 def __init__(self, thread_name): threading.Thread.__init__(self) self.thread_name = thread_name def run(self): # 线程的主函数 left_round = 10 print('Child Thread: Start Running') while left_round > 0: print('Child Thread: Running, left round = %d' % left_round) time.sleep(0.5) left_round = left_round - 1 print("Child Thread Quit") def start_threads(): # 启动线程 thread1 = CustomThread('thread 1') # 创建线程对象 thread1.setDaemon(False) # 设置为非daemon线程 thread1.start() # 启动线程 time.sleep(0.8) print("Active Thread Number = %d" % threading.active_count()) time.sleep(1.8) print("Main Thread Quit") # 主线程退出 if __name__=='__main__': start_threads()
运行结果如下:
$ python demo6.py
Child Thread: Start Running
Child Thread: Running, left round = 10
Child Thread: Running, left round = 9
Active Thread Number = 2 # 2个线程
Child Thread: Running, left round = 8
Child Thread: Running, left round = 7
Child Thread: Running, left round = 6
Child Thread: Running, left round = 5
Main Thread Quit # 主线程退出,但是进程还没有退出
Child Thread: Running, left round = 4
Child Thread: Running, left round = 3
Child Thread: Running, left round = 2
Child Thread: Running, left round = 1
Child Thread Quit # 子线程退出,进程退出
这里需要注意的是:
在构造函数中一定要调用父类的构造函数,然后再进行其他的初始化操作。
run() 就是线程的入口函数,在外部调用实例对象的 start() 接口时就会运行该函数。该函数不能有除 self 之外的其他参数,但是可以通过构造函数传入或者通过设置来传输。
配置线程
在启动线程之前,线程处于“新建”状态,这时可以配置线程的信息。
1) 修改是否为 daemon 线程:这个仅在 Python 3 中才有效。我们可以操作线程对象的 daemon 属性,推荐的用法是使用线程对象的 setDaemon() 和 isDaemon() 接口函数。下面演示了这些用法。
>>> import threading # 引入线程库 >>> import time >>> def thread_entry(): # 定义入口函数 ... time.sleep(30) ... # 入口函数定义结束 >>> thread1 = threading.Thread(target=thread_entry) # 创建线程 >>> thread1.daemon # 查看该线程是否是daemon线程 False >>> thread1.daemon = True # 修改daemon属性 >>> thread1.daemon # 查看该线程是否是daemon线程 True >>> thread1.isDaemon() # 查看该线程是否是daemon线程 True >>> thread1.setDaemon(False) # 修改daemon属性 >>> thread1.isDaemon() # 查看该线程是否是daemon线程 False
2) 修改线程的名称:每个线程都有一个名字,即使我们没有指定,系统也会自动为其分配一个名字。可以使用线程对象的 name
属性来修改或者获得线程的名字。和daemon属性一样,推荐使用线程对象的 setName() 和 getName()
接口函数来操作线程的名字。下面演示了这些用法。
>>> import threading # 引入threading库 >>> import time # 定义线程入口函数 >>> def thread_entry(): ... time.sleep(30) ... # 入口函数定义结束 >>> thread1 = threading.Thread(target=thread_entry) # 创建线程 >>> thread1.name # 得到线程的名字 'Thread-1' # 这是系统分配的名字 >>> thread1.name = "new-name" # 修改线程的名字 >>> thread1.name # 得到线程的名字 'new-name' >>> thread1.getName() # 得到线程的名字 'new-name' >>> thread1.setName("abc") # 修改线程的名字 >>> thread1.getName() # 得到线程的名字 'abc'
启动线程
在配置好线程后,可以启动该线程。这时线程的状态由“新建”变为“就绪”。就绪状态的线程并没有在运行,只是说可以运行了。只有在操作系统可以为其分配相关的处理器资源时,才会将其调度到某个处理器上运行。
threading.Thread 实例对象有一个接口函数 start(),通过调用该函数可以启动该线程。
>>> def thread_entry(): # 定义入口函数 ... time.sleep(30) # 等待30秒 ... print("Child Thread Quit") ... # 入口函数结束 >>> thread1 = threading.Thread(target=thread_entry) # 创建线程 >>> thread1 <Thread(Thread-5, initial)> # 线程状态为initial >>> thread1.start() # 开始运行,进入“就绪”状态 >>> thread1 <Thread(Thread-5, started 123145558048768)>
start() 函数不能被调用多次,否则会抛出异常。下面的例子演示了多次调用 start() 的情形。
>>> thread1.start() # 再次启动线程 Traceback (most recent call last): # 抛出RuntimeError异常 File "<stdin>", line 1, in <module> File "/usr/local/Cellar/python@ 2/2.7.15_1/frameworks/Python.framework/Versions /2.7/lib/python2.7/threading.py", line 730, in start raise RuntimeError("threads can only be started once") RuntimeError: threads can only be started once
停止线程
在 thread 包和 threading 包中都没有提供强制线程退出的接口函数,但可以通过给线程发送信号的方式来强制某个线程退出。
import threading # 引入线程库 import inspect, sys, time import ctypes def async_raise_exception(thread_id, exctype): # 给线程发送信号 tid = ctypes.c_long(thread_id) # 得到线程的id if not inspect.isclass(exctype): exctype = type(exctype) res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(exctype)) if res == 0: raise ValueError("invalid thread id") elif res != 1: ctypes.pythonapi.PyThreadState_SetAsyncExc(thread_id, None) raise SystemError("PyThreadState_SetAsyncExc failed") def thread_entry(): # 线程入口函数 print('Child Thread: Start Running') while True: try : time.sleep(0.2) except SystemExit: # 接收到信号 print("Got an SystemExit Exception, Quit") break # 退出线程 print("Child Thread Quit") def start_threads(): # 启动线程 thread1 = threading.Thread(target=thread_entry) thread1.setDaemon(False) thread1.start() time.sleep(2) print("Active Thread Number = %d" % threading.active_count()) async_raise_exception(thread1.ident, SystemExit) time.sleep(1) print("Active Thread Number = %d" % threading.active_count()) print("Main Thread Quit") if __name__=='__main__': start_threads()
运行结果如下:
$ python forceQuit1.py
Child Thread: Start Running
Active Thread Number = 2
Got an SystemExit Exception, Quit
Child Thread Quit
Active Thread Number = 1
Main Thread Quit
等待线程结束
有时父线程需要等待所有的子线程完成任务,这时可以使用 join() 接口函数。该接口函数会一直等待,直到对象的线程退出。
import sys, time # 引入time库 import threading def thread_entry(): # 定义入口函数 left_round = 10 print('Child Thread: Start Running') while left_round > 0: print('Child Thread: Running, left round = %d' % left_round) time.sleep(0.2) left_round = left_round - 1 print("Child Thread Quit") return 88 def start_threads(): # 启动线程 thread1 = threading.Thread(target=thread_entry) thread1.setDaemon(False) thread1.start() thread1.join() # 等待子线程结束 print("Main Thread Quit") # 主线程退出 if __name__=='__main__': start_threads()
运行结果如下:
$ python joinDemo1.py
Child Thread: Start Running
Child Thread: Running, left round = 10
Child Thread: Running, left round = 9
Child Thread: Running, left round = 8
Child Thread: Running, left round = 7
Child Thread: Running, left round = 6
Child Thread: Running, left round = 5
Child Thread: Running, left round = 4
Child Thread: Running, left round = 3
Child Thread: Running, left round = 2
Child Thread: Running, left round = 1
Child Thread Quit
Main Thread Quit
join() 函数还可以带一个时间参数,表示最多等待多少秒,如果等待超时,该函数返回。但不论是因为超时返回还是因为线程退出,join()
函数的返回值都是 None。为了分辨返回的原因,需要在 join() 之后调用 isAlive() 来判断等待的线程是否真的退出了。不能
join() 自己,否则会抛出异常。不能 join() 处于“新建”状态的线程,否则也会抛出异常。
其他接口函数
下面介绍 threading 包中其他常用的接口函数。
1) 得到当前进程内运行状态线程的个数—— threading.active_count():这是一个类函数,不需要使用线程实例对象,也没有任何输入参数。
>>> def thread_entry(): # 入口函数 ... time.sleep(30) ... print("Child Thread Quit") ... # 入口函数结束 >>> thread1 = threading.Thread(target=thread_entry) # 创建线程 >>> threading.active_count() # 有效线程个数 1 >>> thread1.start() # 启动线程 >>> threading.active_count() # 有效线程个数 2
2) 当前线程——threading.current_thread():这也是一个类函数,不需要使用线程实例对象,也不需要任何输入参数,返回的是一个线程实例对象。在不同的线程内调用会得到不同的实例对象。
>>> thread_obj1 = threading.current_thread() # 得到当前的线程 >>> type(thread_obj1) <class 'threading._MainThread'> >>> isinstance(thread_obj1, threading.Thread) # 是否为线程对象 True >>> thread_obj1 # 查看线程对象 <_MainThread(MainThread, started 140735876817792)> >>> thread_obj1.name # 线程名 'MainThread' >>> thread_obj1.__class__ # 线程的类型 <class 'threading._MainThread'>
3)
设置线程栈大小——threading.stack_size(size):单位是字节。该函数对于所有后来新建的线程有效。目前阶段不能对某个线程设定栈的大小,只能设定系统参数。size
的值可以为 0 或者大于 32768(表示 32KB)的正整数,但最好不要设置为任意值,建议设置为 4KB 的整数倍,如
8192(8KB);如果设为 0 表示取系统默认值。
该函数仅在 Windows 和支持 POSIX 的系统上有效。
4) 得到线程栈的大小——threading.stack_size():该函数返回的是一个整数,0 表示系统默认值,否则表示栈的字节数。
>>> threading.stack_size() # 读取栈的大小 0 # 0表示默认值 >>> threading.stack_size(1024*1024*2) # 设定栈的大小为2M 0 >>> threading.stack_size() # 查看目前栈的大小 2097152
该函数仅在 Windows 和支持 POSIX 的系统上有效。
5) 得到线程 ID:该值可以从线程对象的 ident 属性得到。如果线程处于“新建”状态,那么该属性值为 0。该 ID 值是循环使用的,如果某个线程退出,那么其原来使用的 ID 可能会分配给新的线程使用。
>>> a = threading.current_thread() # 得到当前线程的对象 >>> a # 查看线程对象 <_MainThread(MainThread, started 140735876817792)> >>> a.ident # 查看ID 140735876817792
6) 得到主线程——main_thread():该函数仅在 Python 3.4 以及后面的版本中可用,在 Python 2 和早期的 Python 3 中是不存在的,其返回的是主线程实例对象。
>>> import threading >>> threading.main_thread() <_MainThread(MainThread, started 140735876817792)>
7) 超时时间TIMEOUT_MAX:在调用 Lock.acquire()、RLock.acquire() 时等待的默认时间,单位为秒。如果超过这个时间,调用返回错误码,不再继续等待。这个属性可读可写。
>>> threading.TIMEOUT_MAX # 得到超时时间,单位为秒 9223372036.0 >>> threading.TIMEOUT_MAX = 9223372038 >>> threading.TIMEOUT_MAX 9223372038
8) 得到线程列表——enumerate():该函数返回一个列表,成员是 threading.Thread 的实例对象。
>>> import threading # 引入线程库 >>> threading.enumerate() # 得到遍历线程的对象 [<_MainThread(MainThread, started 140735876817792)>]
10
条内容
通常情况下,一个进程包括至少一个线程,如果有多个线程,其中包括一个主线程。同一个进程内的所有线程共享系统资源,但它们有各自独立的栈、寄存器环境和本地存储。
多线程的好处是可以同时执行多个任务,如果系统有多个计算单元,那么多个线程可以在各自的计算单元并行运行,这样可以极大提升系统的处理效率。
多数情况下进程比线程大,通常一个进程可以包含多个线程。进程的隔离效果比线程好,所以使用多进程会比使用多线程更加安全。多进程相对多线程的缺点是其调度比较重,效率比较低。