【Python之路】第九篇--Python基础之线程、进程和协程
进程与线程的历史
进程就是一个程序在一个数据集上的一次动态执行过程。 进程一般由程序、数据集、进程控制块三部分组成。
我们编写的程序用来描述进程要完成哪些功能以及如何完成;
数据集则是程序加工处理的原始数据,也可以是程序执行时产生的中间或最终结果。
进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
可以说,进程就是包括上下文切换的程序执行时间总和 = CPU加载上下文+CPU执行+CPU保存上下文。
在早期的操作系统里,计算机只有一个核心,进程执行程序的最小单位,任务调度采用时间片轮转的抢占式方式进行进程调度。
每个进程都有各自的一块独立的内存,保证进程彼此间的内存地址空间的隔离。
随着计算机技术的发展,进程出现了很多弊端:
一、进程的创建、撤销和切换的开销比较大,
二、由于对称多处理机(对称多处理机(SymmetricalMulti-Processing)又叫SMP,是指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构)的出现,可以满足多个运行单位,而多进程并行开销过大。
也就是说:进程的颗粒度太大,每次都要有上下的调入,保存,调出。
如果我们把进程比喻为一个运行在电脑上的软件,那么一个软件的执行不可能是一条逻辑执行的,必定有多个分支和多个程序段,就好比要实现程序A,实际分成 a,b,c等多个块组合而成。
程序A得到CPU =》CPU加载上下文,开始执行程序A的a小段,然后执行A的b小段,然后再执行A的c小段,最后CPU保存A的上下文。
这里的a,b,c的执行是共享了A的上下文,CPU在执行的时候没有进行上下文切换的。这里的a,b,c就是线程,也就是说线程是共享了进程的上下文环境的更为细小的CPU时间段。
这个时候就引入了线程的概念。
线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。
线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。
线程没有自己的系统资源,只拥有在运行时必不可少的资源。
但线程可以与同属与同一进程的其他线程共享进程所拥有的其他资源。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务
进程与线程之间的关系
线程是进程的一部分,一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
线程是属于进程的,线程运行在进程空间内,同一进程所产生的线程共享同一内存空间,当进程退出时该进程所产生的线程都会被强制退出并清除。
线程可与属于同一进程的其它线程共享进程所拥有的全部资源,但是其本身基本上不拥有系统资源,只拥有一点在运行中必不可少的信息(如程序计数器、一组寄存器和栈)。
根本区别:进程是操作系统资源分配的基本单位,而线程是任务调度和执行的基本单位。
没有线程的进程可以看做是单线程的,如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。
程序(program)只能有一个进程,一个进程就是一个程序。
打开一个Chrome程序,会发现开了十几个进程,那就是十多个程序,操作系统给他们分配了彼此独立的内存,相互执行不受彼此约束,分配同样时间的CPU。
对于用户而言,他们是一个整体,我们通常称之为应用程序(application)。
对于计算机而言,一个进程就是一个程序,多个进程(比如一个浏览器的多个进程)对计算机而言就是多个不同的程序,它不会把它们理解为一个完整的“程序”。
进程之间的关系只有父子关系,没有主从关系,他们之间是并行独立的。但是线程之间是有主从关系的,而且他们共享的是同一个内存块(包括程序、数据和堆栈)。
父进程 fork 子进程,这个子进程会拷贝一份内存块,把程序和数据都复制过去。
子进程之后就完全独立了,父进程与子进程之间的关系,与其他进程的关系都是一样的,平等的,谁也管不着谁了,他们也只能采用进程间通信才能相互了解。
父进程over了,子进程可以照样活的好好的。
同时,进程可以由多个线程组成,这称之为多线程程序,
子线程由主线程派生,而依附于主线程。主线程一旦over,进程就over了,其他子线程更是over了。
他们的内存和数据都是同一份,没有进行隔离(既方便,也危险),不需要额外的通信函数。
一个计算机可以有多个进程,这称之为多任务,他们共享的是CPU,硬盘,打印机,显示器,但他们的内存是独立的,所以需要进程间通信,这是计算机发展的第一步。
一个进程可以有多个线程,这称之为多线程,他们除了共享进程间的共享内容之外,还共享内存,这是计算机发展的第二步,主要是为了满足并行运算时共享数据,无需额外的通信。
结论是:一个程序(program)就是一个正在执行的进程,而每个进程,可以是单线程的,也可以是多线程的。一个应用程序(application)通常由多个程序组成。
并发、并行、隔离
并发:
指一个系统具有处理多个任务的能力(cpu切换,多道技术)
是为了尽量让硬件利用率高,线程是为了在系统层面做到并发。线程上下文切换效率比进程上下文切换会高很多,这样可以提高并发效率。
并行:
指一个系统具有同时处理多个任务的能力(cpu同时处理多个任务)
并行是并发的一种情况,子集
隔离:
也是并发之后要解决的重要问题,计算机的资源一般是共享的,隔离要能保障崩溃了这些资源能够被回收,不影响其他代码的使用。
所以说一个操作系统只有线程没有进程也是可以的,只是这样的系统会经常崩溃而已,操作系统刚开始发展的时候和这种情形很像。
所以:
线程和并发有关系,进程和隔离有关系。
线程基本是为了代码并发执行引入的概念,因为要分配cpu时间片,暂停后再恢复要能够继续和没暂停一样继续执行;
进程相当于一堆线程加上线程执行过程中申请的资源,一旦挂了,这些资源都要能回收,不影响其他程序。
那为什么python在多线程中为什么不能实现真正的并行操作呢?
就是在多cpu中执行不同的线程(我们知道JAVA中多个线程可以在不同的cpu中,实现并行运行)
这就要提到python中大名鼎鼎GIL,那什么是GIL?
无论你启多少个线程,你有多少个cpu,Python在执行的时候只会的在同一时刻只允许一个线程(线程之间有竞争)拿到GIL在一个cpu上运行。
当线程遇到IO等待或到达者轮询时间的时候,cpu会做切换,把cpu的时间片让给其他线程执行,cpu切换需要消耗时间和资源,
所以计算密集型的功能(比如加减乘除)不适合多线程,因为cpu线程切换太多,IO密集型比较适合多线程。
Python GIL(Global Interpreter Lock)
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.)
上面的核心意思就是,无论你启多少个线程,你有多少个cpu,Python在执行的时候会淡定的在同一时刻只允许一个线程运行。
首先需要明确的一点是GIL
并不是Python的特性,它是在实现Python解析器(CPython)时所引入的一个概念。
就好比C++是一套语言(语法)标准,但是可以用不同的编译器来编译成可执行代码。
有名的编译器例如GCC,INTEL C++,Visual C++等。
Python也一样,同样一段代码可以通过CPython,PyPy,Psyco等不同的Python执行环境来执行。
像其中的JPython就没有GIL。
然而因为CPython是大部分环境下默认的Python执行环境。
所以在很多人的概念里CPython就是Python,也就想当然的把GIL
归结为Python语言的缺陷。
所以这里要先明确一点:GIL并不是Python的特性,Python完全可以不依赖于GIL
这篇文章透彻的剖析了GIL对python多线程的影响,强烈推荐看一下:http://www.dabeaz.com/python/UnderstandingGIL.pdf
threading模块
Threading用于提供线程相关的操作,线程是应用程序中工作的最小单元。 👉中文文档
1. 直接调用:
import threading import time def worker(num): time.sleep(1) print("Thread %d"%num) return for i in range(10): t = threading.Thread(target=worker,args=(i,),name="t.%d"%i) t.start() print('main thread stop')
上述代码创建了10个“前台”线程,然后控制器就交给了CPU,CPU根据指定算法进行调度,分片执行指令。
更多方法:
t.start() : 激活线程,
t.getName() : 获取线程的名称
t.setName() : 设置线程的名称
t.name : 获取或设置线程的名称
t.is_alive() : 判断线程是否为激活状态
t.isAlive() :判断线程是否为激活状态
t.setDaemon() 设置为后台线程(True)或前台线程(False)(默认为:False);
通过一个布尔值设置线程是否为守护线程,必须在执行start()方法之前才可以使用。
如果是守护线程,主线程执行过程中,守护线程也在进行,主线程执行完毕后,守护线程不论成功与否,均停止;
如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序才停止
t.isDaemon() : 判断是否为守护线程
t.ident :获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None。
t.join() :等待直至线程终止。这将阻塞调用线程,直到调用join()
方法的线程终止(通常或通过未处理的异常),或直到可选超时发生。
t.run() :线程被cpu调度后自动执行线程对象的run方法
2.继承式调用:
#!/usr/bin/env python # -*-coding:utf-8 -*- import threading,time class MyThread(threading.Thread): def __init__(self, num): threading.Thread.__init__(self) self.num = num def run(self): print("Threading Running ... %s"%self.num) time.sleep(1) if __name__ == '__main__': t1 = MyThread(1) t2 = MyThread(2) t1.start() t2.start()
注意: 程序退出前,会确保全部线程都执行完成 , 再退出。 , 所以默认末尾有个join()
线程锁(Lock、RLock)
一个进程下可以启动多个线程,多个线程共享父进程的内存空间,也就意味着每个线程可以访问同一份数据
由于线程之间是进行随机调度,并且每个线程可能只执行n条执行之后,当多个线程同时修改同一条数据时可能会出现脏数据,所以,出现了线程锁 - 同一时刻允许一个线程执行操作。
import threading import time globals_num = 0 lock = threading.RLock() def Func(): lock.acquire() # 获得锁 global globals_num globals_num += 1 time.sleep(1) print(globals_num) lock.release() # 释放锁 for i in range(10): t = threading.Thread(target=Func) t.start()
threading.RLock和threading.Lock 的区别
RLock允许在同一线程中被多次acquire。而Lock却不允许这种情况。
如果使用RLock,那么acquire和release必须成对出现,即调用了n次acquire,必须调用n次的release才能真正释放所占用的琐。
import threading lock = threading.Lock() #Lock对象 lock.acquire() lock.acquire() #产生了死琐。 lock.release() lock.release()
import threading rLock = threading.RLock() #RLock对象 rLock.acquire() rLock.acquire() #在同一线程内,程序不会堵塞。 rLock.release() rLock.release()
GIL VS Lock
既然Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock?
注意啦,这里的lock是用户级的lock,跟那个GIL没关系 ,具体我们通过下图来看一下就明白了。
- GIL锁:保证同一时刻只有一个线程能使用到CPU
- 互斥锁:多线程时,保证修改共享数据时有序的修改,不会产生数据修改混乱
- 首先假设只有一个进程,这个进程中有两个线程 Thread1,Thread2, 要修改共享的数据date,并且有互斥锁。
- 执行以下步骤:
(1)多线程运行,假设Thread1获得GIL可以使用cpu,这时Thread1获得 互斥锁lock,Thread1可以改date数据(但并没有开始修改数据);
(2)Thread1线程在修改date数据前发生了 I/O操作 或者 ticks计数满100((注意就是没有运行到修改data数据),这个时候 Thread1 让出了GIL,GIL锁可以被竞争);
(3)Thread1 和 Thread2 开始竞争Gil (注意:如果Thread1是因为 I/O 阻塞 让出的Gil,Thread2必定拿到Gil,如果Thread1是因为ticks计数满100让出Gil这个时候Thread1 和 Thread2 公平竞争);
(4)假设 Thread2正好获得了GIL,运行代码去修改共享数据date,由于Thread1有互斥锁lock,所以Thread2无法更改共享数据date,这时Thread2让出Gil锁,GIL锁再次发生竞争;
(5)假设Thread1又抢到GIL,由于其有互斥锁Lock所以其可以继续修改共享数据data,当Thread1修改完数据释放互斥锁lock,Thread2在获得GIL与lock后才可对data进行修改。
以上描述了互斥锁和GIL锁的 一个关系。
既然用户程序已经自己有锁了,那为什么C python还需要GIL呢?
加入GIL主要的原因是为了降低程序的开发的复杂度,比如现在的你写python不需要关心内存回收的问题,因为Python解释器帮你自动定期进行内存回收,你可以理解为python解释器里有一个独立的线程,每过一段时间它起wake up做一次全局轮询看看哪些内存数据是可以被清空的,此时你自己的程序 里的线程和 py解释器自己的线程是并发运行的,假设你的线程删除了一个变量,py解释器的垃圾回收线程在清空这个变量的过程中的clearing时刻,可能一个其它线程正好又重新给这个还没来及得清空的内存空间赋值了,结果就有可能新赋值的数据被删除了。
为了解决类似的问题,python解释器简单粗暴的加了锁,即当一个线程运行时,其它人都不能动,这样就解决了上述的问题,这可以说是Python早期版本的遗留问题。
信号量(Semaphore)
互斥锁 同时只允许一个线程更改数据,而Semaphore是同时允许一定数量的线程更改数据 ,比如厕所有3个坑,那最多只允许3个人上厕所,后面的人只能等里面有人出来了才能再进去。
import threading,time def run(n): semaphore.acquire() time.sleep(1) print("run the thread: %s" %n) semaphore.release() if __name__ == '__main__': semaphore = threading.BoundedSemaphore(5) #最多允许5个线程同时运行 for i in range(20): t = threading.Thread(target=run,args=(i,)) t.start()
事件 ( threading.Event )
Event是线程间通信最间的机制之一:一个线程发送一个event信号,其他的线程则等待这个信号。用于主线程控制其他线程的执行。
Events 管理一个flag,这个flag可以使用set()设置成True或者使用clear()重置为False,wait()则用于阻塞,在flag为True之前。flag默认为False。
-
Event.wait([timeout]) : 堵塞线程,直到Event对象内部标识位被设为True或超时(如果提供了参数timeout)。
-
Event.set() :将标识位设为Ture
-
Event.clear() : 将标识伴设为False。
-
Event.isSet() :判断标识位是否为Ture。
import threading def do(event): print('start') event.wait() print('execute') if __name__ == '__main__': event_obj = threading.Event() for i in range(10): t = threading.Thread(target=do, args=(event_obj,)) t.start() event_obj.clear() inp = input('input:') if inp == 'true': event_obj.set()
当线程执行的时候,如果flag为False,则线程会阻塞,当flag为True的时候,线程不会阻塞。它提供了本地和远程的并发性。
#!/usr/bin/env python # -*-coding:utf-8 -*- import threading,time import random def light(): count = 1 while True: if count > 6 and count < 10 : time.sleep(1) event.clear() print(" Red Right On .... ") elif count > 10: count = 0 else: time.sleep(1) event.set() print(" Green Right On .... ") count += 1 def car(num): while True: time.sleep(random.randrange(5)) if event.is_set(): print('Car [%s] is across the Street ..'%num) else: print('Car [%s] is Waitting the Right ..'%num) if __name__ == '__main__': event = threading.Event() event.set() Light = threading.Thread(target=light) Light.start() for i in range(3): Car = threading.Thread(target=car,args=(i,)) Car.start()
条件(Condition)
使得线程等待,只有满足某条件时,才释放n个线程
使用Condition对象可以在某些事件触发或者达到特定的条件后才处理数据,
Condition除了具有Lock对象的acquire方法和release方法外,还有wait方法、notify方法、notifyAll方法等用于条件处理。
threading.Condition([lock]):创建一个condition,支持从外界引用一个Lock对象(适用于多个condtion共用一个Lock的情况),默认是创建一个新的Lock对象。
acquire()/release():获得/释放 Lock
wait([timeout]): 线程挂起,直到收到一个notify通知或者超时(可选的,浮点数,单位是秒s)才会被唤醒继续运行。wait()必须在已获得Lock前提下才能调用,否则会触发RuntimeError。调用wait()会释放Lock,直至该线程被Notify()、NotifyAll()或者超时线程又重新获得Lock.
wait_for(predicate, timeout=None) 等待条件的计算结果为True。predicate应为可调用,其结果将被解释为布尔值。可以提供超时,给出最大等待时间。此程序方法可重复调用wait(),直到满足predicate,或直到发生超时。返回值是谓词的最后一个返回值,如果方法超时,则计算为False。
notify(n=1): 通知其他线程,那些挂起的线程接到这个通知之后会开始运行,默认是通知一个正等待该condition的线程,最多则唤醒n个等待的线程。notify()必须在已获得Lock前提下才能调用,否则会触发RuntimeError。notify()不会主动释放Lock。
notifyAll(): 如果wait状态线程比较多,notifyAll的作用就是通知所有线程(这个一般用得少)
例子1:
#!/usr/bin/env python # -*-coding:utf-8 -*- import threading,time L = [] class boy(threading.Thread): def __init__(self, cond, name='A boy'): threading.Thread.__init__(self) self.cond = cond self.name = name def run(self): time.sleep(1) '''''boy start conversation, make sure the girl thread stared before send notify''' self.cond.acquire() print(self.name + ':Hello pretty~,I miss you\n') self.cond.notify() self.cond.wait() print(self.name + ':like moth missing fire\n') self.cond.notify() self.cond.wait() print(self.name + ':and I bought a gift for you in the list L\n') L.append('channel5') self.cond.notify() self.cond.release() class girl(threading.Thread): def __init__(self, cond, name='A girl'): threading.Thread.__init__(self) self.cond = cond self.name = name def run(self): self.cond.acquire() self.cond.wait() print(self.name + ':Really, show me how much~\n') self.cond.notify() self.cond.wait() print(self.name + ':you\'re so sweet~') self.cond.notify() self.cond.wait() print(self.name + ':wow~~, that\'s ' + L.pop() + '---the one I dreamed for so long, I love you') self.cond.release() if __name__ == '__main__': cond = threading.Condition() husband = boy(cond, 'Aidan') wife = girl(cond, 'PPP') husband.start() wife.start() # husband.start() husband.join() # wait untill these two threads end wife.join() print('end converdarion\n')
例子2:
import threading def run(n): con.acquire() con.wait() print("run the thread: %s" %n) con.release() if __name__ == '__main__': con = threading.Condition() for i in range(10): t = threading.Thread(target=run, args=(i,)) t.start() while True: inp = input('>>>') if inp == 'q': break con.acquire() con.notify(int(inp)) con.release()
例子3:
def condition_func(): ret = False inp = input('>>>') if inp == '1': ret = True return ret def run(n): con.acquire() con.wait_for(condition_func) print("run the thread: %s" %n) con.release() if __name__ == '__main__': con = threading.Condition() for i in range(10): t = threading.Thread(target=run, args=(i,)) t.start()
Timer
定时器,指定n秒后执行某操作
from threading import Timer def hello(): print("hello, world") t = Timer(1, hello) t.start() # after 1 seconds, "hello, world" will be printed
queue模块
Queue 就是队列,它是线程安全。优势在于:实现了解耦 和 提高了处理效率 👉中文文档
Queue模块:实现了三类队列,主要差别在于取得数据的顺序上。
FIFO(First In First Out,先进先出)队列中,最早加入的任务会被最先得到。
LIFO(Last In First Out,后进先出)队列中,最后加入的任务会被最先得到(就像栈一样)。
在优先队列中,任务被保持有序(使用heapq模块),拥有最小值的任务(优先级最高)被最先得到。
举例来说,我们去肯德基吃饭。厨房是给我们做饭的地方,前台负责把厨房做好的饭卖给顾客,顾客则去前台领取做好的饭。这里的前台就相当于我们的队列。
这个模型也叫生产者-消费者模型。
import queue q = queue.Queue(maxsize=0) # 构造一个先进先出队列,maxsize指定队列长度,为0 时,表示队列长度无限制。 q = queue.LifoQueue(maxsize=0) # 构造一个后进先出队列,maxsize指定队列长度,为0 时,表示队列长度无限制。 q.join() # 等到队列为空的时候,在执行别的操作 q.qsize() # 返回队列的大小 (不可靠) q.empty() # 当队列为空的时候,返回True 否则返回False (不可靠) q.full() # 当队列满的时候,返回True,否则返回False (不可靠) q.put(item, block=True, timeout=None) # 将item放入Queue尾部,item必须存在,可以参数block默认为True,表示当队列满时,会等待队列给出可用位置, 为False时为非阻塞,此时如果队列已满,会引发queue.Full 异常。 可选参数timeout,表示 会阻塞设置的时间,过后, 如果队列无法给出放入item的位置,则引发 queue.Full 异常 q.get(block=True, timeout=None) # 移除并返回队列头部的一个值,可选参数block默认为True,表示获取值的时候,如果队列为空,则阻塞,为False时,不阻塞, 若此时队列为空,则引发 queue.Empty异常。 可选参数timeout,表示会阻塞设置的时候,过后,如果队列为空,则引发Empty异常。 q.put_nowait(item) # 等效于 put(item,block=False) q.get_nowait() # 等效于 get(item,block=False)
生产者--消费者:
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
import queue,threading,time q = queue.Queue(10) def Producer(i): while True: if q.qsize() <= 0: print('Produce %s'%(i)) q.put(i) time.sleep(1) def Consumer(i): while True: if q.qsize() >= 0 : msg = q.get() print('Counsumer [%s] get %s'%(i,msg)) time.sleep(1) for i in range(4): t = threading.Thread(target=Producer, args=(i,)) t.start() for i in range(7): t = threading.Thread(target=Consumer, args=(i,)) t.start()
例子2:
#!/usr/bin/env python # -*-coding:utf-8 -*- import threading import queue def producer(): for i in range(10): q.put("骨头 %s" % i) print("开始等待所有的骨头被取走...") q.join() print("所有的骨头被取完了...") def consumer(n): while q.qsize() > 0: print("%s 取到" % n, q.get()) q.task_done() # 告知这个任务执行完了 q = queue.Queue() p = threading.Thread(target=producer, ) p.start() c1 = consumer("李闯")
例子3:
#!/usr/bin/env python # -*-coding:utf-8 -*- import time,random import queue,threading q = queue.Queue() def Producer(name): count = 0 while count <20: time.sleep(random.randrange(3)) q.put(count) print('Producer %s has produced %s baozi..' %(name, count)) count +=1 def Consumer(name): count = 0 while count <20: time.sleep(random.randrange(4)) if not q.empty(): data = q.get() print(data) print('\033[32;1mConsumer %s has eat %s baozi...\033[0m' %(name, data)) else: print("-----no baozi anymore----") count +=1 p1 = threading.Thread(target=Producer, args=('A',)) c1 = threading.Thread(target=Consumer, args=('B',)) p1.start() c1.start()
Python 进程
multiprocessing是python的多进程管理包,和threading.Thread类似。直接从侧面用subprocesses替换线程使用GIL的方式,
由于这一点,multiprocessing模块可以让程序员在给定的机器上充分的利用CPU。
在multiprocessing中,通过创建Process对象生成进程,然后调用它的start()方法,但是,由于它是基于fork机制的,因此不被windows平台支持。
想要在windows中运行,必须使用 if __name__ == '__main__: 的方式
from multiprocessing import Process import threading import time def foo(i): print('say hi',i) for i in range(10): p = Process(target=foo,args=(i,)) p.start()
注意:由于进程之间的数据需要各自持有一份,所以创建进程需要的非常大的开销。
每一个子进程都是由父进程建立,并且内存空间独立,数据不共享。
from multiprocessing import Process import os import time def run(i): print('Process pid %s , Parent pid %s'%(os.getpid(),os.getppid())) time.sleep(1) if __name__ == '__main__': for i in range(10): p = Process(target=run, args=(i,)) p.start() print('Main Process Done ...' , os.getpid())
进程间数据共享
进程各自持有一份数据,默认无法共享数据
from multiprocessing import Process import os import time li = [] p_list = [] def run(i): li.append(os.getpid()) print(os.getpid(),li) time.sleep(1) if __name__ == '__main__': for i in range(10): p = Process(target=run, args=(i,)) p_list.append(p) p.start() for i in range(10): p_list[i].join() print('Main Process Done ...',os.getpid()) print(li)
在使用并发设计的时候最好尽可能的避免共享数据,尤其是在使用多进程的时候。 如果你真有需要 要共享数据, multiprocessing提供了两种方式。
1.Queues
使用方法跟threading里的queue差不多
from multiprocessing import Process, Queue def f(q): q.put([42, None, 'hello']) if __name__ == '__main__': q = Queue() p = Process(target=f, args=(q,)) p.start() print(q.get()) # prints "[42, None, 'hello']" p.join()
2.Pipe
Pipe返回的是管道2边的对象:「父连接」和「子连接」。
当子连接发送一个带有hello字符串的列表,父连接就会收到,所以parent_conn.recv()
就会打印出来。这样就可以简单的实现在多进程之间传输Python内置的数据结构了。
from multiprocessing import Process, Pipe def f(conn): conn.send([42, None, 'hello']) conn.close() if __name__ == '__main__': parent_conn, child_conn = Pipe() p = Process(target=f, args=(child_conn,)) p.start() print(parent_conn.recv()) # prints "[42, None, 'hello']" p.join()
内存共享:
1.Array / Value
数据可以用Value或Array存储在一个共享内存地图里,如下:
from multiprocessing import Process, Value, Array def f(n, a): n.value = 3.1415927 for i in range(len(a)): a[i] = -a[i] if __name__ == '__main__': num = Value('d', 0.0) arr = Array('i', range(10)) p = Process(target=f, args=(num, arr)) p.start() p.join() print(num.value) print(arr[:])
创建num和arr时,“d”和“i”参数由Array模块使用的typecodes创建:“d”表示一个双精度的浮点数,“i”表示一个有符号的整数,这些共享对象将被线程安全的处理。
Array(‘i’, range(10))中的‘i’参数:
‘c’: ctypes.c_char ‘u’: ctypes.c_wchar ‘b’: ctypes.c_byte ‘B’: ctypes.c_ubyte
‘h’: ctypes.c_short ‘H’: ctypes.c_ushort ‘i’: ctypes.c_int ‘I’: ctypes.c_uint
‘l’: ctypes.c_long, ‘L’: ctypes.c_ulong ‘f’: ctypes.c_float ‘d’: ctypes.c_double
2.Manager
由Manager()返回的manager提供 list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array类型的支持。
from multiprocessing import Process, Manager def Foo(i,dic): dic[i] = 100 + i print(dic) if __name__ == '__main__': manage = Manager() dic = manage.dict() for i in range(2): p = Process(target=Foo, args=(i,dic)) p.start() p.join()
当创建进程时(非使用时),共享数据会被拿到子进程中,当进程中执行完毕后,再赋值给原值。
同步机制:
multiprocessing的Lock、Condition、Event、RLock、Semaphore等同步原语和threading模块的机制是一样的,用法也类似
#!/usr/bin/env python # -*-coding:utf-8 -*- from multiprocessing import Process, Array, RLock def Foo(lock,temp,i): """ 将第0个数加100 """ lock.acquire() temp[0] = 100+i for item in temp: print(i,'----->',item) lock.release() if __name__ == '__main__': lock = RLock() temp = Array('i', [11, 22, 33, 44]) for i in range(20): p = Process(target=Foo,args=(lock,temp,i,)) p.start()
进程池
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。
进程池中有两个方法:
-
apply 阻塞型,只有func被执行完了,才会继续执行主函数的内容。
-
apply_async 非阻塞型,可以通过 get() 获取结果
from multiprocessing import Process,Pool import time def Foo(i): time.sleep(2) return i+100 def Bar(arg): print(arg,'--') if __name__ == '__main__': pool = Pool(5) for i in range(10): pool.apply_async(func=Foo, args=(i,), callback=Bar) # pool.apply(func=Foo, args=(i,)) # pool.map(Foo,[i for i in range(10)]) # pool.map_async(Foo,[i for i in range(10)],callback=Bar) print('end') pool.close() # 关闭 Pool,使其不再接受新任务 pool.join() # 主进程阻塞,等待子进程退出
官方demo
from multiprocessing import Pool, TimeoutError import time import os def f(x): return x*x if __name__ == '__main__': # 创建4个进程 with Pool(processes=4) as pool: # 打印 "[0, 1, 4,..., 81]" print(pool.map(f, range(10))) # 使用任意顺序输出相同的数字, for i in pool.imap_unordered(f, range(10)): print(i) # 异步执行"f(20)" res = pool.apply_async(f, (20,)) # 只运行一个进程 print(res.get(timeout=1)) # 输出 "400" # 异步执行 "os.getpid()" res = pool.apply_async(os.getpid, ()) # 只运行一个进程 print(res.get(timeout=1)) # 输出进程的 PID # 运行多个异步执行可能会使用多个进程 multiple_results = [pool.apply_async(os.getpid, ()) for i in range(4)] print([res.get(timeout=1) for res in multiple_results]) # 是一个进程睡10秒 res = pool.apply_async(time.sleep, (10,)) try: print(res.get(timeout=1)) except TimeoutError: print("发现一个 multiprocessing.TimeoutError异常") print("目前,池中还有其他的工作") # 退出with块中已经停止的池 print("Now the pool is closed and no longer available")
进程池的方法
-
apply(func[, args[, kwds]]) :使用arg和kwds参数调用func函数,结果返回前会一直阻塞,由于这个原因,apply_async()更适合并发执行,另外,func函数仅被pool中的一个进程运行。
-
apply_async(func[, args[, kwds[, callback[, error_callback]]]]) : apply()方法的一个变体,会返回一个结果对象。如果callback被指定,那么callback可以接收一个参数然后被调用,当结果准备好回调时会调用callback,调用失败时,则用error_callback替换callback。 Callbacks应被立即完成,否则处理结果的线程会被阻塞。
-
close() : 阻止更多的任务提交到pool,待任务完成后,工作进程会退出。
-
terminate() : 不管任务是否完成,立即停止工作进程。在对pool对象进程垃圾回收的时候,会立即调用terminate()。
-
join() : wait工作线程的退出,在调用join()前,必须调用close() or terminate()。这样是因为被终止的进程需要被父进程调用wait(join等价与wait),否则进程会成为僵尸进程。
-
map(func, iterable[, chunksize]) 注意,虽然第二个参数是一个迭代器,但在实际使用中,必须在整个队列都就绪后,程序才会运行子进程。
-
map_async(func, iterable[, chunksize[, callback[, error_callback]]])
-
imap(func, iterable[, chunksize])
-
imap_unordered(func, iterable[, chunksize])
-
starmap(func, iterable[, chunksize])
-
starmap_async(func, iterable[, chunksize[, callback[, error_back]]])
多线程与多进程 理解与速度比较: 👉详情点击
协程
协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是协程:协程是一种用户态的轻量级线程。
线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作则是程序员。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:
协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程;
协程的好处:
-
无需线程上下文切换的开销
-
无需原子操作锁定及同步的开销方便切换控制流,简化编程模型
- "原子操作(atomic operation)是不需要synchronized",所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。
-
高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
-
无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
-
进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
协程一个标准定义,即符合什么条件就能称之为协程:
-
必须在只有一个单线程里实现并发
-
修改共享数据不需加锁
-
用户程序里自己保存多个控制流的上下文栈
-
一个协程遇到IO操作自动切换到其它协程
当我们说“上下文”的时候,指的是程序在执行中的一个状态。通常我们会用调用栈来表示这个状态——栈记载了每个调用层级执行到哪里,还有执行时的环境情况等所有有关的信息。
当我们说“上下文切换”的时候,表达的是一种从一个上下文切换到另一个上下文执行的技术。而“调度”指的是决定哪个上下文可以获得接下去的CPU时间的方法。
Python中的协程是通过“生成器(generator)”的概念实现的。
def consumer(): # 定义消费者,由于有yeild关键词,此消费者为一个生成器 print("[Consumer] Init Consumer ......") r = "init ok" # 初始化返回结果,并在启动消费者时,返回给生产者 while True: n = yield r # 消费者通过yield接收生产者的消息,同时返给其结果 print("[Consumer] conusme n = %s, r = %s" % (n, r)) r = "consume %s OK" % n # 消费者消费结果,下个循环返回给生产者 def produce(c): # 定义生产者,此时的 c 为一个生成器 print("[Producer] Init Producer ......") r = c.send(None) # 启动消费者生成器,同时第一次接收返回结果 print("[Producer] Start Consumer, return %s" % r) n = 0 while n < 5: n += 1 print("[Producer] While, Producing %s ......" % n) r = c.send(n) # 向消费者发送消息并准备接收结果。此时会切换到消费者执行 print("[Producer] Consumer return: %s" % r) c.close() # 关闭消费者生成器 print("[Producer] Close Producer ......") produce(consumer())
greenlet
from greenlet import greenlet def test1(): print(12) gr2.switch() print(34) gr2.switch() def test2(): print(56) gr1.switch() print(78) gr1 = greenlet(test1) gr2 = greenlet(test2) gr1.switch()
gevent
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。
Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
import gevent def foo(): print('Running in foo') gevent.sleep(0) print('Explicit context switch to foo again') def bar(): print('Explicit context to bar') gevent.sleep(0) print('Implicit context switch back to bar') gevent.joinall([ gevent.spawn(foo), gevent.spawn(bar), ])
应用实例
from gevent import monkey; monkey.patch_all() import gevent import requests def f(url): print('GET: %s' % url) resp = requests.get(url) data = resp.text print(url,len(data)) gevent.joinall([ gevent.spawn(f, 'https://www.python.org/'), gevent.spawn(f, 'https://www.yahoo.com/'), gevent.spawn(f, 'https://github.com/'), ])
通过gevent实现单线程下的多socket并发
import sys import socket import time import gevent from gevent import socket,monkey monkey.patch_all() def server(port): s = socket.socket() s.bind(('0.0.0.0', port)) s.listen(500) while True: cli, addr = s.accept() gevent.spawn(handle_request, cli) def handle_request(conn): try: while True: data = conn.recv(1024) print("recv:", data) conn.send(data) if not data: conn.shutdown(socket.SHUT_WR) except Exception as ex: print(ex) finally: conn.close() if __name__ == '__main__': server(8001)
import socket HOST = 'localhost' # The remote host PORT = 8001 # The same port as used by the server s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((HOST, PORT)) while True: msg = bytes(input(">>:"),encoding="utf8") s.sendall(msg) data = s.recv(1024) #print(data) print('Received', repr(data)) s.close()
import socket import threading def sock_conn(): client = socket.socket() client.connect(("localhost",8001)) count = 0 while True: #msg = input(">>:").strip() #if len(msg) == 0:continue client.send( ("hello %s" %count).encode("utf-8")) data = client.recv(1024) print("[%s]recv from server:" % threading.get_ident(),data.decode()) #结果 count +=1 client.close() for i in range(100): t = threading.Thread(target=sock_conn) t.start()
补充:上下文管理
with open实质:
import contextlib @contextlib.contextmanager def Myopen(file_path,mode): f = open(file_path,mode) try: yield f finally: f.close() with Myopen('1.txt','w')as f: f.write('1234')