Python中线程与进程

一. 概念

进程

计算机程序是磁盘中可执行的二进制(或其他类型)的数据,只有在被读取到内存中,被操作系统调用的时候才开始其生命周期。进程是程序的一次执行。每个进程都有自己的地址空间,内存,数据及其它记录其运轨迹的辅助数据。操作系统管理再起上运行的所有进程,并为这些进程公平分配时间,进程也可以通过fork和spwan操作来完成其他的任务。不过各个进程有自己的内存空间,数据栈等,所以只能使用进程间通信(IPC),不可以直接共享信息。

线程

线程与进程有些相似,不同的是,所有的线程运行在同一个进程中,共享相同的运行环境。线程一般是并发执行的,由于这种并行和数据共享机制使得多个任务的合作变为可能。
在单cpu中,真正的并发是不可能的,每个进程会被安排成每次运行一小会儿,然后让出cpu,让其他线程运行。

进程与线程的关系

进程和线程的关系:

  • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
  • 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
  • CPU分给线程,即真正在CPU上运行的是线程。

并行与并发

并行处理(Parallel Processing)是计算机系统中能同时执行两个或更多个处理的一种计算方法。并行处理可同时工作于同一程序的不同方面。并行处理的主要目的是节省大型和复杂问题的解决时间。并发处理(concurrency Processing):指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机(CPU)上运行,但任一个时刻点上只有一个程序在处理机(CPU)上运行

并发的关键是有处理多个任务的能力,不一定要同时。
并行的关键是有同时处理多个任务的能力。

同步与异步

在计算机领域,同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;
异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统会通知进程进行处理,这样可以提高执行的效率。

二. Python全局解释器锁(GIL)

对Python虚拟机的访问有GIL来控制,这个锁保证同一时刻只有一个线程在运行。在多线程环境中,Python虚拟机按以下方式执行:

  • 设置GIL
  • 切换到一个线程中运行
  • 运行
    • 指定数量的字节码指令,或者
    • 线程主动让出控制
  • 把线程设置为睡眠状态
  • 解锁GIL
  • 再次重复以上所有步骤

无论启多少个线程,有多少个cpu, Python在执行一个进程的时候在同一时刻只允许一个线程运行。
所以,python是无法利用多核CPU实现多线程的。
这样,python对于计算密集型的任务开多线程的效率甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。

三. threading模块

threading类直接创建

import threading
import time

def countNum(n): # 定义某个线程要运行的函数

    print("running on number:%s" %n)

    time.sleep(3)

if __name__ == '__main__':

    t1 = threading.Thread(target=countNum,args=(23,)) #生成一个线程实例
    t2 = threading.Thread(target=countNum,args=(34,))

    t1.start() #启动线程
    t2.start()

    print("ending!")

thread类继承方式创建

#继承Thread式创建

import threading
import time

class MyThread(threading.Thread):

    def __init__(self,num):
        threading.Thread.__init__(self)
        self.num=num

    def run(self):
        print("running on number:%s" %self.num)
        time.sleep(3)

t1=MyThread(56)
t2=MyThread(78)

t1.start()
t2.start()
print("ending")

join 和 setDaemon

# join():在子线程完成运行之前,这个子线程的父线程将一直被阻塞。

# setDaemon(True):
        '''
         将线程声明为守护线程,必须在start() 方法调用之前设置,如果不设置为守护线程程序会被无限挂起。

         当我们在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成

         想退出时,会检验子线程是否完成。如果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是只要主线程

         完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法啦'''


import threading
from time import ctime,sleep
import time

def Music(name):

        print ("Begin listening to {name}. {time}".format(name=name,time=ctime()))
        sleep(3)
        print("end listening {time}".format(time=ctime()))

def Blog(title):

        print ("Begin recording the {title}. {time}".format(title=title,time=ctime()))
        sleep(5)
        print('end recording {time}'.format(time=ctime()))


threads = []


t1 = threading.Thread(target=Music,args=('FILL ME',))
t2 = threading.Thread(target=Blog,args=('',))

threads.append(t1)
threads.append(t2)

if __name__ == '__main__':

    #t2.setDaemon(True)

    for t in threads:

        #t.setDaemon(True) #注意:一定在start之前设置
        t.start()

        #t.join()

    #t1.join()
    #t2.join()    #  考虑这三种join位置下的结果?

    print ("all over %s" %ctime())

四. 锁

锁通常被用来实现对共享资源的同步访问。为每一个共享资源创建一个Lock对象,当你需要访问该资源时,调用acquire方法来获取锁对象(如果其它线程已经获得了该锁,则当前线程需等待其被释放),待资源访问完后,再调用release方法释放锁:

import time
import threading

