python核心编程(高级部分)
多线程编程
介绍用多线程编程技术实现代码并行性的几种不同的方法
18.1 引言/动机
并行运行相互独立的子任务可以大幅度地提升整个任务的效率。这就是多线程编程的目的。
多线程编程对于某些任务是最理想的。这些任务具有以下特点:异步的, 有多个并发事务,各个事务的运行顺序可以是不确定的,随机的,不可预测的。这样的编程任务可以被分成多个执行流,每个流都有一个要完成的目标。根据应用的不同,这些子任务可能都要计算出一个中间结果,用于合并得到最后的结果。
运算密集型的任务一般都比较容易分隔成多个子任务,可以顺序执行或以多线程的方式执行。单线程处理多个外部输入源的的任务较难,如果不用多线程的方式处理, 则一定要使用一个或多个计时器来实现。
一个顺序执行的程序要从每个 I/O(输入/输出)终端信道检查用户的输入时,程序无论如何也不能在读取 I/O 终端信道的时候阻塞。因为用户输入的到达是不确定的,阻塞会导致其它 I/O 信息的数据不能被处理。顺序执行的程序必须使用非阻塞 I/O,或是带有计时器的阻塞 I/O(这样才能保证阻塞只是暂时的)。
由于顺序执行的程序只有一个线程在运行。它要保证它要做的多任务,不会有某个任务占用太多的时间,且要合理地分配用户的响应时间。执行多任务的顺序执行的程序一般程序控制流程都很复杂。
使用多线程编程和一个共享的数据结构如 Queue,这种程序任务可以用几个功能单一的线程来组织:
- UserRequestThread: 负责读取客户的输入,可能是一个 I/O 信道。程序可能创建多个线程, 每个客户一个,请求会被放入队列中。
- RequestProcessor: 一个负责从队列中获取并处理请求的线程,它为下面那种线程提供输出。
- ReplyThread: 负责把给用户的输出取出来,如果是网络应用程序就把结果发送出去,否则就保存到本地文件系统或数据库中。
用多线程来组织可以降低程序的复杂度,实现良好组织地程序结构。每个线程的逻辑都不复杂,做的事情很清楚。
18.2 线程和进程
18.2.1 什么是进程?
计算机程序是可执行的二进制(或其它类型)的数据。它们只有在被读取到内存中,被OS调用时才开始它们的生命期。进程(有时被称为重量级进程)是程序的一次执行。每个进程都有自己的地址空间,内存,数据栈以及其它记录其运行轨迹的辅助数据。操作系统管理在其上运行的所有进程,并为这些进程公平地分配时间。进程也可以通过 fork 和 spawn 操作来完成其它的任务。不过各个进程有自己的内存空间,数据栈等,所以只能使用进程间通讯(IPC), 而不能直接共享信息。
18.2.2 什么是线程?
线程(有时被称为轻量级进程),所有的线程运行在同一个进程中,共享相同的运行环境。它们可以想像成是在主进程或“主线程”中并行运行的“迷你进程”。
线程有开始,顺序执行和结束三部分。它有一个自己的指令指针,记录自己运行到什么地方。 线程的运行可能被抢占(中断),或暂时的被挂起(也叫睡眠),让其它的线程运行,这叫做让步。一个进程中的各个线程之间共享同一片数据空间,所以线程之间可以共享数据以及相互通讯。线程一般都是并发执行的,正是由于这种并行和数据共享的机制使得多个任务的合作变为可能。在单 CPU 的系统中,真正的并发是不可能的,每个线程会被安排成每次只运行一小会,然后就把 CPU 让出来,让其它的线程去运行。在进程的整个运行过程中,每个线程都只做自己的事,在需要的时候跟其它的线程共享运行的结果。
当然,这样的共享并不是完全没有危险的。如果多个线程共同访问同一片数据,则由于数据访问的顺序不一样,有可能导致数据结果的不一致的问题。这叫做竞态条件(race condition)。好在大多数线程库都带有一系列的同步原语,来控制线程的执行和数据的访问。
另一个要注意的,由于有的函数会在完成之前阻塞住,在没有特别为多线程做修改的情 况下,这种“贪婪”的函数会让 CPU 的时间分配有所倾斜。导致各个线程分配到的运行时间可能不尽相同,不尽公平。
18.3 Python、线程和全局解释器锁
18.3.1 全局解释器锁(GIL)
Python 代码的执行由 Python 虚拟机(也叫解释器主循环)来控制。Python 在设计之初就考虑到要在主循环中,同时只有一个线程在执行,就像单 CPU 的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在 CPU 中运行。同样地,虽然 Python 解释器中可以“运行” 多个线程,但在任意时刻,只有一个线程在解释器中运行。
对 Python 虚拟机的访问由全局解释器锁(GIL)来控制,正是这个锁能保证同一时刻只有一个线程在运行。在多线程环境中,Python 虚拟机按以下方式执行:
-
设置 GIL
-
切换到一个线程去运行
-
运行:
- 指定数量的字节码指令,或者
- 线程主动让出控制(可以调用 time.sleep(0))
-
把线程设置为睡眠状态
-
解锁 GIL
-
再次重复以上所有步骤
在调用外部代码(如 C/C++扩展函数)时,GIL 将会被锁定,直到这个函数结束为止(由于在这期间没有 Python 的字节码被运行,所以不会做线程切换)。编写扩展的程序员可以主动解锁 GIL。
不过,Python 的开发人员则不用担心在这些情况下你的 Python 代码会被锁住。
例如,对所有面向 I/O 的(会调用内建的操作系统 C 代码的)程序来说,GIL 会在这个 I/O 调用之前被释放,以允许其它的线程在这个线程等待 I/O 的时候运行。如果某线程并未使用很多 I/O 操作, 它会在自己的时间片内一直占用处理器(和 GIL)。也就是说,I/O 密集型的 Python 程序比计算密集型的程序更能充分利用多线程环境的好处。
对源代码,解释器主循环和 GIL 感兴趣的人,可以看看 Python/ceval.c 文件。
18.3.2 退出线程
当一个线程结束计算,它就退出了。线程可以调用 thread.exit()之类的退出函数,或使用Python 退出进程的标准方法,如 sys.exit()或抛出一个 SystemExit 异常等。但不可以直接 “杀掉”("kill")一个线程。
两个跟线程有关的模块中,不建议使用 thread 模块。很明显的一个原因是,当主线程退出时,所有其它线程没有被清除就退出了。但另一个模块 threading 就能确保所有“重要的”子线程都退出后,进程才会结束。
18.3.3 在 Python 中使用线程
在交互式解释器里导入 thread 模块就行了,没出现错误就表示线程可用。
>>> import thread
18.3.4 没有线程支持的情况
例子一:使用 time.sleep()函数来演示线程是怎样工作的
创建两个“计时循环”。一个睡眠 4 秒钟,一个睡眠 2 秒。onethr.py
from time import sleep, ctime
def loop0():
print('start loop 0 at:', ctime())
sleep(4)
print('loop 0 done at:', ctime())
def loop1():
print('start loop 1 at:', ctime())
sleep(2)
print('loop 1 done at:', ctime())
def main():
print('starting at:', ctime())
loop0()
loop1()
print('all DONE at:', ctime())
if __name__ == '__main__':
main()
总共运行时长6s-7s,若能让这些计算并行执行,就可以减少总的运行时间。这就是要介绍的多线程编程的前提条件。
18.3.5 Python 的 threading 模块
几个用于多线程编程的模块,包括 thread, threading 和 Queue 等。thread 和 threading 模块允许程序员创建和管理线程。
- thread 模块提供了基本的线程和锁的支持。
- threading 提供了更高级别,功能更强的线程管理的功能。
- Queue 模块允许用户创建一个可以用于多个线程之间共享数据的队列数据结构。
不建议使用 thread 模块。
- 首先,更高级别的 threading 模块更为先进,对线程的支持更为完善,而且使用 thread 模块里的属性有可能会与 threading 出现冲突。
- 其次, 低级别的 thread 模块的同步原语很少(实际上只有一个),而 threading 模块则有很多。
- 另一个,对于进程什么时候应该结束完全没有控制,当主线程结束时,所有的线程都会被强制结束掉,没有警告也不会有正常的清除工作。而threading 模块能确保重要的子线程退出后进程才退出。
我们将给出使用 thread 模块的例子,让你对为什么应该避免使用 thread 模块有更深的认识,以及让你了解在把代码改为使用 threading 和 Queue 模块时,我们能获得多大的便利。
只建议那些有经验的专家在想访问线程的底层结构的时候,才使用 thread 模块。
18.4 thread 模块
除了产生线程外,thread 模块也提供了基本的同步数据结构锁对象(lock object,也叫原语锁,简单锁,互斥锁,互斥量,二值信号量)。如之前所说, 同步原语与线程的管理是密不可分的。
表 18.1 中所列的是常用的线程函数以及 LockType 类型的锁对象的方法。
thread 模块函数
- start_new_thread(function, args, kwargs=None) 产生一个新的线程,在新线程中用指定的参数和可选的 kwargs 来调用这个函数。
- allocate_lock() 分配一个 LockType 类型的锁对象
- exit() 让线程退出
LockType 类型锁对象方法
- acquire(wait=None) 尝试获取锁对象
- locked() 如果获取了锁对象返回 True,否则返回 False
- release() 释放锁
start_new_thread()函数:语法与内建的 apply()函数完全一样,其参数为:函数,函数的参数以及可选的关键字参数。不同的是,函数不是在主线程里运行,而是产生一个新的线程来运行这个函数。start_new_thread()要求一定要有前两个参数。就算要运行的函数不要参数,也要传一个空的元组。
import thread
from time import sleep, ctime
def loop0():
print('start loop 0 at:', ctime())
sleep(4)
print('loop 0 done at:', ctime())
def loop1():
print('start loop 1 at:', ctime())
sleep(2)
print('loop 1 done at:', ctime())
def main():
print('starting at:', ctime())
thread.start_new_thread(loop0, ())
thread.start_new_thread(loop1, ())
sleep(6)
print('all DONE at:', ctime())
if __name__ == '__main__':
main()
睡眠 4 秒和 2 秒的代码现在是并发执行的。总的运行时间被缩短了。loop1 甚至在 loop0 前面就结束了。程序多了一个“sleep(6)”。因为,如果我们没有让主线程停下来,那主线程就会运行下一条语句,显示 “all done”,然后就关闭运行着 loop0()和 loop1()的两个线程,退出了。
我们没有写让主线程停下来等所有子线程结束之后再继续运行的代码。这就是之前说线程需要同步的原因。这里使用了 sleep()函数做为我们的同步机制。
若循环的执行时间不能事先确定的话,怎么办呢? 可能造成主线程过早或过晚退出。这就是锁的用武之地。
再一次修改程序mtsleep2.py, 引入锁的概念。使用了锁,就可以在两个线程都退出后,马上退出。
import thread
from time import sleep, ctime
loops = [4, 2]
def loop(nloop, nsec, lock):
print('start loop', nloop, 'at:', ctime())
sleep(nsec)
print('loop', nloop, 'done at:', ctime())
lock.release()
def main():
print('starting at:', ctime())
locks = []
nloops = range(len(loops))
for i in nloops:
lock = thread.allocate_lock()
lock.acquire()
locks.append(lock)
for i in nloops:
thread.start_new_thread(loop, (i, loops[i], locks[i]))
for i in nloops:
while locks[i].locked():
pass
print('all DONE at:', ctime())
if __name__ == '__main__':
main()
loop()函数里,增加了一些锁的操作。 每个线程都会被分配一个事先已经获得的锁,在 sleep()的时间到了之后就释放相应的锁以通知主线程,这个线程已经结束了。
主要的工作在包含三个循环的 main()函数中完成。
- 先调用 thread.allocate_lock()函数创建一个锁的列表,并分别调用各个锁的 acquire()函数获得锁。获得锁表示“把锁锁上”。然后把锁放到锁列表中。
- 下一个循环创建线程,每个线程都用各自的循环号,睡眠时间和锁为参数去调用 loop()函数。为什么不在创建锁的循环里创建线程呢?有以下几个原因:(1) 想实现线程的同步,所以要让“所有的马同时冲出栅栏”。(2) 获取锁要花一些时间,如果线程退出得“太快”,可能会导致还没有获得锁,线程就已经结束了的情况。在线程结束时,线程要自己去做解锁操作。
- 最后一个循环只是坐在那一直等(达到暂停主线程的目的),直到两个锁都被解锁为止才继续运行。
18.5 threading 模块
threading模块提供了 Thread 类及各种非常好用的同步机制。表 18.2 列出了 threading 模块里所有的对象。这里将演示Thread的使用,不会提到锁原语。而 Thread 类也有某种同步机制,所以,没有必要详细介绍锁原语。
表 18.2 threading 模块对象
threading 模块对象 | 描述 |
---|---|
Thread | 表示一个线程的执行的对象 |
Lock | 锁原语对象(跟 thread 模块里的锁对象相同) |
RLock | 可重入锁对象。使单线程可以再次获得已经获得了的锁(递归锁定)。 |
Condition | 条件变量对象能让一个线程停下来,等待其它线程满足了某个“条件”。 如,状态的改变或值的改变。 |
Event | 通用的条件变量。多个线程可以等待某个事件的发生,在事件发生后, 所有的线程都会被激活。 |
Semaphore | 为等待锁的线程提供一个类似“等候室”的结构 |
BoundedSemaphore | 与 Semaphore 类似,只是它不允许超过初始值 |
Timer | 与 Thread 相似,只是,它要等待一段时间后才开始运行。 |
核心提示:守护线程
thread模块不支持守护线程。当主线程退出时,所有的子线程不论它们是否还在工作,都会被强行退出。
threading 模块支持守护线程:守护线程一般是一个等待客户请求的服务器, 如果没有客户提出请求,它就在那等着。如果设定一个线程为守护线程,就表示你在说这个线程是不重要的,在进程退出的时候,不用等待这个线程退出。就像你在第 16 章网络编程看到的,服务器线程运行在一个无限循环中,一般不会退出。
如果你的主线程要退出的时候,不用等待那些子线程完成,那就设定这些线程的 daemon 属性。 即,在线程开始(调用 thread.start())之前,调用 setDaemon()函数设定线程的 daemon 标志(thread.setDaemon(True))就表示这个线程“不重要”。
如果你想要等待子线程完成再退出,那就什么都不用做。你可以调用 thread.isDaemon()函数来判断其 daemon 标志的值。新的子线程会继承其父线程的 daemon 标志。整个 Python 会在所有的非守护线程退出后才会结束,即进程中没有非守护线程存在的时候才结束。
18.5.1 Thread 类
三种方法可以用threading.Thread类来创建线程:
- 创建一个Thread的实例,传给它一个函数
- 创建一个Thread的实例,传给它一个可调用的类对象
- 从Thread派生出一个子类,创建一个这个子类的实例
表 18.3 Thread 对象的函数
函数 | 描述 |
---|---|
start() | 开始线程的执行 |
run() | 定义线程的功能的函数(一般会被子类重写) |
join(timeout=None) | 程序挂起,直到线程结束;如果给了 timeout,则最多阻塞 timeout 秒 |
getName() | 返回线程的名字 |
setName(name) | 设置线程的名字 |
isAlive() | 布尔标志,表示这个线程是否还在运行中 |
isDaemon() | 返回线程的 daemon 标志 |
setDaemon(daemonic) | 把线程的 daemon 标志设为 daemonic(一定要在调用 start()函数前调用) |
创建一个 Thread 的实例,传给它一个函数
mtsleep3.py:初始化一个Thread 对象,把函数(及其参数)传进去。在线程开始执行时,这个函数会被执行。
在使用 thread 模块时使用的锁没有了。在实例化每个 Thread 对象时,把函数(target)和参数(args)传进去,得到返回的 Thread 实例。实例化一个Thread(调用 Thread())与调用 thread.start_new_thread()之间最大的区别就是,新的线程不会立即开始。在你创建线程对象,但不想马上开始运行线程的时候,这是一个很有用的同步特性。
import threading
from time import sleep, ctime
loops = [4,2]
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))
for i in nloops:
t = threading.Thread(target=loop, args=(i, loops[i]))
threads.append(t)
for i in nloops: # start threads
threads[i].start()
for i in nloops: # wait for all
threads[i].join() # threads to finish
print('all DONE at:', ctime())
if __name__ == '__main__':
main()
所有的线程都创建了之后,再一起调用 start()函数启动。不用再管理一堆锁(分配锁,获得锁,释放锁,检查锁的状态等),只要简单地对每个线程调用 join()函数就可以了。
join()会等到线程结束,或者在给了 timeout 参数的时候,等到超时为止。使用 join()看上去会比使用一个等待锁释放的无限循环清楚一些(这种锁也被称为"spinlock")
join()的另一个比较重要的方面是它可以完全不用调用。一旦线程启动后,就会一直运行,直到线程的函数结束,退出为止。
创建一个 Thread 的实例,传给它一个可调用的类对象
创建线程时传一个可调用的类的实例供线程启动。相对于一个或几个函数来说,由于类对象里可以使用类的强大的功能,可以保存更多的信息,这种方法更为灵活。
增加了 ThreadFunc 类和创建 Thread 对象时会实例化一个可调用类 ThreadFunc 的类对象。
我们想让这个类在调用什么函数方面尽量地通用,并不局限于那个 loop()函数。这个类保存了函数的参数,函数本身以及函数的名字字符串。构造函数__init__() 里做了这些值的赋值工作。
创建新线程时,Thread 对象调用ThreadFunc 对象,这时会用到一个特殊函数 call()。由于我们有一个参数的元组,要在代码中使用 apply()函数。若使用的是 Python1.6 或是更高版本,可以使用 11.6.3 节中的新的调用语法:self.res = self.func(*self.args)
import threading
from time import sleep, ctime
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)
self.res = 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))
for i in nloops: # create all threads
t = threading.Thread(target=ThreadFunc(loop, (i, loops[i]), loop.__name__))
threads.append(t)
for i in nloops: # start all threads
threads[i].start()
for i in nloops: # wait for completion
threads[i].join()
print('all DONE at:', ctime())
if __name__ == '__main__':
main()
从 Thread 派生出一个子类,创建一个这个子类的实例
介绍如何子类化 Thread 类,这与上一个例子中的创建一个可调用的类非常像。使用子类化创建线程使代码看上去更清晰明了。 mtsleep5.py
先指出最重要的两点改变:
- MyThread 子类的构造函数一定要先调用基类的构造函数。
- 之前的特殊函数call()在子类中,名字要改为 run()。
把 MyThread 类代码保存到 myThread 模块中,并导入这个类。除了简单地使用 apply()函数来运行这些函数之外,还把结果保存到实现的 self.res 属性中,并创建一个新的函数 getResult()来得到结果。
子类化 Thread 类,而不是创建它的实例。这样做可以更灵活地定制我们的线程对象, 而且在创建线程的时候也更简单。
import threading
from time import sleep, ctime
loops = (4, 2)
class MyThread(threading.Thread):
def __init__(self, func, args, name=''):
threading.Thread.__init__(self)
self.name = name
self.func = func
self.args = args
self.res = None
def getResult(self):
return self.res
def run(self):
# apply(self.func, self.args)
self.res = 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))
for i in nloops:
t = MyThread(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 DONE at:', ctime())
if __name__ == '__main__':
main()
在多线程中, 不马上显示结果。由于我们想让 MyThread 类尽可能地通用(能同时适应有输出和没输出的函数), 会等到要结束时才会调用 getResult()函数,并在最后显示每个函数的结果。
18.5.2 斐波那契,阶乘和累加和
例 18.8 中的 mtfacfib.py 脚本比较了递归求斐波那契,阶乘和累加和函数的运行。脚本先在单线程中运行这三个函数,然后在多线程中做同样的事。
由于这些函数运行得很快(斐波那契函数会慢一些),我们得在每个函数中加上一 个 sleep()函数,让函数慢下来,以便于我们能方便地看到多线程能在多大程度上加速程序的运行。 不过实际工作中,你一般不会想在程序中加上 sleep()函数的。下面是程序的输出:
例 18.8 斐波那契,阶乘和累加和 (mtfacfib.py) ,分别在单线程和多线程环境中,运行三个递归函数。
18.5.3 threading 模块中的其它函数
除了各种同步对象和线程对象外,threading 模块还提供了一些函数。
表 18.4 threading 模块的函数
activeCount() | 当前活动的线程对象的数量 |
---|---|
currentThread() | 返回当前线程对象 |
enumerate() | 返回当前活动线程的列表 |
settrace(func) | 为所有线程设置一个跟踪函数 |
setprofile(func) | 为所有线程设置一个 profile 函数 |
18.5.4 生产者-消费者问题和 Queue 模块
生产者生产货物,把货物放到一个队列之类的数据结构中,生产货物所要花费的时间无法预先确定。消费者消耗生产者生产的货物的时间也是不确定的。
表 18.5 常用的 Queue 模块的属性
函数 | 描述 |
---|---|
Queue 模块函数 | |
queue(size) | 创建一个大小为 size 的 Queue 对象 |
Queue 对象函数 | |
qsize() | 返回队列的大小(由于在返回的时候,队列可能会被其它线程修改,所以这个值是近似值) |
empty() | 如果队列为空返回 True,否则返回 False |
full() | 如果队列已满返回 True,否则返回 False |
put(item, block=0) | 把item放到队列中,如果给了block(不为0),函数会一直阻塞到队列中有空间为止 |
get(block=0) | 从队列中取一个对象,如果给了 block(不为 0),函数会一直阻塞到队列中有对象为止 |
Queue 模块可以用来进行线程间通讯,让各个线程之间共享数据。创建一个队列,让生产者(线程)把新生产的货物放进去供消费者(线程)使用。
例 18.9 生产者-消费者问题 (prodcons.py)
创建一个队列,让生产者(线程)把新生产的货物放进去供消费者(线程)使用。这个实现中使用了 Queue 对象和随机地生产(和消耗)货物的方式。生产者和消费者相互独立并且并发地运行。
import threading
from random import randint
from time import sleep
from Queue import Queue
class MyThread(threading.Thread):
def __init__(self, func, args, name=''):
threading.Thread.__init__(self)
self.name = name
self.func = func
self.args = args
self.res = None
def getResult(self):
return self.res
def run(self):
# apply(self.func, self.args)
self.res = self.func(*self.args)
def writeQ(queue):
print('producing object for Q...')
queue.put('xxx', 1)
print("size now", queue.qsize())
def readQ(queue):
val = queue.get(1)
print('consumed object from Q... size now', queue.qsize())
def writer(queue, loops):
for i in range(loops):
writeQ(queue)
sleep(randint(1, 3))
def reader(queue, loops):
for i in range(loops):
readQ(queue)
sleep(randint(2, 5))
funcs = [writer, reader]
nfuncs = range(len(funcs))
def main():
nloops = randint(2, 5)
q = Queue(32)
threads = []
for i in nfuncs:
t = MyThread(funcs[i], (q, nloops), funcs[i].__name__)
threads.append(t)
for i in nfuncs:
threads[i].start()
for i in nfuncs:
threads[i].join()
print('all DONE')
if __name__ == '__main__':
main()
writeQ()和 readQ()函数分别用来把对象放入队列和消耗队列中的一个对象。使用字符串'xxx'来表示队列中的对象。
writer()函数:就是一次往队列中放入一个对象,等待一会,然后再做同样的事。reader()函数做的事比较类似,只是它是用来消耗对象的。
writer 睡眠的时间一般会比 reader 睡眠的时间短。这可以减少 reader 尝试从空队列中取数据的机会。writer 的睡眠时间短,那 reader 在想要数据的时候总是能拿到数据。
18.6 相关模块
下表列出了一些多线程编程中可能用得到的模块:
表 18.6 多线程相关的标准库模块
模块 | 描述 |
---|---|
thread | 基本的,底级别的线程模块 |
threading | 高级别的线程和同步对象 |
Queue | 供多线程使用的同步先进先出(FIFO)队列 mutex 互斥对象 |
SocketServer | 具有线程控制的 TCP 和 UDP 管理器 |