尽管asyncio应用通常作为单线程运行,不过仍被构建为并发应用。由于I/O以及其他外部事件的延迟和中断,每个协程或任务可能按一种不可预知的顺序执行。为了支持安全的并发执行,asyncio包含了threading和multiprocessing模块中的一些底层原语的实现。

锁(LOCK)

锁可以用来保护对一个共享资源的访问。只有锁的持有者可以使用这个资源。如果有多个请求要的到这个锁,那么其将会阻塞,以保证一次只有一个持有者。
看一个锁的例子:

import asyncio
from functools import partial

def unlock(lock):
    print("callback释放锁")
    lock.release()

async def coro1(lock):
    print("并行中,coro1等待锁")
    async with lock:
        print("coro1被锁了")
    print("coro1的锁释放了")

async def coro2(lock):
    print("并行中,coro2等待锁")
    await lock.acquire()
    print(f"当前是否被锁", lock.locked())
    try:
        print("coro2被锁了")
    finally:
        lock.release()
        print("coro2的锁释放了")

async def coro3(lock):
    print("并行中,coro3等待锁")
    try:
        print("coro3没有加锁加试图释放")
        lock.release()
    except RuntimeError as e:
        print("触发RuntimeError的错误")

async def main(loop):
    # 创建一个锁
    lock = asyncio.Lock()
    loop.call_later(0.1, partial(unlock, lock))
    print("等待协程")
    await asyncio.wait([coro1(lock), coro2(lock), coro3(lock)])

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(main(loop))
    finally:
        loop.close()

输出结果:

通过acquire加锁
当前是否被锁 True
等待协程
并行中,coro2等待锁
并行中,coro1等待锁
callback释放锁
coro2被锁了
coro2的锁释放了
coro1被锁了
coro1的锁释放了

通过上面的代码以及结果可以得出以下结论:

  • lock.acquire()需要使用await。
  • lock.release()不需要加await。
  • 在加锁之前coro1和coro2 ,coro3是并发执行的。
  • 锁有两种使用方式和像coro1一样通过async with 异步上下文关键字进行锁定,还可以通过coro2那种通过await方式使用acquire加锁,结束的时候使用release释放锁。
  • 如果没有使用acquire进行加锁,就试图使用release去释放,将触发RuntimeError的异常,像coro3协程一样。

    事件(Event)

    asyncio.Event基于threading.Event。允许多个消费者等待某个事件发生,而不必寻找一个特定值与通知关联。

import asyncio
import functools

def callback(event):
    print('callback中设置event')
    event.set()

async def coro1(name, event):
    print(f'{name}等待事件')
    await event.wait()
    print(f'{name}触发')

async def coro2(name, event):
    print(f'{name}等待事件')
    await event.wait()
    print(f'{name}触发')

async def main(loop):
    event = asyncio.Event()
    print(f'当前事件状态: {event.is_set()}')
    loop.call_later(
        0.1, functools.partial(callback, event)
    )
    await asyncio.wait([coro1('coro1', event), coro2('coro2', event)])
    print(f'当前事件状态: {event.is_set()}')

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main(loop))
    loop.close()

输出

当前事件状态: False
coro2等待事件
coro1等待事件
callback中设置event
coro2触发
coro1触发
当前事件状态: True

emmmm.。。。。。发现好像和lock也没啥区别。其实区别的话就是一旦触发了事件,coro1和coro2协程就会立即启动,不需要得到事件对象上的唯一的锁了。

条件(condition)

Condition的做法与Event类似,只不过不是通知所有的协程等待的协程,被唤醒的等待协程的数目由notify()的一个参数控制。

import asyncio

async def consumer(cond, name, second):
    await asyncio.sleep(second)
    async with cond:
        await cond.wait()
        print(f'{name}:资源可供消费者使用')

async def producer(cond):
    await asyncio.sleep(2)
    for n in range(1, 3):
        async with cond:
            print(f'唤醒消费者 {n}')
            cond.notify(n=n)
        await asyncio.sleep(0.1)

async def producer2(cond):
    await asyncio.sleep(2)
    with await cond:
        print('让资源变的可用')
        cond.notify_all()

async def main(loop):
    condition = asyncio.Condition()

    task = loop.create_task(producer(condition))
    consumers = [consumer(condition, name, index)
                 for index, name in enumerate(('c1', 'c2'))]
    await asyncio.wait(consumers)
    task.cancel()

    task = loop.create_task(producer2(condition))
    consumers = [consumer(condition, name, index)
                 for index, name in enumerate(('c1', 'c2'))]
    await asyncio.wait(consumers)
    task.cancel()

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main(loop))
    loop.close()

输出内容

唤醒消费者 1
c1:资源可供消费者使用
唤醒消费者 2
c2:资源可供消费者使用
让资源变的可用
c1:资源可供消费者使用
c2:资源可供消费者使用

