多任务:指的是一台电脑可以同时运行多个应用程序(一个应用程序可能有多个进程),是一种共享CPU的方法。
协同式多任务(cooperative multitasking):进程在执行过程中不受限制的占用cpu,不存在时间片概念,系统对cpu使用权的收回要靠进程主动上交。
抢先式多任务(preemptive multitasking):当一个新的进程开始时,在它的时间片(timeslice)之内,cpu的使用权是在这个进程手里的,当时间片结束时,系统要收回cpu的使用权做下一轮分配,即由系统进行cup使用权的分配与应用程序无关。
单核:CPU集成了一个运算核心,所有的任务都是由这个运算核心完成的,同一时间只能完成一个任务。
多核:CPU集成了多个运算核心,可以同一时间让多个运算核心同时工作完成多个任务。
并发:任务数多于CPU核心数,通过操作系统调度(Scheduling)方法,在很短时间内不断切换(时间片轮转,并有优先级算法等以任务的重要度优先考虑),实现在固定时间内多个任务共同执行。
并行:任务数小于CPU核心数,多个任务执行于不同的运算核心,真正的同时执行。
程序(program):是一种静态的实体,如封装的EXE文件,.py文件等。
进程(process):可以认为是运行的程序,是一个动态的实体,代表程序的执行过程,随着程序中指令的执行而不断的变化,在某个特定时刻的进程的内容被称为进程映像(process image),分为①正文段(text,被执行的机器指令,即代码)②用户数据段(user segment,存放进程在执行时直接操作的所有数据,包括进程使用的全部变量在内)③系统数据段(system segment,存放程序运行的环境,即进程的控制信息,是进程与程序的区别所在);进程有三种状态,就绪态、执行态、等待态(堵塞态),是多任务的一种实现方式。
线程(thread):线程(有时候称为轻量级进程)与进程类似,不过它们是在同一个进程下执行的,并共享相同的上下文,即将多任务的思想拓展到应用层面,将单个任务分解为小任务,再将这些任务分配给不同的CPU内核,提高进程速度;以python程序为例,其主代码顺序执行即主线程,在主线程过程中有一些函数、类等也需要新的线程去执行,称之为子线程。是多任务的一种轻量级的实现方式。
python中的threading模块
python内置了threading模块(其前身为thread模块,但相对底层,在python3中更名为_thread),可用于多线程的执行,threading模块的类对象如下:
Thread 表示一个执行线程的对象
Lock 锁对象
RLock 可重入锁对象,使单一线程可以(再次)获得已持有的锁(递归锁)
Condition 条件变量对象,使得一个线程等待另外一个线程满足特定的条件,比如改变状态或者某个数据值
Event 条件变量的通用版本,任意数量的线程等待某个事件的发生,在该事件发生后所有的线程都将被激活
Semaphore 为线程间的有限资源提供一个计数器,如果没有可用资源时会被阻塞
BoundedSemaphore 与Semaphore相似,不过它不允许超过初始值
Timer 与Thread类似,不过它要在运行前等待一定时间
Barrier 创建一个障碍,必须达到指定数量的线程后才可以继续
threading.enumerate() 返回一个列表,列表中是当前的线程,其中主线程显示为MainThread
threading.current_thread() 返回当前线程,线程名在定义线程时指定,若不指定系统会默认写为Thread-1等,除了打印时用于显示没有别的用途
其中Thread类是线程的主要执行对象,其他类都属于同步机制。
# Thread类属性
name 线程名
ident 线程的标识符
daemon 布尔值,表示这个线程是否是守护线程
# Thread类的主要方法
__init__(group=None,target=None,name=None,args=(),kwargs={},verbose=None,daemon=None) 实例化一个线程对象,需要一个可调用的target对象,以及参数args或者kwargs。还可以传递name和group参数。daemon的值将会设定thread.daemon的属性
start() 开始执行该线程
run() 定义线程的方法(一般开发者在子类中重写此方法)
join(timeout=None) 直至启动的线程终止之前一直挂起;除非给出了timeout(单位秒),否则一直被阻塞
在python中使用多线程的常用方法有两种:
(1)创建Thread类实例,传递函数作为线程执行的内容。
def loop():
n = 0
while n < 5:
n = n + 1
print(threading.current_thread().name)
time.sleep(1)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('end')
如上所示,直接生成Thread实例,调用start()方法即开始执行线程,join()方法意为等待子线程执行完毕后再执行主线程,否则'end'
会在loop函数调用前就打印出来。
(2)派生Thread的子类,并创建子类的实例。
class MyThread(threading.Thread):
def __init__(self,func,args,name=''):
threading.Thread.__init__(self)
self.func = func
self.name = name
self.args = args
def run(self):
print('开始执行',self.name,' 在:',ctime())
self.res = self.func(*self.args)
print(self.name,'结束于:',ctime())
def getResult(self):
return self.res
如上所示,自定义Thread的派生类,在其中定义run方法(一般在run方法中会直接调用start()/join()方法),其实只是上述方法的封装,但更适合面向对象的开发。
注:①主线程是可能在子线程之前结束的,但其并不会直接将子线程结束掉,join()只有在主线程需要等待子线程完成时候才是有用的(主线程与子线程是相互独立的,其都同属于一个进程,在默认情况下,进程会等待所有的子线程结束后再结束);
②只有调用实例的start方法时,才会创建并开始线程的执行;
③假如在一个函数中使用线程并使用变量传入参数,若子线程函数需要等待,而主线程很快的执行完毕,由于主线程执行完毕后与它关联的所有数据都会被回收,则子线程的参数就不存在了(正常情况下子线程会自动关闭,但特殊情况下其会报错)。
python多线程的同步机制
多线程与全局变量:在一个进程的多线程任务中,多个线程共享全局变量(创建Thread类实例时传入的变量就是全局变量,或处理磁盘上的文件等),即在一个线程中声明或更改了全局变量,按时间顺序另一个线程会获得更改后的全局变量。注意,在共享全局变量时,多个线程不断调用全局变量并且不断改变,可能会产生全局变量在不同进程中同时改变,最终得到的运算结果与预期值不同(例如传入的参数是一个可变对象,则每个线程对参数的改变都会影响到其他线程)。
线程同步:为了解决上述问题,需要同步(协同步调),如按照约定好的次序执行,A在执行到一定程度时依靠B的结果,反之相同,当多个线程几乎同时修改一个共享数据时,需要同步控制,线程同步能保证多个线程安全访问共享资源,threading模块拥有多个同步机制,如下。
互斥锁机制 Lock类
互斥锁为共享资源引入一个状态,当某个线程要修改共享资源时先将其’锁定’,锁定状态下其他线程无法更改(但可以读取)该共享资源,其保证了每次只有一个线程进行写入操作,保证了对共享资源进行修改的原子性(),保证了多线程时数据的正确性。
Lock拥有两种状态:locked and unlocked,并通过acquire()
and release()
来改变状态。有如下规则:
如果当前状态是unlocked状态,调用acquire()方法改变状态为locked。
如果当前状态是locked状态,调用acquire()方法将会阻塞(blocked, 同步阻塞)直到另一个线程调用release()方法。
如果当前状态是unlocked状态,调用release()方法将会造成RuntiemError 异常。
如果当前状态是locked状态,调用release()方法改变状态为unlocked。
mutex = threading.Lock() # 创建锁,默认状态为unlocked
mutex.acquire() # 上锁,返回值为True
mutex.release() # 解锁,无返回值
with mutex: # 锁的另一种用法,其将上锁与解锁封装进了上下文管理器中
dosomething()
注:①上锁的代码越少越好。锁是可以有多个的,当不同的线程持有不同的锁并同时试图获取对方的锁时,可能出现死锁;
②锁的优势是确保了某段关键的代码可以从头到尾的完整执行,但也阻止了多线程的并发执行,在某段加锁的代码实际上只能单线程执行;
③死锁的解决办法:Ⅰ添加超时时间,即在join函数中限定阻塞时间;Ⅱ银行家算法:银行家手中的资金并不足以同时满足多位客户的要求,因此它从当前状态出发,逐个按安全序列检查各客户谁能完成工作,然后假定其完成工作且归还全部贷款,再进而检查下一个能完成工作的客户;
④RLock模块与Lock模块的功能大部分相同,其区别是:RLock模块的lock允许同一线程内对同一lock进行多次获取,但注意必须使获取与释放成对出现。
信号量机制 Semaphore类
①信号量基于内部计数器counter,每次acquire()被调用时counter减1,每次release()被调用计数器加1。如果counter==0,再去调用acquire()将阻塞,其常用于限制对资源的访问;
②Semaphore模块的用法与同步阻塞状态与Lock类似,同样使用acquire()与release()函数,同样可使用with语句。
sem = threading.Semaphore(value = 3)
条件判断机制 Condition类
一个线程在等待特定的条件而另一个线程表明这个特定条件已经发生,只要条件发生,线程就需要获得lock然后独立的使用共享资源,条件判断机制最常用的就是生产者-消费者模式,消费者等待生产者来表明特定条件,其中不仅有acquire(),release()方法,还有用于条件判断的wait(),notify(),notifyAll()方法,举例如下:
class Goods:#产品类
def __init__(self):
self.count = 0
def add(self,num = 1):
self.count += num
def sub(self):
if self.count>=0:
self.count -= 1
def empty(self):
return self.count <= 0
class Producer(threading.Thread):#生产者类
def __init__(self,condition,goods,sleeptime = 1):#sleeptime=1
threading.Thread.__init__(self)
self.cond = condition
self.goods = goods
self.sleeptime = sleeptime
def run(self):
cond = self.cond
goods = self.goods
while True:
cond.acquire()#锁住资源
goods.add()
print("产品数量:",goods.count,"生产者线程")
cond.notifyAll()#唤醒所有等待的线程--》其实就是唤醒消费者进程
cond.release()#解锁资源
time.sleep(self.sleeptime)
class Consumer(threading.Thread):#消费者类
def __init__(self,condition,goods,sleeptime = 2):#sleeptime=2
threading.Thread.__init__(self)
self.cond = condition
self.goods = goods
self.sleeptime = sleeptime
def run(self):
cond = self.cond
goods = self.goods
while True:
time.sleep(self.sleeptime)
cond.acquire()#锁住资源
while goods.empty():#如无产品则让线程等待
cond.wait()
goods.sub()
print("产品数量:",goods.count,"消费者线程")
cond.release()#解锁资源
g = Goods()
c = threading.Condition()
pro = Producer(c,g)
pro.start()
con = Consumer(c,g)
con.start()
同步队列机制
同步队列机制依据队列的特性来实现,其使用的不是threading模块中的类,而是queue中的队列类Queue,Queue将锁的机制封装,其也适用于生产者-消费者机制,即类似于消息队列,当队列中无任务时消费者阻塞,当队列满时生产者阻塞,可以对队列添加一个join()函数使得当队列为空(所有任务都被处理完)时主进程退出。
事件通知机制 Event类
一个线程发出一个event的信号并且其他的线程等待它。Event类实例的方法有set()设置事件,clear()清除事件并通知消费者,wait()保持阻塞状态直至set()被调用。
条件变量与互斥锁、信号量的区别
①互斥锁必须总是由给它上锁的线程解锁,信号量的挂出即不必由执行过它的等待操作的同一进程执行,一个线程可以等待某个给定信号灯,而另一个线程可以挂出该信号灯;
②互斥锁要么锁住,要么被解开(二值状态,类型二值信号量);
③由于信号量有一个与之关联的状态(它的计数值),信号量挂出操作总是被记住。然而当向一个条件变量发送信号时,如果没有线程等待在该条件变量上,那么该信号将丢失;
④互斥锁是为了上锁而设计的,条件变量是为了等待而设计的,信号灯既可用于上锁,也可用于等待,因而可能导致更多的开销和更高的复杂性。
python中的守护线程与join方法
①守护线程要守护的对象是主线程,若某个线程是守护线程,则当主线程结束时该线程随之结束,t.setDaemon(True)
;
②join()方法阻塞的是除自己以外的所有线程(包括主线程与守护线程),因此join()方法一般在所有线程都开启之后统一调用,否则子线程之间会相互阻塞,变成顺序执行。
独立全局变量
threading模块中的l = threading.local()
创建一个全局对象,其作用为在每个线程中都可以将l
作为一个实例进行实例属性的赋值l.name = 'bob'
,在进程中维护每一个线程的副本,且每个属性都是其线程的局部变量(属性名可以重复,线程间互不干扰),相当于一个全局的以线程id为key的嵌套字典。
依存关系
①进程与线程都是由操作系统所提供的程序运行的基本单元,系统利用其实现对应用的并发性;
②线程不能独立运行,其必须依赖于进程才能运行,一个进程中可以包含多个线程,但必须至少有一个线程;
③进程与进程之间没有关系,完全独立;
④同属一个进程的多个线程之间处于相同的级别(不论创建关系如何),进程内的任何线程都可以销毁、挂起、恢复和更改其它线程(包括主线程)的优先权,销毁主线程将导致该进程的销毁,对主线程的修改可能影响所有的线程,因此线程是可以影响其所在的进程的(但一般不要这么做,应等待线程自行停止)。
占据资源
①如前所述,一个进程中必然有三部分资源:
Ⅰ正文段(text,被执行的机器指令,即代码)Ⅱ用户数据段(user segment,存放进程在执行时直接操作的所有数据,包括进程使用的全部变量在内)Ⅲ系统数据段(system segment,存放程序运行的环境,即进程的控制信息/上下文,是进程与程序的区别所在),这部分资源是每一个进程都独立拥有的,进程在开启时就会在内存中开辟一部分空间用以保存进程上下文,各个进程之间互不干扰,都拥有自己的正文段和数据段;
②线程拥有的所有资源都来自于进程,同一个进程的多个线程之间共享进程的资源,即代码段和数据段,具体可划分如下:
Ⅰ堆:是进程与线程共有的空间,分全局堆和局部堆:全局堆就是所有没有分配的空间,局部堆就是用户分配的空间。堆在操作系统对进程初始化的时候分配,运行过程中也可以向系统要额外的堆,但是记得用完了要还给操作系统,要不然就是内存泄漏。
Ⅱ栈:是每个线程独有的,用以保存其运行状态和局部自动变量,栈在线程开始的时候初始化,每个线程的栈互相独立,操作系统在切换线程的时候会自动的切换栈,在高级语言中无须开发者操作切换栈,由系统完成;
Ⅲ同进程下的多线程共享的内容有:进程代码段、进程的公有数据(即全局变量,线程可以利用公有数据很容易的实现相互之间的通讯)、进程打开的文件描述符、信号的处理器、进程的当前目录和进程用户ID与进程组ID等;
Ⅳ多线程并发运行时独有的内容有:唯一的线程ID、寄存器组的值(用以保存线程的运行上下文的容器的位置)、堆栈(线程独有的堆栈用以保存局部变量)、错误返回码、信号屏蔽码、线程优先级等;
③总结来说,线程几乎不额外占用系统资源,其所有的资源都来自于所在的进程,且多个线程之间共享其进程资源,而新建一个进程则需要很大的开销。
开销与切换与根本差异
①操作系统新建一个进程的开销是很大的,CPU其对于一个进程/线程的调用分为三部分:加载上下文→CPU执行代码内容→保存上下文,进程的上下文很大内容很多,因此CPU切换会更加消耗时间、效率低,而线程的上下文较少,且多线程共享进程的上下文,因此在同进程的线程间切换无需重新加载全局上下文,速度快效率高,即进程的创建和切换都很昂贵,而线程的创建和切换相比进程的开销要小很多;
②进程是系统中能独立运行的并作为资源分配的最小单位,线程的划分尺度小于进程,是CPU调度和分派的最小单位,这也是最重要最根本的区别。
通信与安全
①线程间的同步控制参上述(python中的多线程),其拥有互斥锁、信号量、条件判断、信号通知、队列等机制,进程间通信IPC(Interprocess communication)有管道、命名管道、信号量、消息队列、共享内存等机制(后详),进程间同步(后详);
②相比较来说,线程间通信更加的方便(共享全局变量),进程间通信相对复杂,但线程的同步比进程的同步更难操作;
③由于进程相互独立,因此多进程程序更加的安全/健壮,单个进程的死亡不会影响其他进程,而单进程多线程程序相对不安全,单个线程的死亡可能会对整个程序产生很大的影响。
注:
文件描述符:是内核为了高效管理已被打开的文件所创建的索引,用于指向被打开的文件,所有执行I/O操作的系统调用都通过文件描述符;文件描述符是一个简单的非负整数,用以表明每个被进程打开的文件,也可以理解为当前占用文件的身份ID。
每个进程所能占用的文件描述符是有最大限制的(其依赖于系统和内存),所有已打开的文件描述符会组成一个有序列表,其中系统默认占用0,1,2分别为标准输入、标准输出、标准错误,当文件关闭后、后续文件打开会占用之前的文件描述符而非往后顺延。
python中可以使用打开文件对象(即字节流)的fileno()
方法查看当前文件的文件描述符,sys.stdin.fileno()
返回0。
同步控制与通信
同步与通信是不同的两个概念,同步指的是对竞争资源的访问的一种处理方式,避免一个线程/进程长期占用一个资源的目的,而通信指的是不同进程/线程之间传播或交换信息,进程与线程都有各自的同步机制和通信方式。
①线程间通信一般采用全局变量,也有消息、事件等方式,而进程间通信即IPC,有多种方式(后详);
②线程的同步机制参上述,进程的同步机制主要是处理多进程同时使用一个文件、资源的情况,其实也相当于一种以操作系统为全局的全局变量;
③同步其实也是一种通信方式。
进程的创建
①但凡是硬件,都需要有操作系统去管理,只要有操作系统,就有进程的概念,就需要有创建进程的方式,有一些操作系统只为了一个应用程序设计,比如微波炉中的控制器,一旦启动微波炉,所有的进程都已经存在,归属于嵌入式,即对硬件、软件的运行方式都有明确的规定;
②平时常用的Win/Linux等系统都属于通用系统,需要跑很多进程且事先并不确定进程的具体内容,这就需要有系统运行过程中创建或者撤销进程的能力,创建新进程主要有四种情况:
Ⅰ系统初始化(对于操作系统来说其分为前台进程与后台进程,前台进程负责与用户交互,后台进程用于操作硬件、维持运转等);
Ⅱ进程开启子进程;
Ⅲ用户的交互式请求创建子进程(其实也属于进程开启子进程);
Ⅳ一个批处理作业的初始化(只在大型机的批处理系统中应用);
但无论哪种,新进程的创建都是由一个已经存在的进程执行了一个用于创建进程的系统调用而创建的;
③在unix平台下,可以通过系统的fork调用来创建子进程,python中的表示为os.fork()
,这个函数调用时就创建了一个与父进程一模一样的副本,二者拥有相同的存储映像、同样的环境字符串和同样的打开文件,将这个进程称为子进程;fork函数调用后,在父进程和子进程中返回的值是不同的,在父进程中返回子进程的pid,在子进程中返回0,可以通过os.getpid()
来获取当前进程的进程号,os.getppid()
获取父进程的进程号;
④在windows平台下,系统调用的是CreateProcess,其既处理进程的创建,也负责把正确的程序装入新进程;
⑤在UNIX中,子进程的初始地址空间是父进程的一个副本(子进程和父进程是可以有只读的共享内存区的),但是在windows中,从一开始父进程和子进程的地址空间就是不同的。
注:①进程的三种状态(前面已提到):Ⅰ运行态:应用程序正在被CPU执行中;Ⅱ阻塞态:当前进程突然要做I/O操作,然后CPU去执行其他的程序;Ⅲ就绪态:时刻准备着能够被执行。
②写时拷贝(copy-on-write):子进程在开启时会复制复制主进程的所有地址空间、环境变量、文件描述符(file descriptors)到子进程,即子进程可以直接使用主进程的变量,包括 import 过的模块,且这种复制是写时拷贝(copy on write),即只有子进程内容要发生变化时,才将主进程的内容复制一份给子进程并进行改动,否则是共享的。
python中的multiprocessing模块
与threading模块类似的,其中除了Process类用于进程实体的创建外,其余类都是用于进程间通信的,分属不同的同步机制。
进程的一些用法:
Process([group [, target [, name [, args [, kwargs]]]]])
target:如果传递了函数的引用,可以任务这个子进程就执行这里的代码
args:给target指定的函数传递的参数,以元组的方式传递
kwargs:给target指定的函数传递命名参数
name:给进程设定一个名字,可以不设定
group:指定进程组,大多数情况下用不到
Process创建的实例对象的常用方法:
start():启动子进程实例(创建子进程)
is_alive():判断进程子进程是否还在活着
join([timeout]):是否等待子进程执行结束,或等待多少秒
terminate():不管任务是否完成,终止子进程,但不是立即关闭,有一个等待操作系统去关闭这个进程的时间
Process创建的实例对象的常用属性:
name:当前进程的别名,默认为Process-N,N为从1开始递增的整数
pid:当前进程的pid(进程号),使用os.getpid()可获取当前进程进程号,使用os.getppid()可获取当前进程父进程进程号
daemon:表示是否为守护进程,默认为False,若设置为True,则表示为主进程的守护进程,必须在p.start ()之前进行设置
注:①在windows系统下,由于没有fork函数,其在创建进程的时候自动import启动它的这个文件,而在import的时候又执行了整个文件,如果将创建子进程的过程直接写在文件中就会无限递归创建子进程报错,因此必须把创建子进程的部分写在 if __name__ == '__main__'
条件下;
②关于守护进程,守护进程内无法再开启子进程,并随主进程的关闭而关闭;
③进程中的join()
方法与线程中join()
效果不同,进程中的join()方阻塞主进程,令主进程等待该子进程结束后再执行,但线程中的join()方法阻塞所有线程,只要该线程没有结束,其他所有线程都等待;
④Process实例可以控制进程的创建、结束等,但其并不是进程本身;
⑤进程中的run()
方法与线程中是类似的,当实例创建时若target不存在,则实例的start方法调用的就是类中的run方法;
⑥多个子进程执行的顺序不是根据启动顺序决定的;
⑦直接调用terminate()
方法关闭进程会产生僵尸进程,start()
与join()
方法都会对僵尸进程进行处理(所有进程的此两种方法都可以),也可以使用os.wait()
方法对子进程进行回收,其返回一个包含pid的元组;
⑧僵尸进程:就是在主进程开启了一个子进程后,无论什么时候都可以去查看子进程的状态,即使子进程死掉了,也要为主进程保留子进程状态信息,僵尸进程是有害的,因为一个进程死掉后,它的PID不会立马消除,如果僵尸进程多了,PID还被占用着,操作系统再开启新的进程的话可能无法开启;
⑨孤儿进程:就是子进程还没有执行完,主进程就已经死掉了,但是子进程是无害的,此时子进程的PID由init进程去回收;
⑩fork函数兼容性/扩展性都很差,且容易产生僵尸进程和孤儿进程,需要手动回收资源,但是系统自带的接近低层的创建方式,运行效率高;使用python中multiprocessing模块创建子进程,属于高级方式,效率相对低,但功能完善,更安全。
python中多进程的同步机制
与多线程类似的,用于处理多个进程同时修改一块数据(文件、数据库中表等)时可能出现的冲突和错误。
互斥锁 multiprocessing.Lock
信号量 multiprocessing.Semaphore
事件 multiprocessing.Event
条件 multiprocessing.Condition
上述四个类在多进程中同样存在,在python中,其实multiprocessing的很大一部份与threading使用同一套API,只不过换到了多进程的情境,其原理类似,用法也几乎相同,但注意多进程中每个进程都有自己独立的全局变量,因此在进行多进程编程时,若要模拟同时修改某数据,可以使用文件或数据库。
注:①有必要对每个Process对象调用join()方法,以避免其结束后成为僵尸进程(占用pid);
②应尽量避免使用上述接口,在multiprocessing模块中提供了更优秀的IPC接口(Queue和Pipe),使用IPC的方式比上述接口效率更高,应该尽量避免处理复杂的同步和锁问题,这样也在有更多任务时方便扩展;
③使多进程共享资源很容易出问题,既麻烦效率又低,应尽可能的使用IPC的方式实现同步,注意IPC其实只是信息的交互,并不能实现资源共享,即一个进程去修改另一个进程的数据。
python中多进程的进程间通信(Interprocess communication)方式
队列 multiprocessing.Queue
队列本身的机制与其作为数据结构时相同,其额外开辟一块内存用于保存队列中的数据,因此其对于多进程来说相当于是全局的,且应该存储尽量少的数据,其可以完成进程间的数据共享,可以用于解耦(即使各个进程之间的依存度降低),以下为简单用法:
q = Queue(maxsize) maxsize是队列中允许最大项数,省略则无大小限制
q.put (item, blocked=True, time) : 将item放入队列中, 如果当前队列已满, 就会阻塞, 直到有数据从管道中取出,若blocked为False,则不等待直接抛出Queue.Full异常
q.put_nowait (item) : 将item放入队列中, 如果当前队列已满, 不会阻塞, 但是会报错
q.get (blocked=True, time) : 返回放入队列中的一项数据, 由于队列的特点是先进先出(First In First Out, 简称FIFO), 取出的数据将是先放进去的数据, 若当前队列为空, 就会阻塞, 直到放入数据进来
q.get_nowait () : 返回放入队列中的一项数据, 同样是取先放进队列中的数据, 若当前队列为空, 不会阻塞, 但是会报错
q.empty () : 返回队列是否为空的bool值, 为空即为True,不为空即为False, 如果其他进程或线程正在往队列中添加数据, 结果是不可靠的, 即在返回和使用之间, 队列中可能已经放入了新的数据
q.size () : 返回队列中目前数据的正确数量, 同q.empty(), 并不可靠
q.full () : 返回队列是否已满的bool值, 同q.empty(), 并不可靠
高级队列 JoinableQueue
JoinableQueue(maxsize)的实例q除了与Queue对象相同的方法之外,还有:
q.task_done():消费者使用此方法发出信号,表示q.get()的返回项目已经被处理。如果调用此方法的次数大于从队列中删除项目的数量,将引发异常
q.join():生产者使用此方法发出信号,直到队列中所有的项目都被处理。阻塞将持续到队列中的每个项目均调用q.task_done()方法为止
即join()方法是生产者调用的,只需要调用一次,就为其生产的所有内容添加了标记(若有循环,则阻塞到所有标记都被消费者处理后才会继续生产),而task_done()方法是消费者调用的,消费的每一个内容都需要调用一次。
注:①在使用队列时,需要将队列对象作为参数传入子进程调用的函数/方法中,解释器会自动将其作为进程以上的全局类型;
②queue模块中的队列类是线程安全的,multiprocessing模块中的队列类是进程安全的,注意不能混淆。
管道 multiprocessing.Pipe
①管道是一种可选单/双向通信的半双工通信方式,队列其实就是管道和锁的高级封装,且克服了一些问题,因此一般使用队列,管道的常用方法如下:
p,q = multiprocessing.Pipe(duplex=True)
参数默认为True,双向管道,若为False则为单向管道,其返回两个对象分别为管道的两端,都可以使用send()
方法发送字符串,recv()
方法接收字符串,close()
方法关闭管道;
②管道的两端都可以发送或接收消息,但由于其是半双工通信,因此管道中最多只会保存一个数据,只有等一端发送的数据被另一端接收后才可以继续发送数据,否则会阻塞在发送端,为了避免混淆,一般对两个需要通信的进程每个传入一端的管道,足以完成通信;
③注意,管道和队列虽然实现了进程间消息传递,但其实它们使用的Queue()与Pipe()对象并不是同一个(内存地址不同),解释器在底层完成了互相之间的通信工作,并实现了同步。
各种进程间通信机制:
管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
python中的共享存储参下文。
进程池 multiprocessing.Pool
在实际处理问题的过程中, 忙时会有成千上万的任务需要被执行, 我们不可能创建那么多进程去完成任务。首先创建进程需要时间, 销毁进程同样需要时间. 即便是真的创建好了这么多进程, 操作系统也不允许他们同时执行的,这样反而影响了程序的效率.
进程池即定义一个池子, 在里面放上固定数量的进程 , 有任务要处理的时候就会拿一个池中的进程来处理任务, 等到处理完毕, 进程并不关闭而是放回进程池中继续等待任务. 如果需要有很多任务需要执行, 池中的进程数不够, 任务会就要等待进程执行完任务回到进程池, 拿到空闲的进程才能继续执行.池中的进程数量是固定的,那么同一时间最多有固定数量的进程(默认是CPU的核数)在运行. 这样不会增加操作系统的调度难度, 还节省了开闭进程的时间, 也一定程度上能够实现并发效果。
其常用方法如下:
Pool ( [numprocess [ ,initializer [, initargs] ] ] ) : 创建进程池
numprocess : 要创建的进程数, 如果省略, 将默认使用os.cpu_count () 的值
initializer : 是每个工作进程启动时要执行的可调用对象, 默认为None
initargs : 是要传给initializer的参数组
主要方法
p.map(f, args) 与python中的map方法类似
p.map_async() 与map类似,不过是异步变体
p.apply (func [ ,args [ ,kwargs] ] ) : 在一个池工作进程中执行func(*args,**kwargs), 然后返回结果,这个结果就是func函数的返回值
注意: 此操作并不会在所有池工作进程中并发执行func函数, 如果要通过不同参数并发地执行func函数, 必须从不同线程调用p.apply()函数或者使用p.apply_async()
p.apply_async(func [ ,args [ ,kwargs] ] ) : 在一个池工作进程中执行func(*args,**kwargs), 然后返回结果
注意: 此方法的结果是AsyncResult类的实例, callback是可调用对象, 接收输入参数. 当func的结果变为可用时, 将直接传递给callback. callback禁止执行任何阻塞操作, 否则将接收其他异步操作中的结果
p.close() : 关闭进程池,此后进程池不再接收进程执行任务
p.join() : 等待所有工作进程退出. 此方法只能在close () 或terminate () 之后调用
其他方法
方法apply_async () 和map_async () 的返回值是AsyncResult的实例是obj. 实例具有以下方法:
obj.get () : 返回结果, 如果有必要则等待结果到达. timeout是可选的. 如果在制定时间内还没有到达, 将引发异常, 如果远程操作中引发了异常, 它将在调用此方法时再次被引发
obj.ready () : 如果调度完成, 返回True
obj.successful () : 如果调用完成也没有引发异常, 返回True, 如果在结果就绪之前调用此方法, 引发异常
obj.wait (timeout) :等待结果变为可用
obj.terminate () : 立即终止所有工作进程, 同时不执行任何清理或结束任何挂起工作, 如果p被垃圾回收, 将自动调用此函数
注:①apply/map方法在同一时间只允许一个进程进入pool,在该进程处理结束后,才可以有别的进程进入进程池获取任务进行处理;
②apply_async/map_async方法允许多个进程同时进入pool并同步获取任务进行处理,其返回值可以通过回调函数处理也可以直接调用get()方法阻塞等待;
③在异步处理任务时,一般添加p.close()与p.join(),因为异步处理任务时所有的子进程都是守护进程,但其实由于obj.get()方法是阻塞的,所以主进程仍然会阻塞直到所有的子进程任务执行完毕并执行完get方法后才关闭;
④一般在任务数不确定的情况下使用进程池,进程池中的进程其任务代码都是相同的,因此才可以共享资源不开启新的进程。
python中的多进程数据共享
multiprocessing模块中提供了Value(整型与字符串),Array(元素类型相同的数组)等对象可用于共享内存,Manager模块封装了这些类,提供了一个更高级的对象,Manager支持的类型有list,dict,Namespace,Lock,RLock,Semaphore,BoundedSemaphore,Condition,Event,Queue,Value和Array。
使用mgr = multiprocessing.Manager()
创建Manager()对象,其下可以使用mgr.dict()
创建共享字典,mgr.list()
创建列表等,在子进程中对这些对象进行改变后,
注:Manager对象类似于服务器与客户之间的通信 (server-client),与我们在Internet上的活动很类似。我们用一个进程作为服务器,建立Manager来真正存放资源。其它的进程可以通过参数传递或者根据地址来访问Manager,建立连接后,操作服务器上的资源。在防火墙允许的情况下,我们完全可以将Manager运用于多计算机,从而模仿了一个真实的网络情境。
python中的分布式进程
上述Manager类中提供了很多可用于共享的类型,其实这些用于共享的类型就是通过服务器-客户端之间的通信实现的,将其注册到网络上,就可以直接通过网络连接实现分布式共享。
multiprocessing模块中提供了managers模块,其中提供了BaseManager类,该类中封装了注册等方法用以实现多机器的分布式进程。
但实际操作中使用RabbitMQ/redis等作为消息队列实现分布式部署的情况更多,进程间甚至是机器间的数据同步基本是以传递信息的方式实现的,真正的安全的共享数据会极大降低工作效率。
计算密集型和IO密集型任务
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,因此其同时进行的任务数量应等于CPU核数,可使效率最大化。
涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度),对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度(常见的大部分任务都是IO密集型任务,比如Web应用)。
因此对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言。
总结:本文简单介绍了python中的多线程与多进程及其一些使用方法和原理,同步与通信机制等,并着重分析了线程与进程的异同。