python进阶:Python进程、线程、队列、生产者/消费者模式、协程

一、进程和线程的基本理解

1、进程

程序是由指令和数据组成的,编译为二进制格式后在硬盘存储,程序启动的过程是将二进制数据加载进内存,这个启动了的程序就称作进程(可简单理解为进行中的程序)。例如打开一个QQ,word文档等程序,就会在内存中生成相应的进程,当然如上两种比较复杂的程序启动后由于有多个任务要进行处理,比如word在文字输入的同时还会进行拼写检查等,所以会由主进程fork为多个子进程,每个进程内可能还会由主线程生成有多个子线程。

那么操作系统如何并发执行多任务呢? 在以前的单核CPU时代,单个CPU会在多个进程间快速切换,比QQ进程执行了0.1秒,在word进程执行了0.2秒的时间分片,这种切换就是所谓的上下文切换,由于这种切换非常快,所以我们感觉是多个任务并行执行的,其实单核CPU是不可能实现真正的并行任务处理的。只有在多核CPU情况下,才可能实现真正的并行任务。(注意,只是可能,当并发任务数高于CPU核数(其实我们日常使用多数都是这种情况),多个CPU也是进行更复杂的上下文切换的。

2、线程

线程是最小的任务执行单元,此前博客中的Python代码都是单进程单线程的,每个进程至少有一个线程。

那么在多核CPU上想执行多个任务怎么办? 有如下两种解决方案:

1)多进程模式

2)多线程模式

当然也可以1)和2)并用,或者利用后面讲到的协程模式。

另外Python中由于GIL(全局解释器锁)的存在,是无法实现真正的多线程并行执行的,这也是Python多线程广为诟病的一点。本文后面聊到多线程的时候会展开解释。

小结:

进程之间由于独立开辟内存空间,所以数据是无法共享的,而是要通过IPC等方式进行进程间通信。

同一个进程内的多个线程之间是共享数据的,但是可能会产生dirty data,所以有了GIL的存在。

多任务间往往并不是独立的,而是可能有顺序和通信的需求,多进程、多线程编程有时是很复杂的,而且对于Python来说,多线程较为复杂,要尽量避免使用,而是通过进程池而得的多进程或协程来实现多任务并行。

二、多进程

优点:可以同时利用多个CPU。

缺点:每个进程开辟独立的内存空间,耗费资源。

场景:适用于计算密集型任务(相对于IO密集型任务,计算密集型任务对CPU占用较多。)

进程并不是越多越好的,大体上某任务的进程数量和多核CPU的数量相同为宜。

1、创建多进程

import multiprocessingimport osdef f1(a1):    print(a1)if __name__=="__main__":    print('Parent process %s.' % os.getpid())    t=multiprocessing.Process(target=f1,args=(11,))    #t.daemon = True #主进程不等待子进程    t.start()    t2=multiprocessing.Process(target=f1,args=(11,))    t2.start()    print('end')

代码如上,利用multiprocessing模块创建了两个进程,代码非常简单。

需要注意的是windows由于没有进程fork函数,不支持os.fork(),(与之相似的只有create process函数),所以上述代码块必须得在if __name__=="__main__":下执行才能成功。

t.daemon和t.join()都可以用来设置主进程是否等待子进程执行完再往下进行,通常用于进程间的同步。(一般情况下都要等待,否则子进程可能还没处理完任务就被主进程关闭)。

2、进程池

实际工作中需要创建多个进程一般都用进程池。(Python中自带了进程池,但没有线程池,需要要自己写,后面有示例。)

进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进进程,那么程序就会等待,直到进程池中有可用进程为止。

Python中利用Pool来创建进程池。

from multiprocessing import Pool
import time
def Foo(i):
    #time.sleep(1)
    print(i)
    return i+100

def Bar(arg):
    print (arg)

#print pool.apply(Foo,(1,))
#print pool.apply_async(func =Foo, args=(1,)).get()
if __name__ == '__main__':
    pool = Pool(5)
    for i in range(10):
        pool.apply_async(func=Foo, args=(i,),callback=Bar)
    pool.close()
    pool.join()#进程池中进程执行完毕后再关闭,如果注释,那么程序直接关闭。#join方法里有断言(assert,所以不先close或terminate就会报错)

