进程,线程,GIL,Python多线程,生产者消费者模型都是什么鬼
1. 操作系统基本知识,进程,线程
CPU是计算机的核心,承担了所有的计算任务;
操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;那么操作系统是如何进行任务调度的呢?
1.1 任务调度
大部分操作系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来。这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发(别觉得并发有多高深,它的实现很复杂,但它的概念很简单,就是一句话:多个任务同时执行)。多任务运行过程的示意图如下:
注意:当一个任务得到CPU时,相关的资源必须已经就位,然后CPU开始执行,除了CPU以外的所有就构成了这个任务的执行环境,就是所谓的程序上下文,CPU每一次任务的切换都需要保存程序的上下文,这个上下文就是下一次CPU临幸是的环境;
所以任务的轮转方法为:先加载程序A的上下文,然后开始执行A,保存程序A的上下文,调入下一个要执行的程序B的程序上下文,然后开始执行B,保存程序B的上下文。。。。
1.2 进程
说到进程,需要先提一下程序
应用程序是用于实现某种功能的一组指令的有序集合(只不过是磁盘中可执行的二进制(或者其他类型)的数据);应用程序运行于操作系统之上(只有将程序装载到内存中,系统为它分配了资源并被操作系统调用的时候才开始它们的生命周期,即运行)
进程(有时候被称为重量级进程)是具有一定独立功能的程序关于某个数据集合上的一次运行活动,是系统进行资源分配和调度的一个独立单位(不是最小单位),是应用程序运行的载体;
每一个进程都有自己的地址空间、内存、数据栈以及其他记录起运行轨迹的辅助数据。操作系统管理其上运行的所有进程,并为这些进程公平的分配时间、进程也可以通过fork和spawn操作类完成其他的任务。不过各个进程有自己的内存空间、数据栈等,所以只能使用进程间通讯(interprocess communication, IPC),而不能直接共享信息。
我们再看一下wiki上的解释:
An executing instance of a program is called a process.
Each process provides the resources needed to execute a program. A process has a virtual address space, executable code, open handles to system objects, a security context, a unique process identifier, environment variables, a priority class, minimum and maximum working set sizes, and at least one thread of execution. Each process is started with a single thread, often called the primary thread, but can create additional threads from any of its threads.
1)进程的组成和特性
进程一般由程序、数据集合和进程控制块三部分组成。程序用于描述进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需要的数据和工作区;程序控制块(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。
进程的特性:
动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
并发性:任何进程都可以同其他进程一起并发执行;
独立性:进程是系统进行资源分配和调度的一个独立单位;
结构性:进程由程序、数据和进程控制块三部分组成。
2)进程和程序的区别与联系
a)程序只是一组指令的有序集合,它本身没有任何运行的含义,它只是一个静态的实体。而进程则不同,它是程序在某个数据集上的执行。进程是一个动态的实体,它有自己的生命周期。它因创建而产生,因调度而运行,因等待资源或事件而被处于等待状态,因完成任务而被撤消。反映了一个程序在一定的数据集上运行的全部动态过程。
b)进程和程序并不是一一对应的,一个程序执行在不同的数据集上就成为不同的进程,可以用进程控制块来唯一地标识每个进程。而这一点正是程序无法做到的,由于程序没有和数据产生直接的联系,既使是执行不同的数据的程序,他们的指令的集合依然可以是一样的,所以无法唯一地标识出这些运行于不同数据集上的程序。一般来说,一个进程肯定有一个与之对应的程序,而且只有一个。而一个程序有可能没有与之对应的进程(因为它没有执行),也有可能有多个进程与之对应(运行在几个不同的数据集上)。
c)进程还具有并发性和交往性,这也与程序的封闭性不同。进程和线程都是由操作系统所体会的程序运行的基本单元,系统利用该基本单元实现系统对应用的并发性。进程和线程的区别在于:简而言之,一个程序至少有一个进程,一个进程至少有一个线程. 。
3)为什么还要线程
进程有很多优点,它提供了多道编程,让我们感觉我们每个人都拥有自己的CPU和其他资源,可以提高计算机的利用率。很多人就不理解了,既然进程这么优秀,为什么还要线程呢?其实,仔细观察就会发现进程还是有很多缺陷的,比如:
a)进程只能在一个时间干一件事,如果想同时干两件事或多件事,进程就无能为力了。
b)进程在执行的过程中如果阻塞,例如等待输入,整个进程就会挂起,即使进程中有些工作不依赖于输入的数据,也将无法执行。
c)程序变得越来越复杂,对CPU的要求也越来越高(对多个任务之间上下文切换的效率要求越来越高),而进程之间的切换开销较大,无法满足需求
于是就有了线程
1.3 线程
首先,早期操作系统没有线程的概念,那时,进程不但是拥有资源和独立运行的最小单位,也是程序执行和任务调度的最小单位;但是随着程序变得越来越复杂,系统对多个任务之间上下文切换的效率要求越来越高,而进程之间的切换开销较大,无法满足需求。于是发明了线程;
线程(轻量级进程)是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元(比进程更小的能独立运行的基本单位),是CPU处理器调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间),每条线程并行执行不同的任务。
线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈);
一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。
线程有开始、顺序执行和结束三部分。它有一个自己的指令指针,记录自己运行到什么地方。线程的运行可能被占用(中断),或者暂时的被挂起(睡眠),让其他的线程运行,这叫做让步。一个进程中的各个线程之间共享同一片数据空间,所以线程之间可以比进程之间更加方便的共享数据以及相互通讯。
Wiki上对线程的定义:
A thread is an execution context, which is all the information a CPU needs to execute a stream of instructions.
Suppose you're reading a book, and you want to take a break right now, but you want to be able to come back and resume reading from the exact point where you stopped. One way to achieve that is by jotting down the page number, line number, and word number. So your execution context for reading a book is these 3 numbers.
If you have a roommate, and she's using the same technique, she can take the book while you're not using it, and resume reading from where she stopped. Then you can take it back, and resume it from where you were.
Threads work in the same way. A CPU is giving you the illusion that it's doing multiple computations at the same time. It does that by spending a bit of time on each computation. It can do that because it has an execution context for each computation. Just like you can share a book with your friend, many tasks can share a CPU.
On a more technical level, an execution context (therefore a thread) consists of the values of the CPU's registers.
Last: threads are different from processes. A thread is a context of execution, while a process is a bunch of resources associated with a computation. A process can have one or many threads.
Clarification: the resources associated with a process include memory pages (all the threads in a process have the same view of the memory), file descriptors (e.g., open sockets), and security credentials (e.g., the ID of the user who started the process).
1)进程与线程的关系
线程是程序执行的最小单位,是CPU处理器调度和分派的基本单位;而进程是操作系统分配资源的最小单位;一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线,某进程内的线程在其它进程不可见;
a)划分尺度:线程更小,所以多线程程序并发性更高;一个线程可以创建和撤销另一个线程;
b)资源分配:进程是资源分配的基本单位,同一进程内各个线程共享其资源(如打开文件和信号);
c)地址空间:进程拥有独立的地址空间,同一进程内各个线程共享其内存空间(包括代码段、数据集、堆等);
d)处理器调度和切换:线程是处理器调度的基本单位;线程上下文切换比进程上下文切换要快得多,线程开销比进程小很多;
e)执行:线程不能单独执行,必须组成进程,一个进程至少有一个主线程。简而言之,一个程序至少有一个进程,一个进程至少有一个线程;
线程在执行过程中与进程还是有区别的。每个独立的线程有一个程序运行的入口、顺序执行序列和程序的出口。但是线程不能够独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。从逻辑角度来看,多线程的意义在于一个应用程序中,有多个执行部分可以同时执行。但操作系统并没有将多个线程看做多个独立的应用,来实现进程的调度和管理以及资源分配。这就是进程和线程的重要区别。
线程与进程共享关系示意图:
wiki上关于进程线程关系的解释:
a)Threads share the address space of the process that created it; processes have their own address space.
b)Threads have direct access to the data segment of its process; processes have their own copy of the data segment of the parent process.
c)Threads can directly communicate with other threads of its process; processes must use interprocess communication to communicate with sibling processes.
d)New threads are easily created; new processes require duplication of the parent process.
e)Threads can exercise considerable control over threads of the same process; processes can only exercise control over child processes.
f)Changes to the main thread (cancellation, priority change, etc.) may affect the behavior of the other threads of the process; changes to the parent process does not affect child processes.
1.4 多线程与多核
线程一般都是并发执行的。正是由于这种并发和数据共享的机制使得多个任务合作变为可能。实际上,在单CPU的系统中,真正的并发是不可能的,每个线程会被安排成每次只运行一小会儿,然后就把CPU让出来,让其他的线程去运行。那么,如果有多颗CPU,准确的说是多个核心呢?
1)CPU
我们经常看到CPU的参数:双核心四线程,四核心四线程等等;到底是什么意思呢?
多核(心)处理器是指在一个处理器上集成多个运算核心从而提高计算能力,每一个处理核心对应一个内核线程。那双核心四线程是怎么回事呢?因为使用了超线程技术,采用超线程技术(HT)将一个物理处理核心模拟成两个逻辑处理核心,对应两个内核线程。
我们来看几个CPU常用概念:
a)物理CPU :实际Server中插槽上的CPU个数,物理cpu数量;
b)CPU核心数:CPU的核心数是指物理上,也就是硬件上存在着几个核心。比如,双核就是包括2个相对独立的CPU核心单元组,四核就包含4个相对独立的CPU核心单元组。
c) 逻辑CPU:逻辑CPU数量=物理cpu数量 * 每颗CPU的核心数*2(如果支持并开启HT,HT是intel的超线程技术)
d)线程数(内核线程):线程数就是逻辑CPU数目
e)查看CPU信息
-- windows:
cmd命令中输入“wmic”,然后在出现的新窗口中输入“cpu get *”即可查看物理CPU数、CPU核心数、线程数。其中:
Name:表示物理CPU数
NumberOfCores:表示CPU核心数
NumberOfLogicalProcessors:表示CPU线程数
-- Linux:
Linux下top查看的CPU也是逻辑CPU个数
#逻辑CPU个数
cat /proc/cpuinfo | grep "processor" | sort –u | wc -l
#物理CPU个数:
cat /proc/cpuinfo | grep "physical id" | sort -u | wc -l
#CPU核心数
grep 'core id' /proc/cpuinfo | sort -u | wc -l
2)内核进程和用户进程
上面讲到逻辑CPU数目就是线程数,这个线程指的是内核线程,到底什么是内核线程呢?
内核线程(Kernel Thread, KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。一般一个处理核心对应一个内核线程。
程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程(我们在这称它为用户线程),由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。用户线程与内核线程的对应关系有三种模型:一对一模型、多对一模型、多对多模型;
在这以4个内核线程、3个用户线程为例对三种模型进行说明:
a)一对一模型
对于一对一模型来说,一个用户线程就唯一地对应一个内核线程(反过来不一定成立,一个内核线程不一定有对应的用户线程)。
线程之间的并发是真正的并发。一对一模型使用户线程具有与内核线程一样的优点,一个线程因某种原因阻塞时其他线程的执行不受影响;此处,一对一模型也可以让多线程程序在多处理器的系统上有更好的表现。
但一对一模型也有两个缺点:1.许多操作系统限制了内核线程的数量,因此一对一模型会使用户线程的数量受到限制;2.许多操作系统内核线程调度时,上下文切换的开销较大,导致用户线程的执行效率下降。
b)多对一模型
多对一模型将多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行,因此相对一对一模型,多对一模型的线程切换速度要快许多;此外,多对一模型对用户线程的数量几乎无限制。但多对一模型也有两个缺点:1.如果其中一个用户线程阻塞,那么其它所有线程都将无法执行,因为此时内核线程也随之阻塞了;2.在多处理器系统上,处理器数量的增加对多对一模型的线程性能不会有明显的增加,因为所有的用户线程都映射到一个处理器上了。
c)多对多模型
多对多模型结合了一对一模型和多对一模型的优点,将多个用户线程映射到多个内核线程上。多对多模型的优点有:1.一个用户线程的阻塞不会导致所有线程的阻塞,因为此时还有别的内核线程被调度来执行;2.多对多模型对用户线程的数量没有限制;3.在多处理器的操作系统中,多对多模型的线程也能得到一定的性能提升,但提升的幅度不如一对一模型的高。
在现在流行的操作系统中,大都采用多对多的模型。
关于进程,线程,这里介绍的还是简单,建议找一本关于操作系统的书好好研读一番,现在我们回归到Python
2. Python多线程与GIL
首先,让我们看一个问题,运行下面这段python代码,看看CPU占用率多少?
def dead_loop(): while True: pass dead_loop()
因为,我的电脑是双核四线程,所以这个死循环的CPU使用率是由25%
那么如果我使用多线程,运行两个线程,这两个线程的CPU利用率不是就到50%了吗!试一下:
import threading def dead_loop(): while True: pass # 新起一个死循环线程 t = threading.Thread(target=dead_loop) t.start() # 主线程也进入死循环 dead_loop()
但是,实际运行结果还是25%;为什么会这样呢?幕后黑手就是GIL
2.1 GIL
我们先看一下官方对GIL的解释
In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.)
简单的一句话包含了很多信息;
a)在Python众多解释器中,只有Cpython才有GIL,JPython就没有;因为CPython是大部分环境下默认的Python执行环境。所以在很多人的概念里CPython就是Python,也就想当然的把GIL
归结为Python语言的缺陷。明确一点,GIL并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念,Python完全可以不依赖于GIL;
看一下CPython的源代码
static PyThread_type_lock interpreter_lock = 0; /* This is the GIL */
这一行代码摘自 ceval.c —— CPython 2.7 解释器的源代码,Guido van Rossum 的注释”This is the GIL“ 添加于2003 年,但这个锁本身可以追溯到1997年他的第一个多线程 Python 解释器。在 Unix系统中,PyThread_type_lock 是标准 C mutex_t 锁的别名。当 Python 解释器启动时它初始化:
void PyEval_InitThreads(void) { interpreter_lock = PyThread_allocate_lock(); PyThread_acquire_lock(interpreter_lock); }
解释器中的所有 C 代码在执行 Python 时必须保持这个锁。
b)GIL是一把互斥锁
Python代码的执行由Python虚拟机(也叫解释器主循环)来控制,而对Python虚拟机的访问由GIL(全局解释器锁)控制,GIL保证了在任意时刻,只有一个线程在解释器中运行,就像单CPU系统运行多线程一样,内存中可以存放多个程序,但在任意时刻,只有一个线程在解释器中运行;
c)GIL是历史遗留问题,为了解决线程安全的简单粗暴做法
多线程编程可以更有效地利用多核处理器,但是随之带来的就是线程间数据一致性和状态同步的困难(线程安全);多核 CPU 在 1990 年代还属于类科幻,Guido van Rossum 在创造 python 的时候,也想不到他的语言有一天会被用到很可能 1000+ 个核的 CPU 上面,一个全局锁搞定多线程安全在那个时代应该是最简单经济的设计了。简单而又能满足需求,那就是合适的设计(对设计来说,应该只有合适与否,而没有好与不好)。
线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。 线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。
d)GIL的执行机理
记住一个原则:“一个线程运行 Python ,而其他 N 个睡眠或者等待 I/O.”(One thread runs Python, while others sleep or await I/O)
多线程环境中,python虚拟机按以下方式执行:
- 设置GIL
- 切换到一个线程去执行
- 运行
- 指定数量的字节码指令(python2为1000字节指令)或运行了执行时间(python3为15ms)---抢占式多任务处理
- 线程主动让出控制(可以调用time.sleep(0)) -------协同式多任务处理
- 把线程设置完睡眠状态
- 解锁GIL
- 再次重复以上步骤
对所有面向 I/O 的(会调用内建的操作系统 C 代码的)程序来说,GIL 会在这个 I/O 调用之前被释放, 以允许其它的线程在这个线程等待 I/O 的时候运行。 如果某线程并未使用很多 I/O 操作,它会在自己的时间片内一直占用处理器(和 GIL)。也就是说,I/O 密集型(程序大量时间花费在等待I/O操作,CPU总是闲置,在10%左右(如:网络请求socket))的 Python 程序比计算密集型(程序线性执行,大量占用CPU,总是接近100%(如:正则匹配替换大量文本))的程序更能充分利用多线程环境的好处。
线程何时切换?一个线程无论何时开始睡眠或等待网络 I/O,其他线程总有机会获取 GIL 执行 Python 代码。这是协同式多任务处理。CPython 也还有抢占式多任务处理。如果一个线程不间断地在 Python 2 中运行 1000 字节码指令,或者不间断地在 Python 3 运行15 毫秒,那么它便会放弃 GIL,而其他线程可以运行。把这想象成旧日有多个线程但只有一个 CPU 时的时间片。现在,将具体讨论这两种多任务处理。
协同式多任务处理
当一项任务比如网络 I/O启动,而在长的或不确定的时间,没有运行任何 Python 代码的需要,一个线程便会让出GIL,从而其他线程可以获取 GIL 而运行 Python。这种礼貌行为称为协同式多任务处理,它允许并发;多个线程同时等待不同事件。
def do_connect(): s = socket.socket() s.connect(('python.org', 80)) # drop the GIL for i in range(2): t = threading.Thread(target=do_connect) t.start()
两个线程在同一时刻只能有一个执行 Python ,但一旦线程开始连接,它就会放弃 GIL ,这样其他线程就可以运行。这意味着两个线程可以并发等待套接字连接,这是一件好事。在同样的时间内它们可以做更多的工作。
让我们打开盒子,看看一个线程在连接建立时实际是如何放弃 GIL 的,在 socketmodule.c 中:
/* s.connect((host, port)) method */ static PyObject * sock_connect(PySocketSockObject *s, PyObject *addro) { sock_addr_t addrbuf; int addrlen; int res; /* convert (host, port) tuple to C address */ getsockaddrarg(s, addro, SAS2SA(&addrbuf), &addrlen); Py_BEGIN_ALLOW_THREADS res = connect(s->sock_fd, addr, addrlen); Py_END_ALLOW_THREADS /* error handling and so on .... */ }
线程正是在Py_BEGIN_ALLOW_THREADS 宏处放弃 GIL;它被简单定义为:
PyThread_release_lock(interpreter_lock);
当然 Py_END_ALLOW_THREADS 重新获取锁。一个线程可能会在这个位置堵塞,等待另一个线程释放锁;一旦这种情况发生,等待的线程会抢夺回锁,并恢复执行你的Python代码。简而言之:当N个线程在网络 I/O 堵塞,或等待重新获取GIL,而一个线程运行Python。
抢占式多任务处理
Python线程可以主动释放 GIL,也可以先发制人抓取 GIL 。
让我们回顾下 Python 是如何运行的。你的程序分两个阶段运行。首先,Python文本被编译成一个名为字节码的简单二进制格式。第二,Python解释器的主回路,一个名叫 pyeval_evalframeex() 的函数,流畅地读取字节码,逐个执行其中的指令。
当解释器通过字节码时,它会定期放弃GIL,而不需要经过正在执行代码的线程允许,这样其他线程便能运行:默认情况下,检测间隔是1000 字节码。所有线程都运行相同的代码,并以相同的方式定期从他们的锁中抽出。在 Python 3 GIL 的实施更加复杂,检测间隔不是一个固定数目的字节码,而是15 毫秒。然而,对于你的代码,这些差异并不显著。
e)应对GIL
在多核时代,编程的免费午餐没有了。如果程序不能用并发挤干每个核的运算性能,那就意谓着会被淘汰。对软件如此,对语言也是一样。那 Python 的对策呢?
Python 的应对很简单,以不变应万变。在 python 3 中依然有 GIL。之所以不去掉,原因嘛,不外以下几点:
欲练神功,挥刀自宫
CPython 的 GIL 本意是用来保护所有全局的解释器和环境状态变量的。如果去掉 GIL,就需要多个更细粒度的锁对解释器的众多全局状态进行保护。或者采用 Lock-Free 算法。无论哪一种,要做到多线程安全都会比单使用 GIL 一个锁要难的多。而且改动的对象还是有 20 年历史的 CPython 代码树,更不论有这么多第三方的扩展也在依赖 GIL。对 Python 社区来说,这不异于挥刀自宫,重新来过。
就算自宫,也未必成功
有位牛人曾经做了一个验证用的 CPython,将 GIL 去掉,加入了更多的细粒度锁。但是经过实际的测试,对单线程程序来说,这个版本有很大的性能下降,只有在利用的物理 CPU 超过一定数目后,才会比 GIL 版本的性能好。这也难怪。单线程本来就不需要什么锁。单就锁管理本身来说,锁 GIL 这个粗粒度的锁肯定比管理众多细粒度的锁要快的多。而现在绝大部分的 python 程序都是单线程的。再者,从需求来说,使用 python 绝不是因为看中它的运算性能。就算能利用多核,它的性能也不可能和 C/C++ 比肩。费了大力气把 GIL 拿掉,反而让大部分的程序都变慢了,这不是南辕北辙吗。
还是曲线救国,试试其他神功吧
1. 使用多进程模块Multiprocess
还是回到我们开始的那个CPU占有率的实验,fork一个子进程来实现两个死循环:
from multiprocessing import Process, freeze_support def dead_loop(): while True: pass if __name__ == '__main__': freeze_support() #fork一个子进程 t = Process(target=dead_loop) t.start() dead_loop()
结果:
如我们所预期的,出现了两个cpu利用率的25%的进程;
multiprocessing库的出现很大程度上是为了弥补thread库因为GIL而低效的缺陷。它完整的复制了一套thread所提供的接口方便迁移。唯一的不同就是它使用了多进程而不是多线程。每个进程有自己的独立的GIL,因此也不会出现进程之间的GIL争抢。
当然multiprocessing也不是万能良药。它的引入会增加程序实现时线程间数据通讯和同步的困难。就拿计数器来举例子,如果我们要多个线程累加同一个变量,对于thread来说,申明一个global变量,用thread.Lock的context包裹住三行就搞定了。而multiprocessing由于进程之间无法看到对方的数据,只能通过在主线程申明一个Queue,put再get或者用share memory的方法。这个额外的实现成本使得本来就非常痛苦的多线程程序编码,变得更加痛苦了。
2. 使用其他解析器,像JPython和IronPython这样的解析器由于实现语言的特性,他们不需要GIL的帮助。然而由于用了Java/C#用于解析器实现,他们也失去了利用社区众多C语言模块有用特性的机会。所以这些解析器也因此一直都比较小众。
3. 如果不想用多进程这样重量级的解决方案,还有个更彻底的方案,放弃 Python,改用 C/C++。当然,你也不用做的这么绝,只需要把关键部分用 C/C++ 写成 Python 扩展,其它部分还是用 Python 来写,让 Python 的归 Python,C 的归 C。一般计算密集性的程序都会用 C 代码编写并通过扩展的方式集成到 Python 脚本里(如 NumPy 模块)。在扩展里就完全可以用 C 创建原生线程,而且不用锁 GIL,充分利用 CPU 的计算资源了。不过,写 Python 扩展总是让人觉得很复杂。好在 Python 还有另一种与 C 模块进行互通的机制 : ctypes;
最后总结一下:
- 因为GIL的存在,只有IO Bound场景下得多线程会得到较好的性能
- 如果对并行计算性能较高的程序可以考虑把核心部分也成C模块,或者索性用其他语言实现
- GIL在较长一段时间内将会继续存在,但是会不断对其进行改进
2.2 Python多线程应用
常用模块两个:thread和threading;threading是高级模块,建议不要使用thread,很明显的一个原因是:thread模块在主线程退出时,所有其他线程没有被清除就退出了。但threading模块可以确保所有子线程都退出后,进程才会结束;
1)threading模块基本应用
多线程模块有两种方式
a)直接调用
1 import threading 2 from time import sleep, ctime 3 4 #每个线程执行的时间 5 loop_time_list = (4, 2) 6 7 #线程执行的函数 8 def loop(loop_mem, loop_time): 9 print('start loop %s at %s' % (loop_mem, ctime())) 10 sleep(loop_time) 11 print('loop %s done at %s' % (loop_mem, ctime())) 12 13 14 def main(): 15 print('Programming start at: %s' % ctime()) 16 threads = [] 17 #线程数 18 loop_num = range(len(loop_time_list)) 19 20 #create threads 21 for i in loop_num: 22 t = threading.Thread(target=loop, args=(i, loop_time_list[i],)) 23 threads.append(t) 24 25 #start threads 26 for i in loop_num: 27 threads[i].start() 28 29 print('all done at: %s' % ctime()) 30 31 if __name__ == '__main__': 32 main()
b)面向对象方式调用
1 import threading 2 from time import sleep, ctime 3 4 #每个线程执行的时间 5 loop_time_list = (4, 2) 6 7 class MyThread(threading.Thread): 8 def __init__(self, func, args): 9 threading.Thread.__init__(self) 10 self.func = func 11 self.args = args 12 def run(self): 13 self.func(*self.args) 14 15 #线程执行的函数 16 def loop(loop_mem, loop_time): 17 print('start loop %s at %s' % (loop_mem, ctime())) 18 sleep(loop_time) 19 print('loop %s done at %s' % (loop_mem, ctime())) 20 21 22 def main(): 23 print('Programming start at: %s' % ctime()) 24 threads = [] 25 #线程数 26 loop_num = range(len(loop_time_list)) 27 28 #create threads 29 for i in loop_num: 30 t = MyThread(loop, (i, loop_time_list[i])) 31 threads.append(t) 32 33 #start threads 34 for i in loop_num: 35 threads[i].start() 36 37 print('all done at: %s' % ctime()) 38 39 if __name__ == '__main__': 40 main()
本质上就是创建了一个继承自threading.Thread的类,在构造函数中执行了threading.Thread的构造方法,重写run方法;可以通过IDE的断点查看
看源码会发现,调用顺序为:
start()->_bootstrap()->_bootstrap_inner()->run()
在run方法中:
1 def run(self): 2 """Method representing the thread's activity. 3 4 You may override this method in a subclass. The standard run() method 5 invokes the callable object passed to the object's constructor as the 6 target argument, if any, with sequential and keyword arguments taken 7 from the args and kwargs arguments, respectively. 8 9 """ 10 try: 11 if self._target: 12 self._target(*self._args, **self._kwargs) 13 finally: 14 # Avoid a refcycle if the thread is running a function with 15 # an argument that has a member that points to the thread. 16 del self._target, self._args, self._kwargs
可以看到,在run方法里运行了_target,在threading.Thread的构造函数中:
self._target = target
所以,最终调用的是run方法
扩展,多线程如何获取线程返回值,其实就是面向对象内容:
import threading class MyThread(threading.Thread): def __init__(self,func,args=()): super(MyThread,self).__init__() self.func = func self.args = args def run(self): self.result = self.func(*self.args) def get_result(self): try: return self.result # 如果子线程不使用join方法,此处可能会报没有self.result的错误 except Exception: return None def foo(a,b,c): time.sleep(1) return a*2,b*2,c*2 st = time.time() li = [] for i in xrange(4): t = MyThread(foo,args=(i,i+1,i+2)) li.append(t) t.start() for t in li: t.join() # 一定要join,不然主线程比子线程跑的快,会拿不到结果 print(t.get_result()) et = time.time() print(et - st)
2)threading.Thread类
threading的Thread类是主要的运行对象,看一下这个类中的主要方法
其中的start()和run()我们已经了解过了
a)join(timeout=None)
前面我们成功使用了多线程,让我们看一下结果:
首先,主线程提前结束,但是在主线程已经退出的情况下,子线程没有被强制退出,而是继续执行,直到所有子线程都退出,进程才结束;这是优于thread模块的地方;
但是,如果我们希望主线程不要提前结束呢?或者说,在子线程执行的过程中,挂起主线程,等到子线程执行结束后,再恢复主线程运行呢?有,使用join方法
import threading from time import sleep, ctime #每个线程执行的时间 loop_time_list = (4, 2) #线程执行的函数 def loop(loop_mem, loop_time): print('start loop %s at %s' % (loop_mem, ctime())) sleep(loop_time) print('loop %s done at %s' % (loop_mem, ctime())) def main(): print('Programming start at: %s' % ctime()) threads = [] #线程数 loop_num = range(len(loop_time_list)) #create threads for i in loop_num: t = threading.Thread(target=loop, args=(i, loop_time_list[i]), ) threads.append(t) #start threads for i in loop_num: threads[i].start() for i in loop_num: threads[i].join() print('all done at: %s' % ctime()) if __name__ == '__main__': main()
结果:
完美解决;
有时,有的线程执行时间太久,我们不希望因为一个线程而让整个程序阻塞,就可以通过设置timeout来解决;比如对上例的join方法做简单修改:
for i in loop_num: threads[i].join(timeout=3)
完美;
b)守护线程Daemon
threading模块创建的线程默认是非守护线程;
守护线程 daemon thread
守护线程, 是指在程序运行的时候在后台提供一种通用服务的线程, 比如垃圾回收线程就是一个很称职的守护者, 并且这种线程并不属于程序中不可或缺的部分. 因此, 当所有的非守护线程结束时, 程序也就终止了, 同时会杀死进程中的所有守护线程. 反过来说, 只要任何非守护线程还在运行, 程序就不会终止.
1 import threading 2 from time import sleep, ctime 3 4 #每个线程执行的时间 5 loop_time_list = (4, 2) 6 7 #线程执行的函数 8 def loop(loop_mem, loop_time): 9 print('start loop %s at %s' % (loop_mem, ctime())) 10 sleep(loop_time) 11 print('loop %s done at %s' % (loop_mem, ctime())) 12 13 14 def main(): 15 print('Programming start at: %s' % ctime()) 16 threads = [] 17 #线程数 18 loop_num = range(len(loop_time_list)) 19 20 #create threads 21 for i in loop_num: 22 t = threading.Thread(target=loop, args=(i, loop_time_list[i]), ) 23 threads.append(t) 24 25 #将第一个线程(执行4s)设置为守护线程 26 threads[0].setDaemon(True) 27 28 #start threads 29 for i in loop_num: 30 threads[i].start() 31 32 print('all done at: %s' % ctime()) 33 34 if __name__ == '__main__': 35 main()
结果:
因为守护线程loop 0的运行时间为4s,而非守护线程loop 1的运行时间为2s,当loop 0运行结束,主线程运行结束后;程序就退出了,而没有等待守护线程loop 0之行结束
3)Thread类的其他对象
a)线程锁(互斥锁Mutex)LOCK和RLOCK
首先,给出结论;即使Python拥有GIL,很大程度上保证了线程安全,但是,有时仍然需要加锁来保护共享的可变状态;
为什么呢?GIL不是保证了同一时间只有一个线程进入python虚拟机运行吗!
让我们先看一段代码:
n = 0 def foo(): global n n += 1
让我们看一下这个函数用 Python 的标准 dis 模块编译的字节码:
>>> import dis >>> dis.dis(foo) LOAD_GLOBAL 0 (n) LOAD_CONST 1 (1) INPLACE_ADD STORE_GLOBAL 0 (n)
代码的一行中, n += 1,被编译成 4 个字节码,进行 4 个基本操作:
1. 将 n 值加载到堆栈上
2. 将常数 1 加载到堆栈上
3. 将堆栈顶部的两个值相加
4. 将总和存储回n
注意对于n值,每个线程都会有一个加载和恢复n值的工作;我们知道一个线程每运行 1000 字节码,就会被解释器打断夺走 GIL 。如果运气不好,这(打断)可能发生在线程加载 n 值到堆栈期间,以及把它存储回 n 期间。很容易可以看到这个过程会如何导致更新丢失:
比如,如果我起了100个线程来执行foo()函数,结果理论上应该是100,但有时可能会看到99,98;所以,尽管有GIL,仍然需要加锁来保护共享的可变状态
但是,对于原子操作,比如sort()就不需要加锁;感兴趣的可以了解一下
现在来加锁吧
1 import threading 2 3 def addNum(): 4 global num 5 #获得锁 6 lock.acquire() 7 num += 1 8 #释放锁 9 lock.release() 10 11 def main(): 12 threads = [] 13 for i in range(100): 14 t = threading.Thread(target=addNum) 15 threads.append(t) 16 for i in range(100): 17 threads[i].start() 18 for i in range(100): 19 threads[i].join() 20 print('num: ', num) 21 22 num = 0 23 lock = threading.Lock() 24 25 if __name__ == '__main__': 26 main()
RLOCK就是在加多重锁;
b)Semaphorre(信号量)
互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如原来的厕所只有一个坑,那么就配一把钥匙,谁拿到钥匙谁上;现在我在这个厕所里多加了两个坑,那么就可以多配两把钥匙,这样就可以有3个人同时上;其他人只能在外边排队了;可以看出来mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
1 import threading 2 import time 3 4 def addNum(): 5 global num 6 #获得锁 7 semap.acquire() 8 num += 1 9 print('current num:', num) 10 time.sleep(2) 11 #释放锁 12 semap.release() 13 14 def main(): 15 threads = [] 16 for i in range(40): 17 t = threading.Thread(target=addNum) 18 threads.append(t) 19 for i in range(40): 20 threads[i].start() 21 for i in range(40): 22 threads[i].join() 23 print('final num: ', num) 24 25 num = 0 26 semap = threading.BoundedSemaphore(4) 27 28 if __name__ == '__main__': 29 main()
通过结果可以看到current num是一次性打印4个;MySQL的最大连接数就是这样实现的
c)Timer
定时器,start()以后等待n秒以后再执行
1 import threading 2 import time 3 4 def addNum(): 5 global num 6 num += 1 7 print('current num:', num) 8 time.sleep(2) 9 10 def main(): 11 threads = [] 12 for i in range(40): 13 #定时器,设置为3s 14 t = threading.Timer(3, addNum) 15 threads.append(t) 16 for i in range(40): 17 #存在定时器,3s以后再开始执行 18 threads[i].start() 19 for i in range(40): 20 threads[i].join() 21 print('final num: ', num) 22 23 num = 0 24 25 if __name__ == '__main__': 26 main()
d)event
线程的事件处理,事件主要提供了四个方法 set、wait、clear,isSet
事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。
clear:将“Flag”设置为False
set:将“Flag”设置为True
我们可以通过event来模拟一个假的异步模型
1 import threading 2 import time 3 4 def producer(): 5 print('厨师:等人买包子') 6 event.wait() 7 event.clear() 8 print('厨师:有人来买包子了,开始做包子') 9 time.sleep(3) 10 print('厨师:你的包子做好了') 11 event.set() 12 13 def consumer(): 14 print('客户:老板,买包子') 15 event.set() 16 time.sleep(1) 17 print('客户:老板,快点') 18 event.wait() 19 print('客户:谢谢老板,真好吃') 20 21 event = threading.Event() 22 p = threading.Thread(target=producer) 23 c = threading.Thread(target=consumer) 24 p.start() 25 c.start()
结果:
厨师:等人买包子
客户:老板,买包子
厨师:有人来买包子了,开始做包子
客户:老板,快点
厨师:你的包子做好了
客户:谢谢老板,真好吃
实际上,上边并非一个真正的异步模型,真正的异步模型是当客户等待包子做好的过程中还可以干别的事情,只是需要不断地去问老板“包子好了没”;等包子好了再回来付钱买包子;
通过isSet方法可以实现这个异步:
1 import threading 2 import time 3 4 def producer(): 5 print('厨师:等人买包子') 6 event.wait() 7 event.clear() 8 print('厨师:有人来买包子了,开始做包子') 9 time.sleep(5) 10 print('厨师:你的包子做好了') 11 event.set() 12 13 def consumer(): 14 print('客户:老板,买包子') 15 event.set() 16 time.sleep(1) 17 while not event.isSet(): 18 print('客户:老板还没好啊!饿死了') 19 print('客户:我先干点别的吧,睡1秒') 20 time.sleep(1) 21 print('客户:谢谢老板,真好吃') 22 23 event = threading.Event() 24 p = threading.Thread(target=producer) 25 c = threading.Thread(target=consumer) 26 p.start() 27 c.start()
结果:
厨师:等人买包子
客户:老板,买包子
厨师:有人来买包子了,开始做包子
客户:老板还没好啊!饿死了
客户:我先干点别的吧,睡1秒
客户:老板还没好啊!饿死了
客户:我先干点别的吧,睡1秒
客户:老板还没好啊!饿死了
客户:我先干点别的吧,睡1秒
客户:老板还没好啊!饿死了
客户:我先干点别的吧,睡1秒
厨师:你的包子做好了
客户:谢谢老板,真好吃
这样就没有阻塞了,客户在等待期间也可以干别的了
4)queue模块
queue模块用于进行线程间通讯,让各个线程之间共享数据;而且queue是线程安全的
a)queue模块提供3种队列:
1. class queue.
Queue
(maxsize=0) #先进先出
2. class queue.
LifoQueue
(maxsize=0) #后进先出
3. class queue.
PriorityQueue
(maxsize=0) #存储数据时可设置优先级的队列
其中maxsize是队列的最大规模,如果maxsize<=0,那么队列就是无限大
>>> >>> from queue import Queue >>> q = Queue(3) >>> q.put('first_in') >>> q.put('second_in') >>> q.put('third_in') >>> >>> q.get() 'first_in' >>> q.get() 'second_in' >>> q.get() 'third_in' >>> >>> from queue import LifoQueue >>> q_L = LifoQueue(3) >>> q_L.put('first_in') >>> q_L.put('second_in') >>> q_L.put('third_in')) >>> q_L.put('third_in') >>> >>> >>> q_L.get() 'third_in' >>> q_L.get() 'second_in' >>> q_L.get() 'first_in' >>> >>> from queue import PriorityQueue >>> q_P = PriorityQueue(3) >>> q_P.put((6,'first_in')) >>> q_P.put((1,'second_in')) >>> q_P.put((10,'third_in')) >>> >>> q_P.get() (1, 'second_in') >>> q_P.get() (6, 'first_in') >>> q_P.get() (10, 'third_in') >>>
- b)常见的两个异常:
- 1. exception
queue.
Empty
- Exception raised when non-blocking
get()
(orget_nowait()
) is called on aQueue
object which is empty. - 2. exception
queue.
Full
- Exception raised when non-blocking
put()
(orput_nowait()
) is called on aQueue
object which is full. - c)Queue队列常用方法:
方法应用起来非常简单,自己试验一下即可,这里不做赘述
5)生产者消费者模型以及python线程间通信
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
随着软件业的发展,互联网用户的日渐增多,并发这门艺术的兴起似乎是那么合情合理。每日PV十多亿的淘宝,处理并发的手段可谓是业界一流。用户访问淘宝首页的平均等待时间只有区区几秒,但是服务器所处理的流程十分复杂。首先负责首页的服务器就有好几千台,通过计算把与用户路由最近的服务器处理首页的返回。其次是网页上的资源,就JS和CSS文件就有上百个,还有图片资源等。它能在几秒内加载出来。
而在大型电商网站中,他们的服务或者应用解耦之后,是通过消息队列在彼此间通信的。消息队列和应用之间的架构关系就是生产者消费者模型。
生产者:负责产生数据的模块(此处的模块是广义的,可以是类、函数、线程、进程等)。
消费者:处理数据的模块。
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。在这个模型中,最关键就是内存缓冲区为空的时候消费者必须等待,而内存缓冲区满的时候,生产者必须等待。其他时候可以是个动态平衡。
生产者消费者模式的优点:
a)解耦
假设生产者和消费者分别是两个类。如果让生产者直接调用消费者的某个方法,那 么生产者对于消费者就会产生依赖(也就是耦合)。将来如果消费者的代码发生变化, 可能会影响到生产者。而如果两者都依赖于某个缓冲区,两者之间不直接依赖,耦合也 就相应降低了。
举个例子,我们去邮局投递信件,如果不使用邮筒(也就是缓冲区),你必须得把 信直接交给邮递员。有同学会说,直接给邮递员不是挺简单的嘛?其实不简单,你必须 得认识谁是邮递员,才能把信给他(光凭身上穿的制服,万一有人假冒,就惨了)。这 就产生和你和邮递员之间的依赖(相当于生产者和消费者的强耦合)。万一哪天邮递员 换人了,你还要重新认识一下(相当于消费者变化导致修改生产者代码)。而邮筒相对 来说比较固定,你依赖它的成本就比较低(相当于和缓冲区之间的弱耦合)。
b)支持并发
由于生产者与消费者是两个独立的并发体,他们之间是用缓冲区作为桥梁连接,生产者只需要往缓冲区里丢数据,就可以继续生产下一个数据,而消费者只需要从缓冲区了拿数据即可,这样就不会因为彼此的处理速度而发生阻塞。
接上面的例子,如果我们不使用邮筒,我们就得在邮局等邮递员,直到他回来,我们把信件交给他,这期间我们啥事儿都不能干(也就是生产者阻塞),或者邮递员得挨家挨户问,谁要寄信(相当于消费者轮询)。
c)支持忙闲不均
缓冲区还有另一个好处。如果制造数据的速度时快时慢,缓冲区的好处就体现出来 了。当数据制造快的时候,消费者来不及处理,未处理的数据可以暂时存在缓冲区中。 等生产者的制造速度慢下来,消费者再慢慢处理掉。
为了充分复用,我们再拿寄信的例子来说事。假设邮递员一次只能带走1000封信。 万一某次碰上情人节(也可能是圣诞节)送贺卡,需要寄出去的信超过1000封,这时 候邮筒这个缓冲区就派上用场了。邮递员把来不及带走的信暂存在邮筒中,等下次过来 时再拿走。
实际应用:
在版本升级项目中,信息服务器要接收大批量的客户端请求,原来那种串行化的 处理,根本无法及时处理客户端请求,造成信息服务器大量请求堆积,导致丢包异 常严重。之后就采用了生产者消费者模式,在业务请求与业务处理间,建立了一个List 类型的缓冲区,服务端接收到业务请求,就往里扔,然后再去接收下一个业务请求,而 多个业务处理线程,就会去缓冲区里取业务请求处理。这样就大大提高了服务器的相 应速度。
Python中应用:
1 from threading import Thread, RLock 2 from queue import Queue 3 import time 4 5 q = Queue(10) 6 count = 0 7 l = RLock() 8 9 #创建生产者 10 class Producer(Thread): 11 def __init__(self, name, que): 12 super(Producer, self).__init__() 13 self.__name = name 14 self.__que = que 15 16 def run(self): 17 while True: 18 global count 19 l.acquire() 20 count += 1 21 l.release() 22 self.__que.put(count) 23 print('%s produce baozi %s' % (self.__name, count)) 24 time.sleep(0.5) 25 self.__que.join() 26 27 #创建消费者 28 class Consumer(Thread): 29 def __init__(self, name, que): 30 super(Consumer, self).__init__() 31 self.__name = name 32 self.__que = que 33 34 def run(self): 35 while True: 36 data = self.__que.get() 37 print('%s eat baozi %s' % (self.__name, data)) 38 time.sleep(1) 39 self.__que.task_done() 40 41 def main(): 42 #创建1个生产者,3个消费者 43 p1 = Producer('winter', q) 44 c1 = Consumer('elly', q) 45 c2 = Consumer('jack', q) 46 c3 = Consumer('frank', q) 47 p1.start() 48 c1.start() 49 c2.start() 50 c3.start() 51 52 if __name__ == '__main__': 53 main()
生产者消费者模型设计要合理,如果生产者慢了,可以增加生产者,消费者慢了,增加消费者;
实际应用中,生产者,消费者可能是两套不同的系统,不会存在于一个进程里,甚至不在同一台设备上;而queue.Queue只能用于线程间通讯,那么该怎么办呢?
采用消息队列,比如rabbitMQ;
最后,上传一篇将进程线程做了很好的类比的一篇文章
1. 计算机的核心是CPU,它承担了所有的计算任务。它就像一座工厂,时刻在运行。
2. 假定工厂的电力有限,一次只能供给一个车间使用。也就是说,一个车间开工的时候,其他车间都必须停工。背后的含义就是,单个CPU一次只能运行一个任务。
3. 进程就好比工厂的车间,它代表CPU所能处理的单个任务。任一时刻,CPU总是运行一个进程,其他进程处于非运行状态。
4. 一个车间里,可以有很多工人。他们协同完成一个任务。
5. 线程就好比车间里的工人。一个进程可以包括多个线程。
6. 车间的空间是工人们共享的,比如许多房间是每个工人都可以进出的。这象征一个进程的内存空间是共享的,每个线程都可以使用这些共享内存。
7.可是,每间房间的大小不同,有些房间最多只能容纳一个人,比如厕所。里面有人的时候,其他人就不能进去了。这代表一个线程使用某些共享内存时,其他线程必须等它结束,才能使用这一块内存。
8. 一个防止他人进入的简单方法,就是门口加一把锁。先到的人锁上门,后到的人看到上锁,就在门口排队,等锁打开再进去。这就叫“互斥锁”(Mutual exclusion,缩写 Mutex),防止多个线程同时读写某一块内存区域。
9. 还有些房间,可以同时容纳n个人,比如厨房。也就是说,如果人数大于n,多出来的人只能在外面等着。这好比某些内存区域,只能供给固定数目的线程使用。这时的解决方法,就是在门口挂n把钥匙。进去的人就取一把钥匙,出来时再把钥匙挂回原处。后到的人发现钥匙架空了,就知道必须在门口排队等着了。这种做法叫做“信号量”(Semaphore),用来保证多个线程不会互相冲突。
不难看出,mutex是semaphore的一种特殊情况(n=1时)。也就是说,完全可以用后者替代前者。但是,因为mutex较为简单,且效率高,所以在必须保证资源独占的情况下,还是采用这种设计。
10.操作系统的设计,因此可以归结为三点:
(1)以多进程形式,允许多个任务同时运行;
(2)以多线程形式,允许单个任务分成不同的部分运行;
(3)提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源。