def addNum():
    global num #在每个线程中都获取这个全局变量
    #num-=1

    temp=num
    time.sleep(0.1)
    num =temp-1  # 对此公共变量进行-1操作

num = 100  #设定一个共享变量

thread_list = []

for i in range(100):
    t = threading.Thread(target=addNum)
    t.start()
    thread_list.append(t)

for t in thread_list: #等待所有线程执行完毕
    t.join()

print('Result: ', num)

上述的程序每次运行都会产生不同的结果:

  1. 注释掉time.sleep(1) 结果唯一,为0
  2. time.sleep(1) 结果唯一,为99
  3. time.sleep(0.1) 结果不确定
    第一种情况,sleep语句上下两句的间隔时间为0,小于进程间的切换时间,线程切换后能够拿到上一个线程计算后的结果,即每次都可以完成减一操作
    第二种情况,sleep语句上下两句的间隔时间为1,大于100个线程的切换时间,在切换的过程中每个线程在执行sleep函数,那么每一个线程拿到的num都是100,每个线程得到的结果都是99,最后得到的num就是99
    sleep语句上下两句的间隔时间大于0但不足以完成100个线程的切换,开始执行的线程完成了减一操作,剩下的线程拿到的num就不是100了,最后得到的结果也就不确定了。
    如何保证计算结果的正确性
    使用本节开始提到的锁,可以保证在计算减一操作的过程中单线程执行,不执行其他线程,在解锁之后在进行线程调度。加锁方式如下所示
# -*- coding: utf-8 -*-

import time
import threading

lock = threading.Lock()


def subNum():
    global num

    lock.acquire()

    temp = num
    time.sleep(0.01)
    num = temp - 1

    lock.release()

num = 100

thread_list = []

for i in range(100):
    t = threading.Thread(target=subNum)

    t.start()
    thread_list.append(t)

for t in thread_list:
    t.join()

print('Result: ', num)

五. 递归锁

import threading
import time

A = threading.Lock()
B = threading.Lock()


class myThread(threading.Thread):

    def actionA(self):
        A.acquire()
        print(self.name, 'got A from actionA', time.time())
        time.sleep(2)

        B.acquire()
        print(self.name, 'got B from actionA', time.time())
        time.sleep(1)

        B.release()
        A.release()

    def actionB(self):
        B.acquire()
        print(self.name, 'got B from actionB', time.time())
        time.sleep(2)

        A.acquire()
        print(self.name, 'got A from actionB', time.time())
        time.sleep(1)

        A.release()
        B.release()

    def run(self):
        self.actionA()
        self.actionB()

if __name__ == '__main__':
    L = []

    for i in range(5):
        t = myThread()
        t.start()
        L.append(t)

    for i in L:
        i.join()

    print('end...')

"""
5个线程有一个线程T-1先拿到A这把锁,在有一个线程先拿到A这把锁后,其他四个线程只能等着,一直等到A这把锁释放掉。 
T-1继续执行拿到B锁,然后T-1释放B锁,再释放A锁 
第二个线程T-2获得actionA中的A锁,T-1向下执行获得actionB的B锁,
继续向下执行
    T-2已经获得actionA中的A锁,T-2试图获得actionA中的B锁,
    T-1已经获得actionB中的B锁,T-1试图获得actionB中的A锁,
T-2不释放A锁,T-1不释放B锁,造成死锁
"""

使用递归锁可以解决死锁的问题,将上述代码中的锁换成递归锁

import threading
import time

rLock = threading.RLock()


class myThread(threading.Thread):

    def actionA(self):
        rLock.acquire()
        print(self.name, 'got A from actionA', time.time())
        time.sleep(2)

        rLock.acquire()
        print(self.name, 'got B from actionA', time.time())
        time.sleep(1)

        rLock.release()
        rLock.release()

    def actionB(self):
        rLock.acquire()
        print(self.name, 'got B from actionB', time.time())
        time.sleep(2)

        rLock.acquire()
        print(self.name, 'got A from actionB', time.time())
        time.sleep(1)

        rLock.release()
        rLock.release()

    def run(self):
        self.actionA()
        self.actionB()

if __name__ == '__main__':
    L = []

    for i in range(5):
        t = myThread()
        t.start()
        L.append(t)

    for i in L:
        i.join()

    print('end...')

六. 同步对象

线程的一个关键特性是每个线程都是独立运行且状态不可预测。
如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就 会变得非常棘手。
为了解决这些问题,我们需要使用threading库中的Event对象。

  • 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。
  • 在初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。
  • 一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。
  • 如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行
    event类中的方法