对上面的代码做简单的分析
使用notify方法挨个通知单个消费者
使用notify_all方法一次性的通知全部消费者
由于producer和producer2是异步的函数,所以不能使用之前call_later方法,需要用create_task把它创建成一个任务,或者asyncio.ensure_future().

队列(Queue)

asyncio.Queue为协程提供了一个先进先出的数据结构,这与线程queue.Queue或进程的multiprocess,Queue很类似。
这里直接上一个aiohtpp爬虫使用的例子

import aiohttp
import asyncio
import async_timeout
from urllib.parse import urljoin, urldefrag

root_url = "http://python.org/"
crawled_urls, url_hub = [], [root_url, f"{root_url}/sitemap.xml", f"{root_url}/robots.txt"]
headers = {
    'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36'}

async def get_body(url):
    async with aiohttp.ClientSession() as session:
        try:
            with async_timeout.timeout(10):
                async with session.get(url, headers=headers) as response:
                    if response.status == 200:
                        html = await response.text()
                        return {'error': '', 'html': html}
                    else:
                        return {'error': response.status, 'html': ''}
        except Exception as err:
            return {'error': err, 'html': ''}

async def handle_task(task_id, work_queue):
    while not work_queue.empty():  # 如果队列不为空
        queue_url = await work_queue.get()  # 从队列中取出一个元素
        if not queue_url in crawled_urls:
            crawled_urls.append(queue_url)  # crawled_urls可以做一个去重操作
            body = await get_body(queue_url)
            if not body['error']:
                for new_url in get_urls(body['html']):
                    if root_url in new_url and not new_url in crawled_urls:
                        work_queue.put_nowait(new_url)
            else:
                print(f"Error: {body['error']} - {queue_url}")

def remove_fragment(url):
    pure_url, frag = urldefrag(url)
    return pure_url

def get_urls(html):
    new_urls = [url.split('"')[0] for url in str(html).replace("'", '"').split('href="')[1:]]
    return [urljoin(root_url, remove_fragment(new_url)) for new_url in new_urls]

if __name__ == "__main__":
    q = asyncio.Queue()  # 定义一个队列
    [q.put_nowait(url) for url in url_hub]  # 通过put_nowait方法循环往队列添加元素
    loop = asyncio.get_event_loop()
    tasks = [handle_task(task_id, q) for task_id in range(3)]
    loop.run_until_complete(asyncio.wait(tasks))
    loop.close()
    for u in crawled_urls:
        print(u)
    print('-' * 30)
    print(len(crawled_urls))

代码中对关键的部分做了注释总结下Queue的内容:
1.通过asyncio.Queue():定义一个asyncio队列。
2.通过put_nowait可以向队列添加元素。
3.通过empty判断队列中的元素是否为空
4.get方法取出每个元素,需要注意的是要使用await。

信号量(Semaphore)

通过并发量可以去控制协程的并发数,爬虫操作使用中使用该方法减小并发量,可以减少对服务器的压力。Semaphore运作机制可以用停车场停车来比喻,一个停车场5个停车位,第一次5辆车可以都停下,我们知道正常情况下,不能在进入第6辆了,需要有一辆开走,然后才能再来一辆,当然如果有2辆开走,那么可以再同时进来2辆,一次类推我们就知道了,整个过程的关键点是,在车数足够多的时候,整个停车场最多只能放5辆车。下面我们在看个代码进一步做一个了解。

import aiohttp
import asyncio

URL = "http://www.baidu.com"
sem = asyncio.Semaphore(5)

async def branch():
    async with sem:
        await fetch()
        await asyncio.sleep(2)

async def fetch():
    async with aiohttp.ClientSession() as session:
        async with session.get(URL) as req:
            status = req.status
            print("状态码", status)

async def run():
    await branch()

if __name__ == '__main__':
    loop = asyncio.get_event_loop()
    try:
        tasks=[asyncio.ensure_future(run()) for _ in range(21)]
        loop.run_until_complete(asyncio.wait(tasks))
    finally:
        loop.close()

上面代码是一个并发访问百度然后获取状态码的一个简单的例子,并发次数为20次,然后通过asyncio.Semaphore(5)指定并发量为5,通过async with sem做一个限制,然后fetch协程是整个爬虫的逻辑代码,运行上面的代码可以发现每隔2s输出5个请求结果。
其他地方的使用第一章内容讲的很详细了这里就不详细说了。

