Python 线程、进程、协程
本章内容
1、线程、进程介绍
2、线程的基本使用
3、setDeamon、join
4、线程锁
5、线程池
6、信号量(Semaphore)
7、时间(event)
8、条件(Condition)
9、Timer
10、queue队列
11、生产消费者模型
12、进程
13、进程间的通讯
14、进程池
15、协程
16、Greenlet
17、Gevent
线程、进程介绍
1、什么是进程、什么是线程?
2、为什么要有进程?
让我们下面来解答这些答案
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程的实际运算单位,一条线程指的是进程中一个单一顺序的控制流,一个进程中科院并发多个线程,每条线程并行执行不同的任务
进程一个程序所有资源的集合,一个程序的实例,如果进行想要执行必须有一个线程,本身不能执行
线程与进程的区别
1、程序工作的最小单元是线程,是一个指令集
2、一个进程中的所有线程共享同一块内存空间,可以做直接交流,而进程的内存是独立的,如果要通信必须通过一个中间代理来实现
3、一个线程可以控制和操作同一个进程里的其他线程,但进程只能操作子进程
4、一个线程的修改可能影响到其他其他线程,一个进程的修改不会影响到其他进程
5、每个应用程序 --->至少有一个进程 --->至少有一个线程
6、应用场景:
IO密集型:用线程
计算秘籍型:进程
python独有的GIL,全局解释器锁。
保证同一个进程中只有一个线程同时被调度(如果所有线程同时被调用,一个变量可能会被改变多次)。 这是被人诟病的点,但是去掉后,性能会更低,所以还存在,正式由于GIL的存在,所以与cpu打交道时,一个cpu只会处理一个线程。
python是假线程,只是cpu上下文切换的快,所以像多线程。
线程的基本使用
import threading
import time
def task(arg,at):
time.sleep(1)
print(arg,at)
for i in range(10):
t = threading.Thread(target=task,args=[i,12])#target跟的是函数,args跟可迭代的对象,
t.start()
print("main done!!!")
#每个t都是一个线程,这个python程序是主线程。
import threading import time class MyThread(threading.Thread): def __init__(self,n): super(MyThread,self).__init__() self.n = n def run(self): #这里必须得使用run,名字不可以随便改 print('run task',self.n) t1 = MyThread('1') t2 = MyThread('2') t1.start() t2.start()
更多方法:
- start #线程准备就绪,等待CPU调度
- setName #为线程设置名称
- getName #获取线程名称
- setDaemon #设置为后台线程或前台线程(默认)
如果是后台线程,主线程执行过程中,后台线程也在进行,主线程执行完毕后,后台线程不论成功与否,均停止
如果是前台线程,主线程执行过程中,前台线程也在进行,主线程执行完毕后,等待前台线程也执行完成后,程序停止 - join #逐个执行每个线程,执行完毕后继续往下执行,该方法使得多线程变得无意义
- run #线程被cpu调度后自动执行线程对象的run方法
setDeamon、join
import threading
import time
def task(arg,at):
time.sleep(1)
print(arg,at)
for i in range(10):
t = threading.Thread(target=task,args=[i,12]) #target跟的是函数,args跟可迭代的对象
#t.setDaemon(True) #设置为子程序为守护线程,主线程停了,子线程就不会执行
t.start()
#t.join() #使线程依次的进行,一个完了再另外一个,相当于wait
#t.join(1) #等待最大的时间
print("main done!!!",threading.current_thread()) #查看当前线程的类型,主线程or子线程
print(threading.active_count()) #查看活跃的线程
如果我for循环启动50个线程,然后每个线程都想用join那该如何?
t_obj = [] for i in range(50): t = threading.Thread(target=run,args=(i,)) t.start() t_obj.append(t) #把t对象放到列表中 for t in t_obj: t.join() #把t对象与join关联起来
线程锁
告知线程,我要修改这个数据,其线程不要动!python2中需要锁,python3中不需要
比如,淘宝商店的商品计数,如果100个并发过来同时要买商品,这个商品的计数怎么往下减呢,肯定不能同时做修改,需要一个一个的来,这里就用到了锁的概念,意思是虽然是并发来的,但是每个进程都放一把锁,每次只能一个来修改。
#!/usr/bin/env python # -*- coding:utf-8 -*- import threading import time gl_num = 0 def show(arg): global gl_num time.sleep(1) gl_num +=1 print gl_num for i in range(10): t = threading.Thread(target=show, args=(i,)) t.start() print 'main thread stop' #运行结果 MacBook-Air ~/tmp $ python aa.py main thread stop 123 56 78 9 4 10 MacBook-Air ~/tmp $ python aa.py main thread stop 13245 7 8 9 6 10 #在python2中会出现这种错乱的情况,且每次的情况都不一样
#!/usr/bin/env python #coding:utf-8 import threading import time gl_num = 0 lock = threading.Lock() def Func(): lock.acquire() global gl_num gl_num +=1 time.sleep(1) print gl_num lock.release() for i in range(10): t = threading.Thread(target=Func) t.start()
递归锁:
lock.threading.RLock()
递归锁的意思就是,锁了两次,然后再解锁两次才能释放
多人锁:
可以支持批量操作,适用于可批量操作的任务
事件锁
当输入 1 时,才会解锁,继续执行。
肆意妄为锁
想执行几个就执行几个。
执行结果就是输入数字几就会执行几个线程
线程池
python2,中没有线程池,但可以去自己写个,但是比较麻烦,python3有,且简单
下面是个利用线程池进行request 请求的小例子
信号量(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__': num= 0 semaphore = threading.BoundedSemaphore(5) #最多允许5个线程同时运行 for i in range(20): t = threading.Thread(target=run,args=(i,)) t.start() #运行结果是,线程5个5个的向外输出,同时5个进行
事件(event)
python线程的事件用于主线程控制其他线程的执行,事件主要提供了三个方法 set、wait、clear。
事件处理的机制:全局定义了一个“Flag”,如果“Flag”值为 False,那么当程序执行 event.wait 方法时就会阻塞,如果“Flag”值为True,那么event.wait 方法时便不再阻塞。
- clear:将“Flag”设置为False
- set:将“Flag”设置为True
import threading,time import random def light(): if not event.isSet(): event.set() #wait就不阻塞 #绿灯状态 count = 0 while True: if count < 10: print('\033[42;1m--green light on---\033[0m') elif count <13: print('\033[43;1m--yellow light on---\033[0m') elif count <20: if event.isSet(): event.clear() print('\033[41;1m--red light on---\033[0m') else: count = 0 event.set() #打开绿灯 time.sleep(1) count +=1 def car(n): while 1: time.sleep(random.randrange(10)) if event.isSet(): #绿灯 print("car [%s] is running.." % n) else: print("car [%s] is waiting for the red light.." %n) if __name__ == '__main__': event = threading.Event() Light = threading.Thread(target=light) Light.start() for i in range(3): t = threading.Thread(target=car,args=(i,)) t.start()
#红绿灯变化,三辆车随之变化
条件(Condition)
使得线程等待,只有满足某条件时,才释放n个线程
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()
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队列
创建一个“队列”对象
import Queue
myqueue = Queue.Queue(maxsize = 10)
Queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。
将一个值放入队列中
myqueue.put(10)
调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数, 默认为1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数 据单元。如果block为0,put方法将引发Full异 常。
将一个值从队列中取出
myqueue.get()
调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且block为True,get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。
python queue模块有三种队列:
1、python queue模块的FIFO队列先进先出。
2、LIFO类似于堆。即先进后出。
3、还有一种是优先级队列级别越低越先出来。
针对这三种队列分别有三个构造函数:
1、class Queue.Queue(maxsize) FIFO
2、class Queue.LifoQueue(maxsize) LIFO
3、class Queue.PriorityQueue(maxsize) 优先级队列
介绍一下此包中的常用方法:
Queue.qsize() 返回队列的大小
Queue.empty() 如果队列为空,返回True,反之False
Queue.full() 如果队列满了,返回True,反之False
Queue.full 与 maxsize 大小对应
Queue.get([block[, timeout]]) 获取队列,timeout等待时间
Queue.get_nowait() 相当Queue.get(False)
非阻塞 Queue.put(item) 写入队列,timeout等待时间
Queue.put_nowait(item) 相当Queue.put(item, False)
Queue.task_done() 在完成一项工作之后,Queue.task_done() 函数向任务已经完成的队列发送一个信号
Queue.join() 实际上意味着等到队列为空,再执行别的操作
生产消费者模型
在并发编程中使用生产者和消费者模式能够解决绝大多数并发问题。该模式通过平衡生产线程和消费线程的工作能力来提高程序的整体处理数据的速度。
为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决这个问题于是引入了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
生产者消费模型例子
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("李闯")
import queue,threading import time,random 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()
进程
进程的使用方法与线程几乎一致,注意:由于进程之间的数据需要各自持有一份,所以创建进程需要的非常大的开销。
from multiprocessing import Process #导入进程模块
import time
def task(args):
time.sleep(args)
print(args)
if __name__ == '__main__': #window必须加这条,不然会报错!
for i in range(10):
p = Process(target=task , args=(i,)) #创建进程
p.daemon = False
p.start()
p.join(1)
print("主进程最后。。。。。")
进程也有进程锁,进程锁与线程锁有点不一样,由于每个进程都共享屏幕输出,加上进程锁后输出的效果不会变乱,与线程锁使用方法一致。这里可以参考线程锁
进程间的通讯
首先线程的资源可以共享:
from threading import Thread def task(num,li): li.append(num) print(li) if __name__ == '__main__': v = [] for i in range(10): p = Thread(target=task,args=(i,v)) p.start() #运行结果 [0] [0, 1] [0, 1, 2] [0, 1, 2, 3] [0, 1, 2, 3, 4] [0, 1, 2, 3, 4, 5] [0, 1, 2, 3, 4, 5, 6] [0, 1, 2, 3, 4, 5, 6, 7] [0, 1, 2, 3, 4, 5, 6, 7, 8] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
由此可以看出,数据是共享的,因为列表一直在增加。d
那换为进程结果如何?
from multiprocessing import Process def task(num,li): li.append(num) print(li) if __name__ == '__main__': v = [] for i in range(10): p = Process(target=task,args=(i,v)) p.start() #运行结果 [0] [1] [2] [3] [4] [5] [6] [7] [8] [9]
说明:不同进程间内存是不共享的,要想实现两个进程间的数据交换,可以用以下方
第一种方法:
Queues
from threading import Thread from multiprocessing import Process,Queue def task(qq): qq.put(['hello','python','!!!']) if __name__ == '__main__': q = Queue() p = Process(target=task,args=(q,)) p.start() print(q.get()) p.join() #运行结果 ['hello', 'python', '!!!'] #说明父进程可以读取到子进程的数据
第二种方法:
Pipes
from threading import Thread from multiprocessing import Process,Queue,Pipe def f(conn): conn.send(['hello','python','!!!']) conn.close() if __name__ == '__main__': parent_cnn,child_cnn = Pipe() p = Process(target=f,args=(child_cnn,)) p.start() print(parent_cnn.recv()) #打印父进程接收到的数据 p.join() #运行结果 ['hello', 'python', '!!!'] #父进程可以接收到子进程发送过来的数据,同理子进程也可以接收父进程的
以上两种方法都是实现的进程之间的传送
第三种方法:
Managers
from threading import Thread from multiprocessing import Process,Queue,Pipe,Manager import os def f(d,l): d[os.getpid()] = os.getpid() l.append(os.getpid()) print(l) if __name__ == '__main__': with Manager() as manager: d = manager.dict() l = manager.list(range(5)) p_list = [] for i in range(10): p = Process(target=f,args=(d,l)) p.start() p_list.append(p) for res in p_list: res.join() print(d) print(l) #运行结果 [0, 1, 2, 3, 4, 87724] [0, 1, 2, 3, 4, 87724, 87725] [0, 1, 2, 3, 4, 87724, 87725, 87726] [0, 1, 2, 3, 4, 87724, 87725, 87726, 87727] [0, 1, 2, 3, 4, 87724, 87725, 87726, 87727, 87728] [0, 1, 2, 3, 4, 87724, 87725, 87726, 87727, 87728, 87729] [0, 1, 2, 3, 4, 87724, 87725, 87726, 87727, 87728, 87729, 87730] [0, 1, 2, 3, 4, 87724, 87725, 87726, 87727, 87728, 87729, 87730, 87731] [0, 1, 2, 3, 4, 87724, 87725, 87726, 87727, 87728, 87729, 87730, 87731, 87732] [0, 1, 2, 3, 4, 87724, 87725, 87726, 87727, 87728, 87729, 87730, 87731, 87732, 87733] {87728: 87728, 87729: 87729, 87730: 87730, 87731: 87731, 87732: 87732, 87733: 87733, 87724: 87724, 87725: 87725, 87726: 87726, 87727: 87727} [0, 1, 2, 3, 4, 87724, 87725, 87726, 87727, 87728, 87729, 87730, 87731, 87732, 87733] #文件实现了共享修改
#这个是默认枷锁了,不需要再另外添加
进程之间共享数据,实际上是子进程的数据通过pickle然后克隆了一份给其他进程
进程池
起的进程多了,有可能会把系统高瘫痪,所以用到进程池,一般没有线程池,不过也可以自己写一个
进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。
进程池中有两个方法:
- apply
- apply_async
#!/usr/bin/env python
# -*- coding:utf-8 -*-
from multiprocessing import Process,Pool
import time
def Foo(i):
time.sleep(2)
return i+100
def Bar(arg):
print arg
pool = Pool(5)
#print pool.apply(Foo,(1,))
#print pool.apply_async(func =Foo, args=(1,)).get()
for i in range(10):
pool.apply_async(func=Foo, args=(i,),callback=Bar) #回调函数,当执行完毕后运行这个函数
print 'end'
pool.close()
pool.join()#进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。
协程
线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作则是程序员。
协程,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。
协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序。
协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程;
协程的好处:
- 无需线程上下文切换的开销
- 无需原子操作锁定及同步的开销
- "原子操作(atomic operation)是不需要synchronized",所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序是不可以被打乱,或者切割掉只执行部分。视作整体是原子性的核心。
- 方便切换控制流,简化编程模型
- 高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。
缺点:
- 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
- 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序
什么条件就能称之为协程?
- 必须在只有一个单线程里实现并发
- 修改共享数据不需加锁
- 用户程序里自己保存多个控制流的上下文栈
- 一个协程遇到IO操作自动切换到其它协程
Greenlet
greenlet是一个用C实现的协程模块,相比与python自带的yield,它可以使你在任意函数之间随意切换,而不需把这个函数先声明为generator
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() #运行结果: 12 56 34 78
感觉确实用着比generator还简单了呢,但好像还没有解决一个问题,就是遇到IO操作,自动切换,相当于手动挡
Gevent
Gevent 是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。
from greenlet import greenlet import gevent def func1(): print('\033[31;1m李闯在跟海涛搞...\033[0m') gevent.sleep(2) #模拟IO print('\033[31;1m李闯又回去跟继续跟海涛搞...\033[0m') def func2(): print('\033[32;1m李闯切换到了跟海龙搞...\033[0m') gevent.sleep(1) print('\033[32;1m李闯搞完了海涛,回来继续跟海龙搞...\033[0m') def func3(): print('Iam 3') gevent.sleep(0) print('welcome back') gevent.joinall([ gevent.spawn(func1), gevent.spawn(func2), gevent.spawn(func3), ]) #运行结果 李闯在跟海涛搞... 李闯切换到了跟海龙搞... Iam 3 welcome back 李闯搞完了海涛,回来继续跟海龙搞... 李闯又回去跟继续跟海涛搞... #调度的循序,func1 - func2 -fun3,然后回头查看func1/func2发现还在调度中,所以由回到了func3这里。总共耗时2s
import gevent def task(pid): """ Some non-deterministic task """ gevent.sleep(0.5) print('Task %s done' % pid) def synchronous(): for i in range(1,10): task(i) def asynchronous(): threads = [gevent.spawn(task, i) for i in range(10)] #向调度的函数中添加参数 gevent.joinall(threads) print('Synchronous:') synchronous() print('Asynchronous:') asynchronous() #运行后便可以看到结果的区别
上面程序的重要部分是将task函数封装到Greenlet内部线程的gevent.spawn
。 初始化的greenlet列表存放在数组threads
中,此数组被传给gevent.joinall
函数,后者阻塞当前流程,并执行所有给定的greenlet。执行流程只会在 所有greenlet执行完后才会继续向下走。
遇到IO阻塞时会自动切换至其他任务
from gevent import monkey monkey.patch_all() #在爬虫、socket中必须写上这段gevent才会势必到他们在做IO操作 import gevent from urllib.request import urlopen def f(url): print('GET: %s' % url) resp = urlopen(url) data = resp.read() print('%d bytes received from %s.' % (len(data), url)) gevent.joinall([ gevent.spawn(f, 'https://www.python.org/'), #函数 + 参数的形式 gevent.spawn(f, 'https://www.yahoo.com/'), gevent.spawn(f, 'https://github.com/'), ])
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(9995)
import socket cli = socket.socket() cli.connect(('127.0.0.1',9995)) while True: data = input('input >>:') cli.send(data.encode('utf-8')) msg = cli.recv(1024) print(msg.decode('utf-8')) print(repr(data)) cli.close()