函数名使用方法
event.isSet():返回event的状态值
event.wait()如果 event.isSet()==False将阻塞线程
event.set()设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度
event.clear()恢复event的状态值为False
# -*- coding: utf-8 -*-
# python version: 
# author:
# date:
# description:


import threading
import time

event = threading.Event()

class Boss(threading.Thread):
    def run(self):
        print('Boss: 今晚大家都要加班到22:00')
        print(event.is_set())
        event.set()
        time.sleep(5)
        print('Boss: <22:00>可以下班了')
        print(event.isSet())
        event.set()

class Worker(threading.Thread):
    def run(self):
        event.wait() # 等待event被设定,一旦event被设定,等同于pass
        print('Worker: holy shit')
        time.sleep(1)
        event.clear() # 清除被设定的event
        event.wait()  # 等待下一次event被设定
        print('worker: nice')

if __name__ == '__main__':
    threads = []
    for i in range(5):
        threads.append(Worker())
    threads.append(Boss())

    for t in threads:
        t.start()

    for t in threads:
        t.join()

>>Boss: 今晚大家都要加班到22:00
False
Worker: holy shit
Worker: holy shit
Worker: holy shit
Worker: holy shit
Worker: holy shit
Boss: <22:00>可以下班了
False
worker: nice
worker: nice
worker: nice
worker: nice
worker: nice

七. 信号量

信号量是用来控制线程并发数的,BoundedSemaphore 或 Semaphore 管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

BoundedSemaphore 与 Semaphore 的唯一区别在于前者在调用release()时检查计数器的值是否超过了计数器的初始值,如果超过了则抛出一个异常

实例:(同时只有5个线程可以获得semaphore,即可以限制最大连接数为5):

import threading
import time


semaphore = threading.Semaphore(5)

class myThread(threading.Thread):
    def run(self):
        if semaphore.acquire():
            print(self.name)
            time.sleep(1)
            semaphore.release()


if __name__ == '__main__':
    threads = []

    for i in range(100):
        threads.append(myThread())

    for t in threads:
        t.start()

可以看出五个一组打印结果

八. 线程队列

创建一个“队列”对象

import queue
q = queue.Queue(maxsize = 10)

queue.Queue类即是一个队列的同步实现。队列长度可为无限或者有限。可通过Queue的构造函数的可选参数maxsize来设定队列长度。如果maxsize小于1就表示队列长度无限。

将一个值放入队列中
q.put(10)
调用队列对象的put()方法在队尾插入一个项目。put()有两个参数,第一个item为必需的,为插入项目的值;第二个block为可选参数,默认为1。如果队列当前为空且block为1,put()方法就使调用线程暂停,直到空出一个数据单元。如果block为0,put方法将引发Full异常。

将一个值从队列中取出
q.get()
调用队列对象的get()方法从队头删除并返回一个项目。可选参数为block,默认为True。如果队列为空且block为True,
get()就使调用线程暂停,直至有项目可用。如果队列为空且block为False,队列将引发Empty异常。

Python Queue模块有三种队列及构造函数:

类型用法
class queue.Queue(maxsize)FIFO,先进先出
class queue.LifoQueue(maxsize)LIFO,即先进后出。
class queue.PriorityQueue(maxsize)优先级队列,级别越低越先出来。

此包中的常用方法(q = queue.Queue()):

函数名功能
q.qsize()返回队列的大小
q.empty()如果队列为空,返回True,反之False
q.full()如果队列满了,返回True,反之False,q.full 与 maxsize 大小对应
q.get([block[, timeout]])获取队列,timeout等待时间
q.get_nowait()相当q.get(False),非阻塞 q.put(item) 写入队列,timeout等待时间
q.put_nowait(item)相当q.put(item, False)
q.task_done()在完成一项工作之后,q.task_done() 函数向任务已经完成的队列发送一个信号
q.join()等到队列为空,再执行别的操作

九. 生产者消费者模型

为什么要使用生产者和消费者模式
在线程世界里,生产者就是生产数据的线程,消费者就是消费数据的线程。在多线程开发当中,如果生产者处理速度很快,而消费者处理速度很慢,那么生产者就必须等待消费者处理完,才能继续生产数据。同样的道理,如果消费者的处理能力大于生产者,那么消费者就必须等待生产者。为了解决生产者和消费者强耦合造成的时间浪费的问题,引入了生产者和消费者模式。
什么是生产者消费者模式
生产者消费者模式是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力
生产者消费者实例

import time
import random
import queue
import threading

q = queue.Queue()


def producer(name):
    count = 1
    while count < 10:
        print('cooking...')
        time.sleep(random.randrange(3))
        q.put(count)
        print('producer %s has cooked %s hamburgers' % (name, count))
        count += 1
        print('ok...')