注意apply和apply_async的区别,后者包含了callback回调函数。 p = Pool(5)#创建5个进程池

p.apply #每一个任务是排队进行 ;每一个进程都有一个join方法
p.apply_async #每一个任务都并发执行,而且可以设置回调函数;进程无join方法,主进程没有等子进程,进程的daemon=True

再来看如下代码示例,进一步理解t.join()的功能:(代码转自廖雪峰Python教程)

def long_time_task(name):
    print('Run task %s (%s)...' % (name, os.getpid()))
    start = time.time()
    time.sleep(random.random() * 3)
    end = time.time()
    print('Task %s runs %0.2f seconds.' % (name, (end - start)))

if __name__=='__main__':
    print('Parent process %s.' % os.getpid())
    p = Pool(4)
    for i in range(5):
        p.apply_async(long_time_task, args=(i,))
    print('Waiting for all subprocesses done...')
    p.close()
    p.join()
    print('All subprocesses done.')

代码输出结果:

Parent process 15284.
Waiting for all subprocesses done...
Run task 0 (4420)...
Run task 1 (540)...
Run task 2 (6188)...
Run task 3 (12060)...
Task 1 runs 0.13 seconds.
Run task 4 (540)...
Task 0 runs 0.62 seconds.
Task 2 runs 0.35 seconds.
Task 3 runs 0.93 seconds.
Task 4 runs 1.93 seconds.
All subprocesses done.

可以看到task4等待其他任务都执行完了再执行。因为Pool的值是4,所以只能同时执行4个任务,第五个任务要等待其前某个任务执行完了空出来进程了再执行。所以上面改成pool(5)就看不到等待的任务了。另外pool的默认值等于你当前电脑的cpu数。

3、进程间共享数据

如前所述,进程间的数据是隔离的。

from multiprocessing import Process
from multiprocessing import Manager
import time
li = []
def foo(i):
    li.append(i)
    print ('say hi',li)
if __name__ == '__main__':
    for i in range(10):
        p = Process(target=foo,args=(i,))
        p.start()
    print ('ending',li)

输出结果:

ending []
say hi [0]
say hi [1]
say hi [2]
say hi [4]
...

可以看到每个线程的数据都是独立的。

如果想让进程间数据共享有两种方式:(例如上面li列表有10个数据)

#方法1 Manager数据类型:
import os
from multiprocessing import Process
from multiprocessing import Manager
def foo(i,dic):
    dic[i]=100+i
    print(dic.values())
if __name__ == '__main__':
    manage =Manager()#
    dic = manage.dict()#为啥这俩放在外面就会报错  因为这是windows!~—~
    #dic = {}
    for i in range(2):
        print('Parent process %s.' % os.getpid())
        p=Process(target=foo,args=(i,dic))
        print('Parent process %s.' % os.getpid())
        p.start()
        p.join()
#方法二,Array数据类型
from multiprocessing import Process,Array
temp = Array('i', [11,22,33,44])

def Foo(i):
    temp[i] = 100+i
    for item in temp:
        print (i,'----->',item)

for i in range(2):
    p = Process(target=Foo,args=(i,))
    p.start()

4、进程间通信

进程间有时不但需要共享数据,还需要通信,操作系统提供了多种进程间通信机制,

Python的multiprocessing模块包装了底层的机制,提供了QueuePipes等多种方式来交换数据。

我们以Queue为例,在父进程中创建两个子进程,一个往Queue里写数据,一个从Queue里读数据:

from multiprocessing import Process, Queue
import os, time, random

# 写数据进程执行的代码:
def write(q):
    print('Process to write: %s' % os.getpid())
    for value in ['A', 'B', 'C']:
        print('Put %s to queue...' % value)
        q.put(value)
        time.sleep(random.random())

# 读数据进程执行的代码:
def read(q):
    print('Process to read: %s' % os.getpid())
    while True:
        value = q.get(True)
        print('Get %s from queue.' % value)

if __name__=='__main__':
    # 父进程创建Queue,并传给各个子进程:
    q = Queue()
    pw = Process(target=write, args=(q,))
    pr = Process(target=read, args=(q,))
    # 启动子进程pw,写入:
    pw.start()
    # 启动子进程pr,读取:
    pr.start()
    # 等待pw结束:
    pw.join()
    # pr进程里是死循环,无法等待其结束,只能强行终止:
    pr.terminate()