python异步编程模块asyncio学习(二)的更多相关文章

  1. Python 异步编程笔记:asyncio

    个人笔记,不保证正确. 虽然说看到很多人不看好 asyncio,但是这个东西还是必须学的.. 基于协程的异步,在很多语言中都有,学会了 Python 的,就一通百通. 一.生成器 generator ...

  2. 深入理解 Python 异步编程(上)

    http://python.jobbole.com/88291/ 前言 很多朋友对异步编程都处于"听说很强大"的认知状态.鲜有在生产项目中使用它.而使用它的同学,则大多数都停留在知 ...

  3. 深入理解Python异步编程(上)

    本文代码整理自:深入理解Python异步编程(上) 参考:A Web Crawler With asyncio Coroutines 一.同步阻塞方式 import socket def blocki ...

  4. 这篇文章讲得精彩-深入理解 Python 异步编程(上)!

    可惜,二和三现在还没有出来~ ~~~~~~~~~~~~~~~~~~~~~~~~~ http://python.jobbole.com/88291/ ~~~~~~~~~~~~~~~~~~~~~~~~~~ ...

  5. python 异步编程

    Python 3.5 协程究竟是个啥 Yushneng · Mar 10th, 2016 原文链接 : How the heck does async/await work in Python 3.5 ...

  6. python异步编程之asyncio

    python异步编程之asyncio   前言:python由于GIL(全局锁)的存在,不能发挥多核的优势,其性能一直饱受诟病.然而在IO密集型的网络编程里,异步处理比同步处理能提升成百上千倍的效率, ...

  7. 最新Python异步编程详解

    我们都知道对于I/O相关的程序来说,异步编程可以大幅度的提高系统的吞吐量,因为在某个I/O操作的读写过程中,系统可以先去处理其它的操作(通常是其它的I/O操作),那么Python中是如何实现异步编程的 ...

  8. (转)python异步编程--回调模型(selectors模块)

    原文:https://www.cnblogs.com/zzzlw/p/9384308.html#top 目录 0. 参考地址 1. 前言 2. 核心类 3. SelectSelector核心函数代码分 ...

  9. python异步编程--回调模型(selectors模块)

    目录 0. 参考地址 1. 前言 2. 核心类 3. SelectSelector核心函数代码分析 3.1 注册 3.2 注销 3.3 查询 4. 别名 5. 总结 6. 代码报错问题 1. 文件描述 ...

随机推荐

  1. 关于交叉熵(cross entropy),你了解哪些

    二分~多分~Softmax~理预 一.简介 在二分类问题中,你可以根据神经网络节点的输出,通过一个激活函数如Sigmoid,将其转换为属于某一类的概率,为了给出具体的分类结果,你可以取0.5作为阈值, ...

  2. Openldap命令详解

    Openldap 客户端常用管理命令 1.ldapadd -x: 简答认证方式 -W: 不需要在命令上写密码 ldapapp -x -D "cn=Manager,dc=suixingpay, ...

  3. Redis之RDB与AOF 笔记

    AOF定义:以日志的形式记录每个操作,将Redis执行过的所有指令全部记录下来(读操作不记录),只许追加文件但不可以修改文件,Redis启动时会读取AOF配置文件重构数据 换句话说,就是Redis重启 ...

  4. inux进程/线程调度策略与 进程优先级

    目的: 系统性的认识linux的调度策略(SCHED_OTHER.SCHED_FIFO.SCHED_RR): 实时调度?分时调度? 混搭系统(实时任务+分时任务),怎样调度. linux的调度策略 l ...

  5. 【leetcode-100】 简单 树相关题目

    100. 相同的树 (1过,熟练) 给定两个二叉树,编写一个函数来检验它们是否相同. 如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的. 示例 1: 输入: 1 1 / \ / \ 2 ...

  6. Openresty 学习笔记(二)Nginx Lua 正则表达式相关API

    ngx.re.match 语法: captures, err = ngx.re.match(subject, regex, options?, ctx?, res_table?) 环境: init_w ...

  7. 转---redshift database ---学习

    摘自他人 前沿 根据最近一段时间对redshift的研究,发现一些特性比较适合我们当前的业务. 1 比如它的快速恢复能力,因为这一点,我们可以尽量在redshit里面存放一定生命周期的数据,对过期的数 ...

  8. 【九】虚拟机工具 02 - jstat命令使用

    java8jstat官方文档 jstat命令可以查看堆内存各部分的使用量,以及加载类的数量.命令的格式如下: jstat [-命令选项] [vmid] [间隔时间/毫秒] [查询次数] 注意:使用的j ...

  9. Puppeteer - 谷歌推出的自动化测试工具库

    Puppeteer 是一个 Node 库,它提供了一个高级 API 来通过 DevTools 协议控制无头 Chrome 或 Chromium.它允许你从浏览器之外的环境(即命令行)与Chromium ...

  10. linux关闭防火墙及开放端口

    1) 重启后生效 开启: chkconfig iptables on 关闭: chkconfig iptables off 2) 即时生效,重启后失效 开启: service iptables sta ...