def consumer(name):
    count = 0
    while count < 10:
        time.sleep(random.randrange(4))
        if not q.empty():
            data = q.get()
            print('\033[32;1mConsumer %s has eaten %s hamburger...\033[0m' % (name, data))
        else:
            print("-----no hamburger anymore----")
        count += 1


p1 = threading.Thread(target=producer, args='A')

c1 = threading.Thread(target=consumer, args='B')
c2 = threading.Thread(target=consumer, args='C')
c3 = threading.Thread(target=consumer, args='D')

p1.start()

c1.start()
c2.start()
c3.start()

使用队列后,消费者与管理者解耦,消费者不管生产者是否生产出产品都可以去队列中取,生产者不管消费者是否取走商品都可以生产产品,两者互不干扰

但还有一种场景需要生产者生产完产品后通知消费者,消费者得到通知再做自己的事,这一需求可以通过task_done 和 join配合完成,task_done用来向队列发送消息通知队列自己的动作以完成,接收方通过join方法得到消息,继续执行下面的操作。没有task_done那么jion一直阻塞,通过下面的例子进说明。

task_done & join

import time
import random
import queue
import threading

q = queue.Queue()

def producer(name):
    count = 1
    while count < 30:
        print('cooking...')
        time.sleep(1)
        q.put(count)
        print('\033[31;1mProducer %s has cooked %s hamburgers\031[0m' % (name, count))
        count += 1
        q.task_done()  # 通知队列消息发送完毕
        print('ok...')


def consumer(name):
    count = 1
    while count < 10:
        q.join()
        data = q.get()

        print('\033[32;1mConsumer %s has eaten %s hamburger...\033[0m' % (name, data))
        count += 1


p1 = threading.Thread(target=producer, args='A')

c1 = threading.Thread(target=consumer, args='B')
c2 = threading.Thread(target=consumer, args='C')
c3 = threading.Thread(target=consumer, args='D')

p1.start()

c1.start()
c2.start()
c3.start()

从结果可以看出,生产者没生产一个货物,消费者消耗一个货物,生产者在生产完成消费者所需货物后把计划生产的货品生产完毕,结束整个程序

十. 多进程调用

由于GIL的存在,python中的多线程其实并不是真正的多线程,如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。

multiprocessing包是Python中的多进程管理包。与threading.Thread类似,它可以利用multiprocessing.Process对象来创建一个进程。该进程可以运行在Python程序内部编写的函数。该Process对象与Thread对象的用法相同,也有start(), run(), join()的方法。此外multiprocessing包中也有Lock/Event/Semaphore/Condition类 (这些对象可以像多线程那样,通过参数传递给各个进程),用以同步进程,其用法与threading包中的同名类一致。所以,multiprocessing的很大一部份与threading使用同一套API,只不过换到了多进程的情境。
下面通过例子说明一下multiprocessing模块的使用方法

from multiprocessing import Process

import time


def f(name):
    time.sleep(1)
    print('hello', name, time.time())


def direct_call_process():
    p_list = []
    for i in range(3):
        p = Process(target=f, args=('process:%s' % i,))
        p_list.append(p)
        p.start()
    for i in p_list:
        p.join()
    print('end')

#--------------------------------------------


class myProcess(Process):
    def __init__(self):
        super(myProcess, self).__init__()

    def run(self):
        print('hello', self.name, time.time())
        time.sleep(1)


def inherit_call_process():
    p_list = []

    for i in range(3):
        p = myProcess()
        p.start()
        p_list.append(p)

    for p in p_list:
        p.join()

    print('end')

if __name__ == '__main__':
    direct_call_process()
    print('------------------------------------')
    inherit_call_process()

上下两种使用方式,打印的时间是几乎一样的,理论上是一样的,但存在打印时间差和cpu本身的误差,会存在一点误差。
结果说明开辟了多个进程。

process类的使用

构造方法:

 Process([group [, target [, name [, args [, kwargs]]]]])
  • group: 线程组,目前还没有实现,库引用中提示必须是None;
  • target: 要执行的方法;
  • name: 进程名;
  • args/kwargs: 要传入方法的参数。

实例方法:

  • is_alive():返回进程是否在运行。
  • join([timeout]):阻塞当前上下文环境的进程程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。
  • start():进程准备就绪,等待CPU调度
  • run():strat()调用run方法,如果实例进程时未制定传入target,这star执行t默认run()方法。
  • terminate():不管任务是否完成,立即停止工作进程 

属性:

  • daemon:和线程的setDeamon功能一样
  • name:进程名字。
  • pid:进程号。 
from multiprocessing import Process
import os
import time

def info(name):
    print('name:', name)
    print('parent process:', os.getppid())
    print('process id', os.getpid())
    print('-----------------------')
    time.sleep(1)