在Unix/Linux下,multiprocessing模块封装了fork()调用,使我们不需要关注fork()的细节。由于Windows没有fork调用,因此,multiprocessing需要“模拟”出fork的效果,父进程所有Python对象都必须通过pickle序列化再传到子进程去,所以,如果multiprocessing在Windows下调用失败了,要先考虑是不是pickle失败了。

总结:

在Unix/Linux下,可以使用fork()调用实现多进程。

要实现跨平台的多进程,可以使用multiprocessing模块。

进程间通信是通过QueuePipes等实现的。

三、多线程

多任务可以由多进程完成,也可以由一个进程内的多个线程完成。

Python的标准库提供了两个支持多线程的模式:_thread(3.0之前是thread,3.0后改为_thread) 和threading模块,threading模块提供了更高级的机制,所以一般都用threading模块。

1. 创建多线程

创建线程很简单,就是用threading实例传入函数。

import time, threading

# 新线程执行的代码:
def loop():
    print('thread %s is running...' % threading.current_thread().name)
    n = 0
    while n < 5:
        n = n + 1
        print('thread %s >>> %s' % (threading.current_thread().name, n))
        time.sleep(1)
    print('thread %s ended.' % threading.current_thread().name)

print('thread %s is running...' % threading.current_thread().name)
t = threading.Thread(target=loop, name='LoopThread')
t.start()
t.join()
print('thread %s ended.' % threading.current_thread().name)

上面的代码可以利用循环创建多个threading实例执行多个任务就实现了多线程。

import threading
import time

def show(arg):
    time.sleep(1)
    print( 'thread'+str(arg))

for i in range(10):
    t = threading.Thread(target=show, args=(i,))
    t.start()

print( 'main thread stop')

2. 线程锁

由于同一个进程内的多线程之间是数据共享的,而不像多进程间那样数据隔离,所以多个线程修改同一份数据,很可能会造成脏数据,因此就有了线程锁RLOCK。

此处要注意线程锁和GIL全局解释器锁的区别。 GIL是用来控制同时只有一个线程运行的,就像单核CPU的多进程那样。更多解释可以参见Python核心编程第三版中关于GIL的解释。而RLOCK是用来锁数据的。

线程锁代码示例:

import threading
import time
global_num = 0
lock = threading.RLock()

def Func():
    lock.acquire()#获得锁
    global global_num
    global_num+=1
    lock.release() #注意给线程解锁的位置
    time.sleep(1)
    print(global_num)

for i in range(10):
    t = threading.Thread(target=Func)
    t.start()

利用线程锁还要必要死锁的出现。

当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。

获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try...finally来确保锁一定会被释放。

balance = 0
lock = threading.Lock()

def run_thread(n):
    for i in range(100000):
        # 先要获取锁:
        lock.acquire()
        try:
            # 放心地改吧:
            change_it(n)
        finally:
            # 改完了一定要释放锁:
            lock.release()

锁的好处就是确保了某段关键代码只能由一个线程从头到尾完整地执行,坏处当然也很多,首先是阻止了多线程并发执行,包含锁的某段代码实际上只能以单线程模式执行,效率就大大地下降了。其次,由于可以存在多个锁,不同的线程持有不同的锁,并试图获取对方持有的锁时,可能会造成死锁,导致多个线程全部挂起,既不能执行,也无法结束,只能靠操作系统强制终止。

threading.event

import threading
def do(test):
    print('start 10 公里')
    test.wait()
    print('continue')

event_obj = threading.Event()

for i in range(10):
    t = threading.Thread(target=do, args=(event_obj,))
    t.start()

event_obj.clear()
Flag = input('请输入:')
if Flag == 'True':
    event_obj.set()

3.自定义线程池

Python中自带了进程池,但没有线程池,所以需要自己写。

利用pool实现的简单版线程池:

python进阶:Python进程、线程、队列、生产者/消费者模式、协程python进阶:Python进程、线程、队列、生产者/消费者模式、协程
#创建max_num个queue,再把threading.Thread类放入执行一个任务从queue中取走一个,然后再add一个。
import queue
import threading
class ThreadPool():

    def __init__(self, max_num=20):
        self.queue = queue.Queue(max_num)
        for i in range(max_num):
            self.queue.put(threading.Thread)

    def get_thread(self):
        return self.queue.get()

    def add_thread(self):
        self.queue.put(threading.Thread)

