一、简介
多线程编程对于具有如下特点的编程任务而言是非常理想的:本质上是异步的;需要多个并发活动;每个活动的处理顺序可能是不确定的,或者说是随机的、不可预测的。这种编程任务可以被组织或划分成多个执行流,其中每个执行流都有一个指定要完成的任务。根据应用的不同,这些子任务可能需要计算出中间结果,然后合并为最终的输出结果。
• UserRequestThread:负责读取客户端输入,该输入可能来自 I/O 通道。程序将创建多个线程,每个客户端一个,客户端的请求将会被放入队列中。
• RequestProcessor:该线程负责从队列中获取请求并进行处理,为第 3 个线程提供输出。
• ReplyThread:负责向用户输出,将结果传回给用户(如果是网络应用),或者把数据写到本地文件系统或数据库中。
二、线程和进程
1、进程
计算机程序只是存储在磁盘上的可执行二进制(或其他类型)文件。只有把它们加载到内存中并被操作系统调用,才拥有其生命期。进程(有时称为重量级进程)则是一个执行中的程序。每个进程都拥有自己的地址空间、内存、数据栈以及其他用于跟踪执行的辅助数据。操作系统管理其上所有进程的执行,并为这些进程合理地分配时间。进程也可以通过派生(fork 或 spawn)新的进程来执行其他任务,不过因为每个新进程也都拥有自己的内存和数据栈等,所以只能采用进程间通信(IPC)的方式共享信息。
2、线程
线程(有时候称为轻量级进程)与进程类似,不过它们是在同一个进程下执行的,并共享相同的上下文。可以将它们认为是在一个主进程或“主线程”中并行运行的一些“迷你进程”。
线程包括开始、执行顺序和结束三部分。它有一个指令指针,用于记录当前运行的上下文。当其他线程运行时,它可以被抢占(中断)和临时挂起(也称为睡眠) ——这种做法叫做让步(yielding)。
一个进程中的各个线程与主线程共享同一片数据空间,因此相比于独立的进程而言,线程间的信息共享和通信更加容易。线程一般是以并发方式执行的,正是由于这种并行和数据共享机制,使得多任务间的协作成为可能。当然,在单核 CPU 系统中,因为真正的并发是不可能的,所以线程的执行实际上是这样规划的:每个线程运行一小会儿,然后让步给其他线程(再次排队等待更多的 CPU 时间)。在整个进程的执行过程中,每个线程执行它自己特定的任务,在必要时和其他线程进行结果通信。
当然,这种共享并不是没有风险的。如果两个或多个线程访问同一片数据,由于数据访问顺序不同,可能导致结果不一致。这种情况通常称为竞态条件(race condition)。幸运的是,大多数线程库都有一些同步原语,以允许线程管理器控制执行和访问。
三、全局解释器锁
Python 代码的执行是由 Python 虚拟机(又名解释器主循环)进行控制的。尽管 Python 解释器中可以运行多个线程,但是在任意给定时刻只有一个线程会被解释器执行。对 Python 虚拟机的访问是由全局解释器锁(GIL)控制的。这个锁就是用来保证同时只能有一个线程运行的。在多线程环境中, Python 虚拟机将按照下面所述的方式执行。
1.设置 GIL。
2.切换进一个线程去运行。
3.执行下面操作之一。
a.指定数量的字节码指令。
b.线程主动让出控制权(可以调用 time.sleep(0)来完成)。
4.把线程设置回睡眠状态(切换出线程)。
5.解锁 GIL。
6.重复上述步骤。
四、退出线程
当一个线程完成函数的执行时,它就会退出。另外,还可以通过调用诸如 thread.exit()之类的退出函数,或者 sys.exit()之类的退出 Python 进程的标准方法,亦或者抛出 SystemExit异常,来使线程退出。不过,你不能直接“终止”一个线程。
主线程应该做一个好的管理者,负责了解每个单独的线程需要执行什么,每个派生的线程需要哪些数据或参数,这些线程执行完成后会提供什么结果。这样,主线程就可以收集每个线程的结果,然后汇总成一个有意义的最终结果。
五、不使用线程的情况
我们将使用 time.sleep()函数来演示线程是如何工作的。 time.sleep()函数需要一个浮点型的参数,然后以这个给定的秒数进行“睡眠”,也就是说,程序的执行会暂时停止指定的时间。
import time
def loop():
print('start loop() at:', time.ctime())
time.sleep(4)
print('loop() done at:', time.ctime())
def main():
print('start at:', time.ctime())
loop()
print('done at:', time.ctime())
if __name__ == '__main__':
main()
四、Python 的 thread 模块
除了派生线程外, thread 模块还提供了基本的同步数据结构,称为锁对象(lock object,也叫原语锁、 简单锁、 互斥锁、 互斥和二进制信号量)。thread 模块的核心函数是 start_new_thread()。它的参数包括函数(对象)、函数的参数以及可选的关键字参数。将专门派生新的线程来调用这个函数。
start_new_thread()必须包含开始的两个参数,于是即使要执行的函数不需要参数,也需要传递一个空元组。注意第一个参数仅为函数名,第二个参数是函数使用的变量名。
import time
import _thread
def loop():
print('start loop() at:', time.ctime())
time.sleep(4)
print('loop() done at:', time.ctime())
def main():
print('start at:', time.ctime())
_thread.start_new_thread(loop,())
time.sleep(4)
print('done at:', time.ctime())
if __name__ == '__main__':
main()
这个应用程序中剩下的一个主要区别是增加了一个 sleep(4)调用。为什么必须要这样做呢?这是因为如果我们没有阻止主线程继续执行,它将会继续执行下一条语句,显示“done”然后退出,而 loop这两个线程将直接终止。我们没有写让主线程等待子线程全部完成后再继续的代码,即我们所说的线程需要某种形式的同步。在这个例子中,调用 sleep()来作为同步机制。
通过引入锁,是比在主线程中额外延时 6 秒更好的线程管理方式。
import time
import _thread
loops = [4,2]
def loop(nloop,nsec,lock):
print('start loop:{0} at:{1}'.format(nloop,time.ctime()))
time.sleep(nsec)
print('loop:{0} done at:{1}'.format(nloop,time.ctime()))
lock.release()
def main():
print("starting at:{0}".format(time.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(loop, (i, loops[i], locks[i])) # 启动2个线程用来执行loop函数并传递参数
# 反复检查锁是否被锁住,如果被锁住就一直死循环,否者停止循环检查
for i in nloops:
while locks[i].locked(): pass # 最后会阻塞当前的线程,反复检查当前的锁是否被锁住,如果被锁住就暂停等待解锁,才能#让主线程停止
# 当所有的线程都执行完毕后就会执行最后的打印
print("all DONE at:{0}".format(time.ctime()))
if __name__ == '__main__':
main()