def foo(name):
    info(name)

if __name__ == '__main__':
    info('main process line')

    p1 = Process(target=info, args=('p1', ))
    p2 = Process(target=foo, args=('p2', ))

    p1.start()
    p2.start()

    p1.join()
    p2.join()

    print('ending')

十一. 进程间通信

进程间通信使用三种方法:进程队列Queue管道manager

进程队列Queue

from multiprocessing import Process, Queue  # 导入进程间队列


def f(q, n):
    q.put(n + 1)
    print('sub process: ', id(q))


def conn_queue():
    q = Queue()
    print('main process: ', id(q))

    for i in range(3):
        p = Process(target=f, args=(q, i))
        p.start()

    print(q.get())
    print(q.get())
    print(q.get())


if __name__ == '__main__':
    conn_queue()

数据进入不同进程是经过拷贝的

管道

Pipe()返回的两个连接对象代表管道的两端。 每个连接对象都有send()和recv()方法。 请注意,如果两个进程(或线程)尝试同时读取或写入管道的同一端,管道中的数据可能会损坏。

from multiprocessing import Process, Pipe

def f(conn):
    conn.send('hello')
    response = conn.recv()
    print('process-1 ', response)
    conn.close()

if __name__ == '__main__':

    parent_conn, sub_conn = Pipe()
    p = Process(target=f, args=(sub_conn,))
    p.start()
    print('process-2 ', parent_conn.recv())
    parent_conn.send("world")
    p.join()

>>process-2  hello
>>process-1  world

通过管将两个进程连接起来,通过p.start() 启动进程1,进程1发送消息hello,然后等下接受消息,进程2在接收消息后发送消息world ,进程1收到消息后关闭进程。

manager

Queue和pipe只是实现了数据交互,并没实现数据共享。实现数据共享,即实现一个进程修改数据,另一个进程的数据也随之改变。
使用manager可以实现数据共享,manager支持共享的数据类型包括:list,dict,Namespace,Lock,RLock,Semaphore,BoundedSemaphore,Condition,Event,Barrier,Queue,Value ,Array
使用方法如下所示

from multiprocessing import Process, Manager


def f(d, l, n):
    d[n] = n
    l.append(n)


if __name__ == '__main__':

    with Manager() as manager:

        d = manager.dict()
        l = manager.list()
        p_list = []

        for i in range(10):
            p = Process(target=f, args=(d, l, i))
            p.start()
            p_list.append(p)

        for res in p_list:
            res.join()

        print(d)
        print(l)

>>{1: 1, 0: 0, 2: 2, 3: 3, 4: 4, 5: 5, 6: 6, 7: 7, 8: 8, 9: 9} 
>>[1, 0, 2, 3, 4, 5, 6, 7, 8, 9]

可以看出主进程声明了一个空列表和一个空字典,在子进程中对字典和列表进行了修改,主进程中的字典和列表也随着修改。实现了数据共享。

十二. 进程池

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

进程池中有以下几个主要方法:

  • apply:从进程池里取一个进程并执行
  • apply_async:apply的异步版本
  • terminate:立刻关闭线程池
  • join:主进程等待所有子进程执行完毕,必须在close或terminate之后
  • close:等待所有进程结束后,才关闭线程池
from multiprocessing import Pool
import time
import os

def foo(args):
    time.sleep(1)
    print(args)
    print('foo: ', os.getpid())

def Bar(arg): # 回调函数在主进程中调用,arg是foo函数的返回值
    print('Bar: ', os.getpid())

if __name__ == '__main__':
    print('main:', os.getpid())
    p = Pool(5)
    for i in range(30):
        p.apply_async(func=foo, args=(i,), callback=Bar)

    p.close()   # 等子进程执行完毕后关闭线程池
    p.join()

# 结果5个5个打印
# 必须先close再join

十三. 协程(coroutine)

协程的优点

  • 协程的执行效率非常高。因为子程序切换不是线程切换,而是由程序自身控制。因此,没有线程切换的开销,和多线程相比,线程数量越多,相同数量的协程体现出的优势越明显
  • 不需要多线程的锁机制。由于只有一个线程,也不存在同时写变量的冲突,在协程中控制共享资源不需要加锁,只需要判断数据的状态,所以执行效率远高于线程
    对于多核cpu,可以使用多进程+协程来尽可能高效率地利用cpu

yield实现生产者消费者模型

传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。
如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率比之传统高非常多。

import time

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return

        print('[CONSUMER] <--- Consuming %s...' % n)
        time.sleep(1)
        r = 'ok'

