尽管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. Zabbix Server 自带模板监控有密码MySQL数据库

    Zabbix Server 自带模板监控有密码MySQL数据库 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任.  一.Zabbix-agent端的配置 1>.为数据库设置密码 ...

  2. Netty中的三种Reactor(反应堆)

    目录: Reactor(反应堆)和Proactor(前摄器) <I/O模型之三:两种高性能 I/O 设计模式 Reactor 和 Proactor> <[转]第8章 前摄器(Proa ...

  3. WebStorm记录(2)

    继续效果图 CSS初始化 前面理解错了,背景图应该铺满 <div class="bg"> html,body,*{ /*盒子模型使用边框模式*/ box-sizing: ...

  4. ubuntu主题收集

    ubuntu主题收集 一些cmd常用命令: 任务栏底部,进入Dash并打开终端,命令最后一个是参数可选 ( Left | Bottom ) gsettings set com.canonical.Un ...

  5. Centos配置ARP和Tomcat Native

    Tomcat Native是用于Tomcat的一个可选组件,能够允许Tomcat使用一定的本地资源.性能.兼容性. 具体来说,Tomcat Native给了Tomcat访问 Apache Portab ...

  6. 058、flannel概述(2019-03-27 周三)

    参考https://www.cnblogs.com/CloudMan6/p/7412150.html   flannel 是 CentOS开发的容器网络解决方案.flannel 为每个host分配一个 ...

  7. tomcat解决GET请求中文参数乱码

    通常,在使用Spring MVC框架的应用程序中,为了解决中文参数乱码的问题,都会添加如下过滤器配置: <filter> <filter-name>encodingFilter ...

  8. Nginx 配置反向代理后,页面中取绝对URL地址的问题显示代理端口

    本文有V型知识库提供 upstream tomcat {    server 127.0.0.1:82;} location / {     proxy_pass http://tomcat;} 如上 ...

  9. [设计模式] javascript 之 命令模式

    模式定义: [定义]: 将一个请求封装成一个对象,使得你用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复功能. 组成: 调用者(Invoker),命令请求(Comman ...

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

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