多线程编程必须理解的一些基本概念,适用于所有编程语言。内容:
并发式编程
多任务操作系统
多线程vs多进程
线程安全
线程的生命周期
线程的类型
并发式编程
不同的编程范式对软件有不同的视角。并发式编程将软件看做任务和资源的组合——任务之间竞争和共享资源,当资源满足时执行任务,否则等待资源。
并发式编程使得软件易于理解和重用,在某些场景能够极大提高性能。
多任务操作系统
要实现并发,首先需要操作系统的支持。现在的操作系统大部分都是多任务操作系统,可以“同时”执行多个任务。
多任务可以在进程或线程的层面执行。
进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间。多任务操作系统可以“并发”执行这些进程。
线程是指进程中乱序、多次执行的代码块,多个线程可以“同时”运行,所以认为多个线程是“并发”的。多线程的目的是为了最大限度的利用cpu资源。比如一个jvm进程中,所有的程序代码都以线程的方式运行。
这里面的“同时”、“并发”只是一种宏观上的感受,实际上从微观层面看只是进程/线程的轮换执行,只不过切换的时间非常短,所以产生了“并行”的感觉。
多线程vs多进程
操作系统会为每个进程分配不同的内存块,而多个线程共享进程的内存块。这带来最直接的不同就是创建线程的开销远小于创建进程的开销。
同时,由于内存块不同,所以进程之间的通信相对困难。需要采用pipe/named pipe,signal, message queue, shared memory,socket等手段;而线程间的通信简单快速,就是共享进程内的全局变量。
但是,进程的调度由操作系统负责,线程的调度就需要我们自己来考虑,避免死锁,饥饿,活锁,资源枯竭等情况的发生,这会增加一定的复杂度。而且,由于线程之间共享内存,我们还需要考虑线程安全性的问题。
线程安全
以为线程间共享进程中的全局变量,所以当其他线程改变了共享的变量时,可能会对本线程产生影响。所谓线程安全的约束是指一个函数被多个并发线程反复调用时,要一直产生正确的结果。要保证线程安全,主要是通过加锁的方式保证共享变量的正确访问。
比线程安全更严格的约束是”可重入性”,即函数在一个线程内执行的过程中被暂停,接下来又在另一个线程内被调用,之后在返回原线程继续执行。在整个过程中都能保证正确执行。保证可重入性,通常通过制作全局变量的本地拷贝来实现。
线程的生命周期
所谓的xx生命周期,其实就是某对象的包含产生和销毁的一张状态图。线程的生命周期如下图所示:
各状态的说明如下:
new新建。新创建的线程经过初始化后,进入runnable状态。
runnable就绪。等待线程调度。调度后进入运行状态。
running运行。
blocked阻塞。暂停运行,解除阻塞后进入runnable状态重新等待调度。
dead消亡。线程方法执行完毕返回或者异常终止。
可能有3种情况从running进入blocked:
同步:线程中获取同步锁,但是资源已经被其他线程锁定时,进入locked状态,直到该资源可获取(获取的顺序由lock队列控制)
睡眠:线程运行sleep()或join()方法后,线程进入sleeping状态。区别在于sleep等待固定的时间,而join是等待子线程执行完。当然join也可以指定一个“超时时间”。从语义上来说,如果两个线程a,b, 在a中调用b.join(),相当于合并(join)成一个线程。最常见的情况是在主线程中join所有的子线程。
等待:线程中执行wait()方法后,线程进入waiting状态,等待其他线程的通知(notify)。
线程的类型
主线程:当一个程序启动时,就有一个进程被操作系统(os)创建,与此同时一个线程也立刻运行,该线程通常叫做程序的主线程(main thread)。每个进程至少都有一个主线程,主线程通常最后关闭。
子线程:在程序中创建的其他线程,相对于主线程来说就是这个主线程的子线程。
守护线程:daemon thread,对线程的一种标识。守护线程为其他线程提供服务,如jvm的垃圾回收线程。当剩下的全是守护线程时,进程退出。
前台线程:相对于守护线程的其他线程称为前台线程。
python对多线程的支持
虚拟机层面
python虚拟机使用gil(global interpreter lock,全局解释器锁)来互斥线程对共享资源的访问,暂时无法利用多处理器的优势。
语言层面
在语言层面,python对多线程提供了很好的支持,python中多线程相关的模块包括:thread,threading,queue。可以方便地支持创建线程、互斥锁、信号量、同步等特性。
thread:多线程的底层支持模块,一般不建议使用。
threading:对thread进行了封装,将一些线程的操作对象化,提供下列类:
thread 线程类
timer与thread类似,但要等待一段时间后才开始运行
lock 锁原语
rlock 可重入锁。使单线程可以再次获得已经获得的锁
condition 条件变量,能让一个线程停下来,等待其他线程满足某个“条件”
event 通用的条件变量。多个线程可以等待某个事件发生,在事件发生后,所有的线程都被激活
semaphore为等待锁的线程提供一个类似“等候室”的结构
boundedsemaphore 与semaphore类似,但不允许超过初始值
queue:实现了多生产者(producer)、多消费者(consumer)的队列,支持锁原语,能够在多个线程之间提供很好的同步支持。提供的类:
queue队列
lifoqueue后入先出(lifo)队列
priorityqueue 优先队列
其中thread类是你主要的线程类,可以创建进程实例。该类提供的函数包括:
getname(self) 返回线程的名字
isalive(self) 布尔标志,表示这个线程是否还在运行中
isdaemon(self) 返回线程的daemon标志
join(self, timeout=none) 程序挂起,直到线程结束,如果给出timeout,则最多阻塞timeout秒
run(self) 定义线程的功能函数
setdaemon(self, daemonic) 把线程的daemon标志设为daemonic
setname(self, name) 设置线程的名字
start(self) 开始线程执行
第三方支持
如果你特别在意性能,还可以考虑一些“微线程”的实现:
stackless python:python的一个增强版本,提供了对微线程的支持。微线程是轻量级的线程,在多个线程间切换所需的时间更多,占用资源也更少。
greenlet:是 stackless 的副产品,其将微线程称为 “tasklet” 。tasklet运行在伪并发中,使用channel进行同步数据交换。而”greenlet”是更加原始的微线程的概念,没有调度。你可以自己构造微线程的调度器,也可以使用greenlet实现高级的控制流。
下一节,将开始用python创建和启动线程。