协程

    协程产生背景

进程、线程存在的问题

    进程是资源分配的最小单位,线程是CPU调度的最小单位。但无论是创建多进程还是创建多线程来解决问题,都要消耗一定的时间来创建进程、创建线程、以及管理他们之间的切换。

    cpu正在运行一个任务,会在两种情况下切走去执行其他的任务(切换由操作系统强制控制):

  • 一种情况是该任务发生了阻塞
  • 另外一种情况是该任务计算的时间过长

    

    在介绍进程理论时,提及进程的三种执行状态,而线程才是执行单位,所以也可以将上图理解为线程的三种状态 :

  • 其中第二种情况并不能提升效率,只是为了让cpu能够雨露均沾,实现看起来所有任务都被“同时”执行的效果,如果多个任务都是纯计算的,这种切换反而会降低效率。
  • 第一种情况的切换。在任务一遇到io情况下,切到任务二去执行,这样就可以利用任务一阻塞的时间完成任务二的计算,效率的提升就在于此。

    基于yield验证对于纯计算任务,串行与并发效率对比:

yield:
1) yield可以存状态,yield的状态保存与操作系统的保存线程状态很像,但是yield是代码级别控制的,更轻量级。
2) send可以把一个函数的结果传给另一个函数,以此实现单线程内程序之间的切换。
yield
import time


# 串行执行
def consumer(res):
    pass


def producer():
    res = []
    for i in range(1000000):
        res.append(i)
    return res


start = time.time()
res = producer()
consumer(res)
stop = time.time()
print(stop - start)  # 0.13910341262817383


# 基于yield并发执行

def consumer():
    while 1:
        x = yield


def producer():
    g = consumer()
    next(g)
    for i in range(1000000):
        g.send(i)


start = time.time()
producer()
stop = time.time()
print(stop - start)  # 0.14209675788879395
串行与并发效率对比
import time


def consumer():
    while 1:
        x = yield


def producer():
    g = consumer()
    next(g)
    for i in range(2):
        g.send(i)
        time.sleep(2)


start = time.time()
producer()
stop = time.time()
print(stop - start)  # 4.000760316848755
yield无法解决IO阻塞

基于单线程实现并发

    随着我们对于效率的追求不断提高,基于单线程来实现并发(并发本质:切换+保存状态)又成为一个新的课题,即只用一个主线程(很明显可利用的cpu只有一个)情况下实现并发。这样就可以节省创建线进程所消耗的时间。

    在单线程下,程序中不可避免会出现io操作,但如果我们能在自己的程序中(即用户程序级别,而非操作系统级别)控制单线程下的多个任务能在一个任务遇到io阻塞时就切换到另外一个任务去计算,这样就保证了该线程能够最大限度地处于就绪态,即随时都可以被cpu执行的状态,相当于我们在用户程序级别将自己的io操作最大限度地隐藏起来,从而可以迷惑操作系统,让其看到:该线程好像是一直在计算,io比较少,从而更多的将cpu的执行权限分配给我们的线程。

    协程的本质就是在单线程下,由用户自己控制一个任务遇到io阻塞了就切换另外一个任务去执行,以此来提升效率。

    为了实现它,我们需要找寻一种可以同时满足以下条件的解决方案:

  • 可以控制多个任务之间的切换,切换之前将任务的状态保存下来,以便重新运行时,可以基于暂停的位置继续执行
  • 可以检测IO操作,在遇到IO操作的情况下才发生切换

协程

定义

    协程:Coroutine,单线程下的并发,又称微线程,纤程。是一种用户态的轻量级线程,即协程是用户程序自己控制调度的

优点:

  • 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级
  • 单线程内就可以实现并发的效果,最大限度地利用cpu

缺点:

  • 协程的开启是在单线程下,无法利用多核。只能一个程序开启多个进程,每个进程开启多个线程,每个线程开启多个协程
  • 协程的开启是在单线程下,因而一旦协程出现阻塞,将会阻塞整个线程

协程与线程区别

  • 线程属于内核级别的,即由操作系统控制调度(如单线程遇到IO或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行)
  • 协程属于应用程序级别的,一旦遇到IO,就会从应用程序级别(而非操作系统)控制调度,以此减小线程间切换时间来提升效率

特点:

  • 必须在只有一个单线程时实现并发
  • 修改共享数据不需加锁
  • 用户程序里自己保存多个控制流的上下文栈
  • 一个协程遇到IO操作自动切换到其他协程(检测IO,yield、greenlet都无法实现,gevent模块(select机制)能实现)
  • 一个线程一般最大可以开启500个协程

 

greenlet模块

greenlet

  • 安装:pip install greenlet

基本使用

1. 创建greenlet:  xxx = greenlet.greenlet(func)
当创建一个greenlet时,首先初始化一个空的栈,switch到这个栈时会运行在greenlet构造时传入的函数

2.切换协程:xxx.switch()
表示切换到xxx, 此时该协程会被挂起,等到切换回来。当一个协程对应函数执行完毕,那么这个协程就变成dead状态。
创建协程及方法使用

 

import greenlet


def eat(name):
    print("%s eat 1" % name)
    g2.switch("egon")
    print("%s eat 2" % name)


def play(name):
    print("%s play 1" % name)
    g1.switch()                # 没有切换回来,一直被挂起
    print("%s play 2" % name)  # 不打印


g1 = greenlet.greenlet(eat)
g2 = greenlet.greenlet(play)