pool = ThreadPool(10)
def func(arg, p):
    print (arg)
    import time
    time.sleep(2)
    p.add_thread()

for i in range(30):
    thread = pool.get_thread()  #相当于threading.Thread
    t = thread(target=func, args=(i, pool))
    t.start()

简单版线程池

python进阶:Python进程、线程、队列、生产者/消费者模式、协程python进阶:Python进程、线程、队列、生产者/消费者模式、协程
import queue
import threading
import contextlib
import time

StopEvent = object()

class ThreadPool(object):

    def __init__(self, max_num, max_task_num = None):
        if max_task_num:
            self.q = queue.Queue(max_task_num)
        else:
            self.q = queue.Queue()
        # 线程池最大容量
        self.max_num = max_num
        self.cancel = False
        self.terminal = False
        # 真实创建的线程列表
        self.generate_list = []
        # 空闲的线程列表
        self.free_list = []

    def run(self, func, args, callback=None):
        """
        线程池执行一个任务
        :param func: 任务函数
        :param args: 任务函数所需参数
        :param callback: 任务执行失败或成功后执行的回调函数,回调函数有两个参数1、任务函数执行状态;2、任务函数返回值(默认为None,即:不执行回调函数)
        :return: 如果线程池已经终止,则返回True否则None
        """
        if self.cancel:
            return
        if len(self.free_list) == 0 and len(self.generate_list) < self.max_num:
            self.generate_thread()
        w = (func, args, callback,)
        self.q.put(w)

    def generate_thread(self):
        """
        创建一个线程
        """
        t = threading.Thread(target=self.call)
        t.start()

    def call(self):
        """
        循环去获取任务函数并执行任务函数
        """
        # 获取当前线程
        current_thread = threading.currentThread()
        self.generate_list.append(current_thread)

        event = self.q.get()
        while event != StopEvent:
            # 是元组是任务
            # 解开任务包
            # 执行任务

            func, arguments, callback = event

            try:
                result = func(*arguments)
                status = True
            except Exception as e:
                status = False
                result = e

            if callback is not None:
                try:
                    callback(status, result)
                except Exception as e:
                    pass

            with self.worker_state(self.free_list, current_thread):
                if self.terminal:
                    event = StopEvent
                else:
                    event = self.q.get()
                    #优化前是以下三句代码
                    # self.free_list.append(current_thread)
                    # event =  self.q.get()
                    # self.free_list.remove(current_thread)
        else:

            self.generate_list.remove(current_thread)

    def close(self):
        """
        执行完所有的任务后,所有线程停止
        """
        self.cancel = True
        full_size = len(self.generate_list)
        while full_size:
            self.q.put(StopEvent)
            full_size -= 1

    def terminate(self):
        """
        无论是否还有任务,终止线程
        """
        self.terminal = True

        while self.generate_list:
            self.q.put(StopEvent)

        self.q.queue.clear()  # 清空队列? self.q.empty()

    @contextlib.contextmanager
    def worker_state(self, state_list, worker_thread):
        """
        用于记录线程中正在等待的线程数
        """
        state_list.append(worker_thread)
        try:
            yield
        finally:
            state_list.remove(worker_thread)

# How to use

pool = ThreadPool(5)

def callback(status, result):
    # status, execute action status
    # result, execute action return value
    pass

def action(i):
    print(i)

for i in range(30):
    ret = pool.run(action, (i,), callback)

time.sleep(5)
print(len(pool.generate_list), len(pool.free_list))
print(len(pool.generate_list), len(pool.free_list))
pool.close()
# pool.terminate()

高级版线程池

四、生产者、消费者模型

商品或服务的生产者生成商品,然后将其放到类似队列的数据结构中。生产者生产商品的时间是不确定的,消费者消费产品的时间也是不确定的。生产者消费者对立且并发的执行线程。

这种模型实际上是利用了Python的queue模块,提供线程间的通信。上面线程池的实现其实就是用了这种模型。

