asyncio并发编程-中

ThreadPoolExecutor和asyncio完成阻塞IO请求

这个小节我们看下如何将线程池和asyncio结合起来。

在协程里面我们还是需要使用多线程的,那什么时候需要使用多线程呢?

我们知道协程里面是不能加入阻塞IO的,但是有时我们必须执行阻塞IO的操作的时候,我们就需要多线程编程了,即我们要在协程中集成阻塞IO的时候就需要多线程操作。

import asyncio
from concurrent.futures import ThreadPoolExecutor
import socket
from urllib.parse import urlparse


def get_url(url):
    #通过socket请求html
    url = urlparse(url)
    host = url.netloc
    path = url.path
    if path == "":
        path = "/"

    #建立socket连接
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    client.connect((host, 80)) #阻塞不会消耗cpu

    #不停的询问连接是否建立好, 需要while循环不停的去检查状态
    #做计算任务或者再次发起其他的连接请求

    client.send("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))

    data = b""
    while True:
        d = client.recv(1024)
        if d:
            data += d
        else:
            break

    data = data.decode("utf8")
    html_data = data.split("\r\n\r\n")[1]
    print(html_data)
    client.close()


if __name__ == "__main__":
    import time
    start_time = time.time()
    
    loop = asyncio.get_event_loop()
    
    # 获得线程池的 executor
    executor = ThreadPoolExecutor()
    
    # 同样我们可以控制线程池的并发数量
    # executor = ThreadPoolExecutor()
    
    # 并发20个请
    tasks = []
    for url in range(20):
        url = "http://shop.projectsedu.com/goods/{}/".format(url)
        
        # 将阻塞的代码放到线程池中运行 返回的是 task
        task = loop.run_in_executor(executor, get_url, url)
        tasks.append(task)
        
    loop.run_until_complete(asyncio.wait(tasks))
    print("last time:{}".format(time.time()-start_time))
# 输出
last time:2.110485076904297

上面的代码会生成一个线程池 然后让阻塞的代码去线程池中执行。

看下源码:

def run_in_executor(self, executor, func, *args):
    self._check_closed()
    if self._debug:
        self._check_callback(func, 'run_in_executor')
    if executor is None:
        executor = self._default_executor
        # 即使我们没创建 executor 也会自己创建一个
        if executor is None:
            executor = concurrent.futures.ThreadPoolExecutor()
            self._default_executor = executor
            
    # 最后将阻塞代码放到线程池执行 然后返回一个 future 对象
    return futures.wrap_future(executor.submit(func, *args), loop=self)
  
def wrap_future(future, *, loop=None):
  """Wrap concurrent.futures.Future object."""
  if isfuture(future):
      return future
  assert isinstance(future, concurrent.futures.Future), \
      'concurrent.futures.Future is expected, got {!r}'.format(future)
  if loop is None:
      loop = events.get_event_loop()
  new_future = loop.create_future()
  _chain_future(future, new_future)
  return new_future

当我们需要在协程中调用阻塞IO的时候 就可以按照这种方式 放到线程池中

asyncio模拟http请求

asyncio里面凡是异步的地方都会创建一个future

import asyncio
from urllib.parse import urlparse


async def get_url(url):

    url = urlparse(url)
    host = url.netloc
    path = url.path
    if path == "":
        path = "/"

    # 通过协程的方式 建立socket连接 返回两个对象
    reader, writer = await asyncio.open_connection(host, 80)
    writer.write("GET {} HTTP/1.1\r\nHost:{}\r\nConnection:close\r\n\r\n".format(path, host).encode("utf8"))
    all_lines = []
    
    
    async for raw_line in reader:
        data = raw_line.decode("utf8")
        all_lines.append(data)
    html = "\n".join(all_lines)
    return html


async def main():
    tasks = []
    for url in range(20):
        url = "http://shop.projectsedu.com/goods/{}/".format(url)
        # 添加 future 对象到列表中
        tasks.append(asyncio.ensure_future(get_url(url)))
        
    # 将完成的打印出来 as_completed 返回的是协程
    for task in asyncio.as_completed(tasks):
        result = await task
        print(result)


if __name__ == "__main__":
    import time

    start_time = time.time()

    loop = asyncio.get_event_loop()

    loop.run_until_complete(main())

    print('last time:{}'.format(time.time() - start_time))

if __name__ == "__main__":
    import time

    start_time = time.time()

    loop = asyncio.get_event_loop()
    tasks = []
    for url in range(20):
        url = "http://shop.projectsedu.com/goods/{}/".format(url)
        tasks.append(get_url(url))

    loop.run_until_complete(asyncio.wait(tasks))

    print('last time:{}'.format(time.time() - start_time))

整个过程和之前我们实现的完全一致

future和task

future是一个结果容器会将结果放到future中,结果容器运行完毕之后会运行callback,类似线程池中的futuretaskfuture的一个子类。

我们看下一个特殊的函数

class Future:
    """This class is *almost* compatible with concurrent.futures.Future.

    Differences:

    - result() and exception() do not take a timeout argument and
      raise an exception when the future isn't done yet.

    - Callbacks registered with add_done_callback() are always called
      via the event loop's call_soon_threadsafe().

    - This class is not compatible with the wait() and as_completed()
      methods in the concurrent.futures package.

    (In Python 3.4 or later we may be able to unify the implementations.)
    """
    
    def set_result(self, result):
        """Mark the future done and set its result.
    
        If the future is already done when this method is called, raises
        InvalidStateError.
        """
        if self._state != _PENDING:
            raise InvalidStateError('{}: {!r}'.format(self._state, self))
        self._result = result
        self._state = _FINISHED
        # 运行完赋值之后 执行回调
        self._schedule_callbacks()
        
    def _schedule_callbacks(self):
    """Internal: Ask the event loop to call all callbacks.

    The callbacks are scheduled to be called as soon as possible. Also
    clears the callback list.
    """
    callbacks = self._callbacks[:]
    if not callbacks:
        return

    self._callbacks[:] = []
    # 因为是单线程模式 调用 call_soon 放到 loop 队列中
    # 然后由loop队列取数据执行 
        # 其他部分和线程池类似
    for callback in callbacks:
        self._loop.call_soon(callback, self)

为什么需要一个Task对象呢?

实际上task是协程和future之间的一个重要桥梁。

我们看下具体代码

我们知道在定义一个协程之后,在驱动协程之前,必须对这个协程调用一次nextsend方法,让这个协程生效

image.png

我们从源码看出task对象在初始化的时候调用了_step函数,而这个函数做了两个必要的事情。

第一个就是启动协程:

协程是和线程不一样的,协程必须要经历一个启动的过程。线程则不必,因此线程是由操作系统来调用的。但是协程是程序员自己调度的,我们必须要解决协程启动的问题。所以为了解决这个问题,抽象除了一个task对象,在初始化的时候就会启动协程。

第二个就是将协程的返回值设置到result中:

当运行时抛出StopIteration的时候,就会运行set_result将协程的return值保存到result中。线程中是没有StopIteration异常的。

为了保持协程和线程接口一致问题,创造了task对象来解决协程和线程不一样的地方所需要解决的问题。

我们看下上篇的图片,其中将上面的代码图形化了。

image.png
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260

推荐阅读更多精彩内容