Python线程、进程、协程
线程与进程
我们都知道计算机是由硬件和软件组成的。硬件中的CPU是计算机的核心,它承担计算机的所有任务。 操作系统是运行在硬件之上的软件,是计算机的管理者,它负责资源的管理和分配、任务的调度。 程序是运行在系统上的具有某种功能的软件,比如说浏览器,音乐播放器等。 每次执行程序的时候,都会完成一定的功能,比如说浏览器帮我们打开网页,为了保证其独立性,就需要一个专门的管理和控制执行程序的数据结构——进程控制块。 进程就是一个程序在一个数据集上的一次动态执行过程。 进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。
在早期的操作系统里,计算机只有一个核心,进程执行程序的最小单位,任务调度采用时间片轮转的抢占式方式进行进程调度。每个进程都有各自的一块独立的内存,保证进程彼此间的内存地址空间的隔离。 随着计算机技术的发展,进程出现了很多弊端,一是进程的创建、撤销和切换的开销比较大,二是由于对称多处理机(对称多处理机(SymmetricalMulti-Processing)又叫SMP,是指在一个计算机上汇集了一组处理器(多CPU),各CPU之间共享内存子系统以及总线结构)的出现,可以满足多个运行单位,而多进程并行开销过大。 这个时候就引入了线程的概念。 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合 和堆栈共同组成。线程的引入减小了程序并发执行时的开销,提高了操作系统的并发性能。 线程没有自己的系统资源,只拥有在运行时必不可少的资源。但线程可以与同属与同一进程的其他线程共享进程所拥有的其他资源。
线程是属于进程的,线程运行在进程空间内,同一进程所产生的线程共享同一内存空间,当进程退出时该进程所产生的线程都会被强制退出并清除。线程可与属于同一进程的其它线程共享进程所拥有的全部资源,但是其本身基本上不拥有系统资源,只拥有一点在运行中必不可少的信息(如程序计数器、一组寄存器和栈)。
区别
进程(process)是一块包含了某些资源的内存区域。操作系统利用进程把它的工作划分为一些功能单元。
进程中所包含的一个或多个执行单元称为线程。进程还拥有一个私有的虚拟地址空间,该空间仅能被它所包含的线程访问。
线程只能归属于一个进程并且它只能访问该进程所拥有的资源。当操作系统创建一个进程后,该进程会自动申请一个名为主线程或首要线程的线程。
处理IO密集型任务或函数用线程;
处理计算密集型任务或函数用进程。
python 线程
操作系统能够进行运算调度的最小单位,被包含再进程之中,是进程中的实际单位。
一条线程指的是进程中的一个单一顺序的控制流,一个进程中可以并发多个线程,每个线程执行不同的任务,
1.threading模块
下面代码中定义函数,然后创建5个线程并启动,可以认为是并发,但不是真正的并发,因为有GIL存在。
import threading import time class MyTread(threading.Thread): def __init__(self, n): threading.Thread.__init__(self) self.n = n def run(self): time.sleep(2) print('run', self.n) if __name__ == '__main__': for i in range(0, 5): t = MyTread(i) t.start()
import threading import time def run(n): time.sleep(2) print('run', n) if __name__ == '__main__': for i in range(0,5): t = threading.Thread(target=run, args=(i,)) #创建线程 t.start() #启动线程
Thread方法
t.start() : 启动线程, t.getName() : 获取线程的名称 t.setName() : 设置线程的名称 t.name : 获取或设置线程的名称 t.is_alive() : 判断线程是否为激活状态 t.isAlive() :判断线程是否为激活状态 t.isDaemon() : 判断是否为守护线程 t.ident :获取线程的标识符。线程标识符是一个非零整数,只有在调用了start()方法之后该属性才有效,否则它只返回None。 t.setDaemon() 设置为守护线程(默认:False);通过一个布尔值设置线程是否为守护线程,必须在执行start()方法之前才可以使用。主线程执行过程中,守护线程也正常进行,当主线程执行完毕后,守护线程不论结束与否,均停止。
import threading import time def run(n): print('start run', n) time.sleep(2) print('end run', n) if __name__ == '__main__': for i in range(0,5): t= threading.Thread(target=run, args=(i,)) # 必须在未线程active前设置 t.setDaemon(True) t.start()
t.join() :阻塞线程,当某个线程运用此方法,那么其他线程就会被阻塞,待这个线程运行完,才会继续运行其他线程;该方法使得多线程变得无意义。
可以将线程添加入列表,然后循环线程列表对线程jion(),这样主线程会等所以线程执行完,再继续执行。
import threading import time def run(n): time.sleep(2) print('run', n) if __name__ == '__main__': for i in range(0,5): t= threading.Thread(target=run, args=(i,)) t.start() t.join() print('end')
t.run() :线程被cpu调度后自动执行线程对象的run方法,可以继承threading类对run进行重写。
import threading import time class MyThread(threading.Thread): def __init__(self, n): threading.Thread.__init__(self) self.n = n def run(self): time.sleep(2) print('run', self.n) if __name__ == '__main__': for i in range(0, 5): t = MyThread(i) t.start()
2.互斥锁
由于线程之间运行是抢占模式,CPU调度一个线程后,有可能只执行几条代码,就去调度其他线程;由于线程之间的资源是共享的,多个线程同时调用某一时刻的数据进行操作,只能保留最后一次修改时的数据,就发生了冲突,这时可以用锁来解决。
import threading import time class MyLock(threading.Thread): def run(self): global total lock.acquire() # 获取锁 tmp = total time.sleep(0.0001) total = tmp -1 lock.release() # 释放锁 if __name__ == '__main__': total = 100 lock = threading.Lock() # 创建一个锁 thread_list = [] for i in range(0, 100): t = MyLock() thread_list.append(t) t.start() for i in thread_list: # 等待所有线程执行完 i.join() print(total)
3.死锁和递归锁
死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
import threading import time class MyThread(threading.Thread): def doA(self): lockA.acquire() print(self.name,"gotlockAA",time.ctime()) time.sleep(3) lockB.acquire() print(self.name,"gotlockAB",time.ctime()) lockB.release() lockA.release() def doB(self): lockB.acquire() print(self.name,"gotlockBB",time.ctime()) time.sleep(2) lockA.acquire() print(self.name,"gotlockBA",time.ctime()) lockA.release() lockB.release() def run(self): self.doA() self.doB() if __name__=="__main__": lockA = threading.Lock() lockB = threading.Lock() threads = [] for i in range(5): threads.append(MyThread()) for t in threads: t.start() for t in threads: t.join()
import threading def foo(): lock.acquire() print('First Lock') #上面获取锁后还未释放,此时没有办法继续获取锁 lock.acquire() print('Second Lock') lock.release() lock.release() if __name__ == '__main__': lock = threading.Lock() t = threading.Thread(target=foo) t.start()
递归锁就可以解决死锁问题,创建一个锁,可以重复获取,但也要依此释放;
import threading class MyRlock(threading.Thread): def run(self): r_lock.acquire() print('First Lock') r_lock.acquire() print('Second Lock') r_lock.release() r_lock.release() if __name__ == '__main__': r_lock = threading.RLock() t = MyRlock() t.start()
4.信号量Semaphore
信号量Semaphore允许一定数量的线程在同一时间点更改同一块数据。它是基于内部计数器,每调用一次acquire(),计数器-1,每调用一次release(),计数器+1,当计数器为0时,acquire()调用被阻塞。就像排队吃饭,一共5个位子;刚开始可以一下进入5个人,再有吃饭的需要等待,吃完一个才能进去一个。
import threading import time class MySemaphore(threading.Thread): def __init__(self, n): threading.Thread.__init__(self) self.n = n def run(self): semaphore.acquire() print('Semaphore', self.n) time.sleep(2) semaphore.release() if __name__ == '__main__': semaphore = threading.Semaphore(4) for i in range(0, 10): t = MySemaphore(i) t.start()
5.条件变量同步Condition
Codition有两层锁,一把底层锁会在进入wait方法的时候释放,离开wait方法的时候再次获取,上层锁会在每次调用wait时分配一个新的锁,并放入condition的等待队列中,而notify负责释放这个锁。
acquire([timeout])/release(): 调用关联的锁的相应方法。 wait([timeout]): 调用这个方法将使线程进入Condition的等待池等待通知,并释放锁。 notify(): 调用这个方法将从等待池挑选一个线程并通知,收到通知的线程将自动调用acquire()尝试获得锁定(进入锁定池);其他线程仍然在等待池中。 notifyAll(): 调用这个方法将通知等待池中所有的线程,这些线程都将进入锁定池尝试获得锁定。
import threading class Buyer(threading.Thread): def run(self): condition.acquire() #获取锁 print('西瓜怎么卖?') condition.notifyAll() #调用这个方法将通知等待池中所有的线程,这些线程都将进入锁定池,尝试获得锁定。 condition.wait() #调用该方法的线程进入WAITING状态,只有等待另外线程的通知或被中断才会返回,需要注意会释放对象的锁。 print('包甜吗?') condition.notifyAll() condition.release() #释放锁 class Seller(threading.Thread): def run(self): condition.acquire() condition.wait() print('五块') condition.notifyAll() condition.wait() print('不甜我吃了它') condition.release() if __name__ == '__main__': condition = threading.Condition() buyer = Buyer() seller = Seller() seller.start() buyer.start()
6.同步条件Event
条件同步和条件变量同步差不多意思,只是少了锁功能,因为条件同步设计于不访问公共资源的条件环境。
事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法便不再阻塞。
set(): 将标志设为True,并通知所有处于等待阻塞状态的线程恢复运行状态。 clear(): 将标志设为False。 wait(timeout): 如果标志为True将立即返回,否则阻塞线程至等待阻塞状态,等待其他线程调用set()。 isSet(): 获取内置标志状态,返回True或False
import threading import time class Buyer(threading.Thread): def __init__(self): threading.Thread.__init__(self, name='buyer') def run(self): print('%s:扫码支付' % self.name) event.is_set() or event.set() time.sleep(3) print('%s:付完了' % self.name) event.is_set() or event.set() class Seller(threading.Thread): def __init__(self): threading.Thread.__init__(self, name='sller') def run(self): event.wait() print('%s:扫这里' % self.name) event.clear() event.wait() print('%s:收到了' % self.name) if __name__ == '__main__': event = threading.Event() buyer = Buyer() seller = Seller() seller.start() buyer.start()
7.队列Queue
Python的Queue模块中提供了同步的、线程安全的队列类,包括FIFO队列(Queue)、LIFO队列(LifoQueue)和优先级队列(PriorityQueue)。这些队列都实现了锁原语,能够在多线程中直接使用。可以使用队列来实现线程间的同步。
q = queue.Queue(maxsize = 0): 构造一个FIFO队列,maxsize可以限制队列的大小。如果超出maxsize,就会加锁,再加入就会阻塞,直到队列的内容被消费掉。maxsize的值小于等于0,那么队列的尺寸就是无限制。
q.qsize():返回队列的大小 q.empty():如果队列为空,返回True,反之False q.full():如果队列满了,返回True,反之False q.full:与 maxsize 大小对应 q.get([[block=True], timeout=None]):获取队列,可选参数block默认为True,表示当队列满时,会等待队列给出可用位置,为False时为非阻塞,此时如果队列已满,会引发queue.Full 异常。可选参timeout,表示 会阻塞设置的时间,过后,如果队列无法给出放入item的位置,则引发 queue.Full 异常 q.get_nowait():相当Queue.get(False) q.put(item, [block=True, [timeout=None]]):写入队列,timeout等待时间 q.put_nowait(item):相当Queue.put(item, False) q.task_done():在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号 q.join():实际上意味着等到队列为空,再执行别的操作
import threading import time import queue class Producer(threading.Thread): def __init__(self, num): threading.Thread.__init__(self) self.num = num + 1 self.food = 1 def run(self): while True: food_num = '-'.join([str(self.num), str(self.food)]) q.put(food_num) print('Producer%s:生产 %s' % (self.num, food_num)) self.food += 1 time.sleep(2) class Consumer(threading.Thread): def __init__(self, num): threading.Thread.__init__(self) self.num = num + 1 def run(self): while True: food_num = q.get() print('Consumer%s:消费 %s' % (self.num, food_num)) time.sleep(5) if __name__ == '__main__': q = queue.Queue(10) thread_list = [] for i in range(3): t = Producer(i) thread_list.append(t) for i in range(5): thread_list.append(Consumer(i)) for t in thread_list: t.start()
python进程
fork()
Unix/Linux操作系统提供了一个fork()系统调用,它非常特殊。普通的函数调用,调用一次,返回一次,但是fork()调用一次,返回两次,因为操作系统自动把当前进程(称为父进程)复制了一份(称为子进程),然后,分别在父进程和子进程内返回。
子进程永远返回0,而父进程返回子进程的ID。这样做的理由是,一个父进程可以fork出很多子进程,所以,父进程要记下每个子进程的ID,而子进程只需要调用getppid()就可以拿到父进程的ID。
Python的os
模块封装了常见的系统调用,其中就包括fork,可以在Python程序中轻松创建子进程。
import os print('main process %s' % os.getpid()) pid = os.fork() print('my pid %s' % os.getpid()) #执行结果 #main process 5430 #my pid 5430 #my pid 5431
multiprocessing模块
GIL机制在一个进程中同时刻只能调用一个线程,不可以多个线程并发运行,就只能利用一个CPU;multiprocessing模块可以启动多个进程,这样就可以充分的利用CPU。
from multiprocessing import Process import time import os class MyProcess(Process): def __init__(self, n): Process.__init__(self, name=str(n)) def run(self): print('parent %s' % os.getppid()) time.sleep(2) print('me %s' % os.getpid()) if __name__ == '__main__': print('main process %s' % os.getpid()) process_list =[] for i in range(2): p = MyProcess(i) p.start() process_list.append(p) for p in process_list: p.join() print('end')
from multiprocessing import Process import time def foo(n): print('start %s' % n) time.sleep(2) print('end %s' % n) if __name__ == '__main__': for i in range(5): p = Process(target=foo, args=(i,)) p.start()
进程间通信
Process之间肯定是需要通信的,操作系统提供了很多机制来实现进程间的通信。Python的multiprocessing模块包装了底层的机制,提供了Queue、Pipes等多种方式来交换数据。
Queue队列
q = multiprocessing.Queue(maxsize = -1): 构造一个FIFO队列,maxsize可以限制队列的大小。如果超出maxsize,就会加锁,再加入就会阻塞,直到队列的内容被消费掉。maxsize的值小于等于0,那么队列的尺寸就是无限制。
q.qsize():返回队列的大小 q.empty():如果队列为空,返回True,反之False q.full():如果队列满了,返回True,反之False q.full:与 maxsize 大小对应 q.get([[block=True], timeout=None]):获取队列,可选参数block默认为True,表示当队列满时,会等待队列给出可用位置,为False时为非阻塞,此时如果队列已满,会引发queue.Full 异常。可选参timeout,表示 会阻塞设置的时间,过后,如果队列无法给出放入item的位置,则引发 queue.Full 异常 q.get_nowait():相当Queue.get(False) q.put(item, [block=True, [timeout=None]]):写入队列,timeout等待时间 q.put_nowait(item):相当Queue.put(item, False) q.task_done():在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号 q.join():实际上意味着等到队列为空,再执行别的操作
from multiprocessing import Process, Queue import time class Producer(Process): def __init__(self, num, queue): Process.__init__(self) self.queue = queue self.num = num + 1 self.food = 1 def run(self): while True: food_num = '-'.join([str(self.num), str(self.food)]) self.queue.put(food_num) print('Producer%s:生产 %s' % (self.num, food_num)) self.food += 1 time.sleep(2) class Consumer(Process): def __init__(self, num, queue): Process.__init__(self) self.queue = queue self.num = num + 1 def run(self): while True: food_num = self.queue.get() print('Consumer%s:消费 %s' % (self.num, food_num)) time.sleep(5) if __name__ == '__main__': q = Queue(10) thread_list = [] for i in range(3): t = Producer(i, q) thread_list.append(t) for i in range(5): thread_list.append(Consumer(i, q)) for t in thread_list: t.start()
Pipe管道
利用Pipe()创建两个可双向通信的PipeConnection,然后利用send, recv方法进行进程间的通信。
from multiprocessing import Process, Pipe class Consumer(Process): def __init__(self, conn): Process.__init__(self) self.conn = conn def run(self): send_data = '我要消费!' self.conn.send(send_data) print(self.conn.recv()) #不需要指定接受字节数 class Producer(Process): def __init__(self, conn): Process.__init__(self) self.conn = conn def run(self): recv_data = self.conn.recv() self.conn.send('楼上请!') #可传输str,不需要转成bytes print(recv_data) if __name__ == '__main__': conn1, conn2 = Pipe() print(conn1, conn2) p = Producer(conn2) c = Consumer(conn1) p.start() c.start()
数据共享
由Manager()返回的manager提供list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event, Barrier, Queue, Value and Array类型的支持。
from multiprocessing import Process, Manager class Extender(Process): def __init__(self, data): Process.__init__(self) self.data = data def run(self): self.data.append('hello') class Reducer(Process): def __init__(self, data): Process.__init__(self) self.data = data def run(self): del self.data[0] if __name__ == '__main__': with Manager() as manager: L = manager.list(range(5)) extender = Extender(L) reducer = Reducer(L) extender.start() reducer.start() extender.join() reducer.join() print(L)
python协程
多线程工作过程中,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时。
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
协程适用于IO密集型的应用。
yield实现协程
如果有两个代码块,程序只有执行完前一个,才会去执行下一个;那么可以利用yield中断代码块的执行,去执行下一个;
class Productor(): def start(self): num = yield print('%d.P准备原材' % num) num = yield print('%d.P处理食材' % num) num = yield print('%d.P制作成品' % num) yield class Tester(): def start(self): num = yield print('%d.T检测原材' % num) num = yield print('%d.T检测食材' % num) num = yield print('%d.T检测成品' % num) yield if __name__ == '__main__': p = Productor() t = Tester() p_g = p.start() #返回的是一个生成器,不是执行结果 t_g = t.start() next(p_g) next(t_g) for i in range(3): p_g.send(i+1) t_g.send(i+1)
greenlet模块
一个 “greenlet” 是一个小型的独立伪线程。可以把它想像成一些栈帧,栈底是初始调用的函数,而栈顶是当前greenlet的暂停位置。你使用greenlet创建一堆这样的堆栈,然后在他们之间跳转执行。跳转必须显式声明的:一个greenlet必须选择要跳转到的另一个greenlet,这会让前一个挂起,而后一个在此前挂起处恢复执行。不同greenlets之间的跳转称为切换(switching) 。
greenlet模块需要安装。
from greenlet import greenlet class Productor(): def start(self): print('P准备原材') t.switch() print('P处理食材') t.switch() print('P制作成品') t.switch() class Tester(): def start(self): print('T检测原材') p.switch() print('T检测食材') p.switch() print('T检测成品') if __name__ == '__main__': p = Productor() t = Tester() p = greenlet(p.start) t = greenlet(t.start) p.switch()
gevent模块
gevent是第三方库,通过greenlet实现协程,其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO
gevent.spawn():创建一个普通的Greenlet对象并切换 gevent.spawn_later(seconds=3):延时创建一个普通的Greenlet对象并切换 gevent.spawn_raw():创建的协程对象属于一个组 gevent.getcurrent():返回当前正在执行的greenlet gevent.joinall(jobs):将协程任务添加到事件循环,接收一个任务列表 gevent.wait():可以替代join函数等待循环结束,也可以传入协程对象列表 gevent.kill():杀死一个协程 gevent.killall():杀死一个协程列表里的所有协程 monkey.patch_all():会自动将python的一些标准模块替换成gevent框架
import gevent class Productor(): def start(self): print('P准备原材') gevent.sleep(2) print('P处理食材') gevent.sleep(2) print('P制作成品') class Tester(): def start(self): gevent.sleep(1) print('T检测原材') gevent.sleep(2) print('T检测食材') gevent.sleep(2) print('T检测成品') if __name__ == '__main__': p = Productor() t = Tester() pg = gevent.spawn(p.start) tg = gevent.spawn(t.start) gevent.joinall([pg, tg])
爬取网页内容
import gevent import requests def func(url): print("get: %s"%url) gevent.sleep(0) data =requests.get(url) ret = data.text print('%d bytes received from %s.' % (len(ret), url)) gevent.joinall([ gevent.spawn(func, 'https://www.python.org/'), gevent.spawn(func, 'https://www.yahoo.com/'), gevent.spawn(func, 'https://github.com/'), ])