def producer(c):
    next(c)
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] ---> Producing %s...' % n)
        c_ret = c.send(n)  # 和next有同样的同能,可以触发函数到下一个yield,却别于next的是可以像yield的左边的变量传值,例如上面yield左边的n

        print('[PRODUCER] Consumer return %s' % c_ret)
    c.close()


if __name__ == '__main__':
    c = consumer()
    producer(c)

>>[PRODUCER] ---> Producing 1...
[CONSUMER] <--- Consuming 1...
[PRODUCER] Consumer return ok
[PRODUCER] ---> Producing 2...
[CONSUMER] <--- Consuming 2...
[PRODUCER] Consumer return ok
...

可以看出通过协程协程实现的生产者消费者模型是一种可控的生产消费模型,在消费者producer调用send之后启动生产者,实现可控的生产者消费者之间的通信。
使用yield手动实现协程是比较麻烦的,Python提供了一个模块greenlet用来实现协程。

greenlet模块

from greenlet import greenlet

def foo_1():
    print('12')
    g2.switch()
    print('56')
    g2.switch()


def foo_2():
    print('34')
    g1.switch()
    print('78')
    g1.switch()

g1 = greenlet(foo_1)
g2 = greenlet(foo_2)

g1.switch()

>>12
34
56
78

#从结果可以看出每次switch就会调用到switch对应的那个函数中上一个switch的位置之后开始执行

greenlet比手动实现协程方便了很多,但复杂io操作场景下,手动去各种switch还是很麻烦的。没有最好只有更好,so,有一个基于greenlet实现的叫做gevent的模块实现了更加逆天的功能。

gevent模块

Python通过yield提供了对协程的基本支持,但是不完全。而第三方的gevent为Python提供了比较完善的协程支持。
gevent是第三方库,通过greenlet实现协程,其基本思想是:
当一个greenlet遇到IO操作时,比如访问网络,就自动切换到其他的greenlet,等到IO操作完成,再在适当的时候切换回来继续执行。由于IO操作非常耗时,经常使程序处于等待状态,有了gevent为我们自动切换协程,就保证总有greenlet在运行,而不是等待IO。
由于切换是在IO操作时自动完成,所以gevent需要修改Python自带的一些标准库,这一过程在启动时通过monkey patch完成。

from gevent import monkey
monkey.patch_all()
import gevent
from urllib import request
import time

def f(url):
    print('GET: %s' % url)
    resp = request.urlopen(url)
    data = resp.read()
    print('%d bytes received from %s.' % (len(data), url))

start = time.time()

gevent.joinall([
        gevent.spawn(f, 'https://www.douban.com/'),
        gevent.spawn(f, 'https://www.github.com/'),
        gevent.spawn(f, 'https://zhihu.com/'),
])

print(time.time()-start)

gevent是一个基于协程(coroutine)的Python网络函数库,通过使用greenlet提供了一个在libev事件循环顶部的高级别并发API。

主要特性有以下几点:

  1. 基于libev的快速事件循环,Linux上面的是epoll机制
  2. 基于greenlet的轻量级执行单元
  3. API复用了Python标准库里的内容
  4. 支持SSL的协作式sockets
  5. 可通过线程池或c-ares实现DNS查询
  6. 通过monkey patching功能来使得第三方模块变成协作式

gevent.spawn()方法spawn一些jobs,然后通过gevent.joinall将jobs加入到微线程执行队列中等待其完成,设置超时为2秒。执行后的结果通过检查gevent.Greenlet.value值来收集。gevent.socket.gethostbyname()函数与标准的socket.gethotbyname()有相同的接口,但它不会阻塞整个解释器,因此会使得其他的greenlets跟随着无阻的请求而执行。
monkey patching
Python的运行环境允许我们在运行时修改大部分的对象,包括模块、类甚至函数。虽然这样做会产生“隐式的副作用”,而且出现问题很难调试,但在需要修改Python本身的基础行为时,Monkey patching就派上用场了。Monkey patching能够使得gevent修改标准库里面大部分的阻塞式系统调用,包括socket,ssl,threading和select等模块,而变成协作式运行。
通过monkey.patch_socket()方法,urllib2模块可以使用在多微线程环境,达到与gevent共同工作的目的。
事件循环
不像其他网络库,gevent和eventlet类似, 在一个greenlet中隐式开始事件循环。没有必须调用run()或dispatch()的反应器(reactor),在twisted中是有 reactor的。当gevent的API函数想阻塞时,它获得Hub实例(执行时间循环的greenlet),并切换过去。如果没有集线器实例则会动态 创建。
libev提供的事件循环默认使用系统最快轮询机制,设置LIBEV_FLAGS环境变量可指定轮询机制。LIBEV_FLAGS=1为select, LIBEV_FLAGS = 2为poll, LIBEV_FLAGS = 4为epoll,LIBEV_FLAGS = 8为kqueue。
Libev的API位于gevent.core下。注意libev API的回调在Hub的greenlet运行,因此使用同步greenlet的API。可以使用spawn()和Event.set()等异步API。