g1.switch("egon")  # 可以在第一次switch时传入参数,以后都不需要


结果:
egon eat 1
egon play 1
egon eat 2
代码示例

    greenlet只是提供了一种比generator更加便捷的切换方式,当切到一个任务执行时如果遇到io,那就原地阻塞,仍然是没有解决遇到IO自动切换来提升效率的问题。

gevent模块

gevent

    gevent是一个第三方库,可以轻松通过gevent实现并发同步或异步编程,在gevent中用到的主要模式是Greenlet, 它是以C扩展模块形式接入Python的轻量级协程。 Greenlet全部运行在主程序操作系统进程的内部,但它们被协作式地调度。

g1=gevent.spawn(func,1,,2,3,x=4,y=5)
创建一个协程对象g1,spawn括号内第一个参数是函数名,如eat,后面可以有多个参数,可以是位置实参或关键字实参,都是传给函数eat的

g1.join() :等待g1结束

g2.join() :等待g2结束

gevent.joinall([g1,g2]):等待g1,g2结束

g1.value:拿到func1的返回值

gevent.sleep(2):gevent可以识别的阻塞

而time.sleep(2)或其他的阻塞,gevent无法识别,需要在import time,socket等前加上补丁:from gevent import monkey,monkey.patch_all()
用法介绍
import gevent


def eat(name):
    print("%s eat 1" % name)
    gevent.sleep(2)
    print("%s eat 2" % name)


def play(name):
    print("%s play 1" % name)
    gevent.sleep(1)
    print("%s play 2" % name)


g1 = gevent.spawn(eat, "egon")
g2 = gevent.spawn(play, "egon")

gevent.joinall([g1, g2])
print("主线程代码结束")
遇到IO主动切换示例

    识别time.sleep()

from gevent import monkey; monkey.patch_all()
import gevent
import time


def eat():
    print("eat food 1")
    time.sleep(2)
    print("eat food 2")


def play():
    print("play 1")
    time.sleep(1)
    print("play 2")


g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)

gevent.joinall([g1, g2])
print("主线程代码结束")
识别time.sleep()

用threading.current_thread().getName()查看g1和g2, 查看结果为Dummy Thread-n,即假线程

from gevent import monkey; monkey.patch_all()
import gevent
import time
import threading


def eat():
    print(threading.current_thread().getName())
    print("eat food 1")
    time.sleep(2)
    print("eat food 2")


def play():
    print(threading.current_thread().getName())
    print("play 1")
    time.sleep(1)
    print("play 2")


g1 = gevent.spawn(eat)
g2 = gevent.spawn(play)

gevent.joinall([g1, g2])
print("主线程代码结束")


结果:
DummyThread-1
eat food 1
DummyThread-2
play 1
play 2
eat food 2
主线程代码结束
查看g1和g2

gevent之同步与异步

gevent同步与异步

gevent应用——爬虫

from gevent import monkey; monkey.patch_all()
import gevent
import requests
import time


def get_page(url):
    print("GET:%s" % url)
    response = requests.get(url)
    if response.status_code == 200:
        print("%d bytes received from %s" % (len(response.text), url))


if __name__ == '__main__':
    # 并发
    start_time = time.time()
    gevent.joinall([
        gevent.spawn(get_page, "https://www.python.org/"),
        gevent.spawn(get_page, "https://www.yahoo.com/"),
        gevent.spawn(get_page, "https://github.com/"),
    ])
    stop_time = time.time()
    print("run time is %s" % (stop_time - start_time))

    # 串行
    print("-----------------------------------------")
    s = time.time()
    requests.get("https://www.python.org/")
    requests.get("https://www.yahoo.com/")
    requests.get("https://github.com/")
    t = time.time()
    print("串行时间>>", t-s)
协程应用:爬虫

gevent应用——单线程下的socket并发

    gevent无法识别socket的阻塞,需在import socket前添加补丁:from gevent import monkey; monkey.patch_all(),也可使用gevent自带的socket

import gevent
from gevent import socket


def server(ip, port):
    s = socket.socket()
    s.bind((ip, port))
    s.listen()
    while 1:
        conn, addr = s.accept()
        gevent.spawn(talk, conn, addr)


def talk(conn, addr):
    try:
        while 1:
            res = conn.recv(1024)
            print("client %s:%s msg: %s" % (addr[0], addr[1], res))
            conn.send(res.upper())
    except Exception as e:
        print(e)
    finally:
        conn.close()


if __name__ == '__main__':
    server("127.0.0.1", 8001)
服务端
import socket

client = socket.socket()
client.connect(("127.0.0.1", 8001))


while 1:
    msg = input("<<<:").strip()
    if not msg:
        continue
    client.send(msg.encode("utf-8"))
    print(">>>", client.recv(1024).decode("utf-8"))
客户端
from threading import Thread
import socket
import threading


def client(server_ip, port):
    c = socket.socket()
    c.connect((server_ip, port))

    count = 0
    while 1:
        c.send(("%s say hello %s" % (threading.current_thread().getName(), count)).encode("utf-8"))
        print(">>>", c.recv(1024).decode("utf-8"))
        count += 1


if __name__ == '__main__':
    for i in range(500):
        t = Thread(target=client, args=("127.0.0.1", 8001))
        t.start()
多线程并发多个客户端,请求上面的服务端也是没问题的

 

posted @ 2018-12-04 20:06  Ethan_Y  阅读(820)  评论(0编辑  收藏  举报