python进阶:Python进程、线程、队列、生产者/消费者模式、协程python进阶:Python进程、线程、队列、生产者/消费者模式、协程
import queue
import threading
class ThreadPool():

    def __init__(self, max_num=20):
        self.queue = queue.Queue(max_num)
        for i in range(max_num):
            self.queue.put(threading.Thread)

    def get_thread(self):
        return self.queue.get()

    def add_thread(self):
        self.queue.put(threading.Thread)

pool = ThreadPool(10)
def func(arg, p):
    print (arg)
    import time
    time.sleep(2)
    p.add_thread()

for i in range(30):
    thread = pool.get_thread()  #相当于threading.Thread
    t = thread(target=func, args=(i, pool))
    t.start()

queue

五、协程

线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作则是程序员。

协程存在的意义:对于多线程应用,CPU通过切片的方式来切换线程间的执行,线程切换时需要耗时(保存状态,下次继续)。协程,则只使用一个线程,在一个线程中规定某个代码块执行顺序。

协程的适用场景:当程序中存在大量不需要CPU的操作时(IO),适用于协程;

协程是在一个线程执行过程中可以在一个子程序的预定或者随机位置中断,然后转而执行别的子程序,在适当的时候再返回来接着执行。他本身是一种特殊的子程序或者称作函数。

应用:协程基于generator,Python3中内置了异步IO。遇到IO密集型的业务时,总是很费时间啦,多线程加上协程,你磁盘在那该读读该写写,我还能去干点别的。在WEB应用中效果尤为明显。

在“发出IO请求”到收到“IO完成”的这段时间里,同步IO模型下,主线程只能挂起,但异步IO模型下,主线程并没有休息,而是在消息循环中继续处理其他消息。这样,在异步IO模型下,一个线程就可以同时处理多个IO请求,并且没有切换线程的操作。对于大多数IO密集型的应用程序,使用异步IO将大大提升系统的多任务处理能力。

那和多线程比,协程有何优势?

最大的优势就是协程极高的执行效率。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

第二大优势就是不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多。

因为协程是一个线程执行,那怎么利用多核CPU呢?最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

Python对协程的支持是通过generator实现的。

在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。

但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。

Python中的协程经历了很长的一段发展历程。其大概经历了如下三个阶段:

  1. 最初的生成器变形yield/send
  2. Python3.4引入@asyncio.coroutine和yield from
  3. 在最近的Python3.5版本中引入async/await关键字

如下两个博客已经写的非常明确了:

http://blog.csdn.net/soonfly/article/details/78361819

http://python.jobbole.com/86069/

https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001432090954004980bd351f2cd4cc18c9e6c06d855c498000

import asyncio

async def wget(host):
    print('wget %s...' % host)
    connect = asyncio.open_connection(host, 80)
    reader, writer = await connect
    header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
    writer.write(header.encode('utf-8'))
    await writer.drain()
    while True:
        line = await reader.readline()
        if line == b'\r\n':
            break
        print('%s header > %s' % (host, line.decode('utf-8').rstrip()))
    # Ignore the body, close the socket
    writer.close()

loop = asyncio.get_event_loop()
tasks = [wget(host) for host in ['www.sina.com.cn', 'www.sohu.com', 'www.iqiyi.com', 'www.youku.com']]
loop.run_until_complete(asyncio.wait(tasks))
loop.close()

协程是异步I/O的实现方式。

Python2.7中的实现方式:

python进阶:Python进程、线程、队列、生产者/消费者模式、协程python进阶:Python进程、线程、队列、生产者/消费者模式、协程
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()

greenlet

greenlet

python进阶:Python进程、线程、队列、生产者/消费者模式、协程python进阶:Python进程、线程、队列、生产者/消费者模式、协程
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),
])

gevent

gevent

python进阶:Python进程、线程、队列、生产者/消费者模式、协程python进阶:Python进程、线程、队列、生产者/消费者模式、协程
from gevent import monkey; monkey.patch_all()
import gevent
import urllib2

def f(url):
    print('GET: %s' % url)
    resp = urllib2.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/'),
])

gevent 遇到IO操作自动切换

上一篇:Alibaba Cluster Data 开放下载:270GB 数据揭秘你不知道的阿里巴巴数据中心


下一篇:DOM hash