十四. 事件驱动模型

十五. IO模型

准备知识

用户空间和内核空间

操作系统都是采用虚拟存储器,对于32位操作系统而言,他的寻址空间为4G
操作系统的核心是内核,独立于不同的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。
为了保证用户进程不能直接操作内核,保证内核的安全,操作系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。
针对Linux操作系统,将高位1G字节分配给内核使用,称为内核空间,将低位3G字节分配给各个进程使用,成为用户空间

进程切换

为了控制进程执行,内核不许有能力挂起在cpu上运行的进程。并执行之前挂起的某个进程。这种行为成为进程切换,这种切换是由操作系统完成的。任何的进程操作都是内核相关的行为。
从一个进程切换到另外一个进程需要完成的操作如下:
- 保存处理器上下文,包括程序计数器和寄存器
- 更新PCB信息
- 把进程的PCB移入相应的队列,如就绪、在某时间阻塞等队列
- 选择另一个进程执行,并更新其PCB
- 恢复处理器上下文

进程阻塞

正在执行的进程,由于期待的某些事件没有发生,如果请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新操作执行等,则由操作系统自动执行阻塞原语(Block)。是自己由运行状态变为阻塞状态。
由此可见,进程的阻塞是进程自身的一种主动行为,也因此只有出于运行状态的进程才可以进入阻塞状态。当进程进入阻塞状态,不再占用cpu资源。

文件描述符fd

文件描述符(file descriptor)是一种用于描述指向文件引用的抽象化概念。
文件描述符是一个索引值,指向内核每一个进程所维护的该进程打开文件的记录表。
当程序打开一个现有文件,或者创建一个新文件时,内核向进程返回一个文件描述符。
在程序设计中,一些涉及底层的程序编写往往会围绕着文件描述符展开。但是文件描述符这一概念往往只适用与posix系统。

缓存I/O

缓存I/O又被称作标准I/O,大多数文件系统默认的I/O操作都是缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存(page cache)中。即数据会被拷贝到操作系统内核的缓存区,然后从操作系统的内核的缓存区拷贝到应用程序的地址空间。用户空间没办法直接访问内核空间。
缓存I/O的缺点:数据在传输国产的那个中需要在应用程序地址空间和内核进行多次数据拷贝操作,这些拷贝操作带来的cpu和内存开销是非常大的。
对于I/O的解释,不同的背景下是不一样的,下面讨论的I/O都是基于Linux系统的network IO
I/O模型主要有以下五种
- blocking I/O
- noblocking I/O
- I/O multiplexing
- asynchronous I/O
- signal I/O
这里主要讨论前四种

IO发生时涉及的对象和步骤。

对于一个network IO (这里我们以read举例),它会涉及到两个系统对象,一个是调用这个IO的process (or thread),另一个就是系统内核(kernel)。当一个read操作发生时,它会经历两个阶段:
- 等待数据准备 (Waiting for the data to be ready)
- 将数据从内核拷贝到进程中 (Copying the data from the kernel to the process)
不同的IO Model的区别就是在两个阶段上各有不同的情况。

阻塞IO(blocking IO)

在linux中,默认情况下所有的socket都是blocking,一个典型的读操作流程大概是这样:
这里写图片描述
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据。对于network io来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候kernel就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

非阻塞IO(nobloking IO)

linux下,可以通过设置socket使其变为non-blocking。当对一个non-blocking socket执行读操作时,流程如下所示:
这里写图片描述
从图中可以看出,当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。所以,用户进程其实是需要不断的主动询问kernel数据好了没有。
注意:
在网络IO时候,非阻塞IO也会进行recvform系统调用,检查数据是否准备好,与阻塞IO不一样,”非阻塞将大的整片时间的阻塞分成N多的小的阻塞, 所以进程不断地有机会 ‘被’ CPU光顾”。即每次recvform系统调用之间,cpu的权限还在进程手中,这段时间是可以做其他事情的,
也就是说非阻塞的recvform系统调用调用之后,进程并没有被阻塞,内核马上返回给进程,如果数据还没准备好,此时会返回一个error。进程在返回之后,可以干点别的事情,然后再发起recvform系统调用。重复上面的过程,循环往复的进行recvform系统调用。这个过程通常被称之为轮询。轮询检查内核数据,直到数据准备好,再拷贝数据到进程,进行数据处理。需要注意,拷贝数据整个过程,进程仍然是属于阻塞的状态。

import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
sk.setsockopt
sk.bind(('127.0.0.1',6667))
sk.listen(5)
sk.setblocking(False)
while True:
    try:
        print ('waiting client connection .......')
        connection,address = sk.accept()   # 进程主动轮询
        print("+++",address)
        client_messge = connection.recv(1024)
        print(str(client_messge,'utf8'))
        connection.close()
    except Exception as e:
        print (e)
        time.sleep(4)

#############################client

import time
import socket
sk = socket.socket(socket.AF_INET,socket.SOCK_STREAM)

while True:
    sk.connect(('127.0.0.1',6667))
    print("hello")
    sk.sendall(bytes("hello","utf8"))
    time.sleep(2)
    break

优点:

  • 能够在等待任务完成的时间里干其他活了(包括提交其他任务,也就是 “后台” 可以有多个任务在同时执行)。
    缺点:
  • 任务完成的响应延迟增大了,因为每过一段时间才去轮询一次read操作,而任务可能在两次轮询之间的任意时间完成。这会导致整体数据吞吐量的降低。
  • 发出多次系统调用请求
  • 拿到的数据不是实时数据

此外说明一点,copy data是阻塞的

IO多路复用(IO multiplexing)

单个process就可以同时处理多个网络连接的IO。它的基本原理就是不断的轮询所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:
这里写图片描述
当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
这个图和blocking IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而blocking IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(多说一句。所以,如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用multi-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。)
在IO multiplexing Model中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。
注意1:select函数返回结果中如果有文件可读了,那么进程就可以通过调用accept()或recv()来让kernel将位于内核中准备到的数据copy到用户区。
注意2: select的优势在于可以处理多个连接,不适用于单个连接

#***********************server.py
import socket
import select
sk=socket.socket()
sk.bind(("127.0.0.1",8801))
sk.listen(5)
inputs=[sk,]
while True:
    r,w,e=select.select(inputs,[],[],5)  # 5代表只监测5秒钟,5秒之后继续向下执行
    print(len(r))

    for obj in r:
        if obj==sk:
            conn,add=obj.accept()
            print(conn)
            inputs.append(conn)
        else:
            data_byte=obj.recv(1024)
            print(str(data_byte,'utf8'))
            inp=input('answer %s>>>'%inputs.index(obj))
            obj.sendall(bytes(inp,'utf8'))

    print('>>',r)

#***********************client.py

import socket
sk=socket.socket()
sk.connect(('127.0.0.1',8801))

while True:
    inp=input(">>>>")
    sk.sendall(bytes(inp,"utf8"))
    data=sk.recv(1024)
    print(str(data,'utf8'))

异步IO(asynchronous IO)

这里写图片描述
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

不同IO之间的比较

到目前为止,已经将四个IO Model都介绍完了。现在回过头来回答最初的那几个问题:blocking和non-blocking的区别在哪,synchronous IO和asynchronous IO的区别在哪。
先回答最简单的这个:blocking vs non-blocking。前面的介绍中其实已经很明确的说明了这两者的区别。调用blocking IO会一直block住对应的进程直到操作完成,而non-blocking IO在kernel还准备数据的情况下会立刻返回。
在说明synchronous IO和asynchronous IO的区别之前,需要先给出两者的定义。Stevens给出的定义(其实是POSIX的定义)是这样子的:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
两者的区别就在于synchronous IO做”IO operation”的时候会将process阻塞。按照这个定义,之前所述的blocking IO,non-blocking IO,IO multiplexing都属于synchronous IO。有人可能会说,non-blocking IO并没有被block啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个system call。non-blocking IO在执行recvfrom这个system call的时候,如果kernel的数据没有准备好,这时候不会block进程。但是,当kernel中数据准备好的时候,recvfrom会将数据从kernel拷贝到用户内存中,这个时候进程是被block了,在这段时间内,进程是被block的。而asynchronous IO则不一样,当进程发起IO 操作之后,就直接返回再也不理睬了,直到kernel发送一个信号,告诉进程说IO完成。在这整个过程中,进程完全没有被block。
这里写图片描述
经过上面的介绍,会发现non-blocking IO和asynchronous IO的区别还是很明显的。在non-blocking IO中,虽然进程大部分时间都不会被block,但是它仍然要求进程去主动的check,并且当数据准备完成以后,也需要进程主动的再次调用recvfrom来将数据拷贝到用户内存。而asynchronous IO则完全不同。它就像是用户进程将整个IO操作交给了他人(kernel)完成,然后他人做完后发信号通知。在此期间,用户进程不需要去检查IO操作的状态,也不需要主动的去拷贝数据。

十六. select与epoll

select 和 epoll 点这里

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值