在了解异步协程之前，我们首先得了解一些基础概念，如阻塞和非阻塞、同步和异步、多进程和协程。
阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间，自身无法继续处理其他的事情，则称该程序在该操作上是阻塞的。
常见的阻塞形式有：
用户输入阻塞等。
阻塞是无处不在的，包括CPU切换上下文时，所有的进程都无法真正处理事情，它们也会被阻塞。如果是多核CPU则正在执行上下文切换操作的核不可被利用。
程序在等待某操作过程中，自身不被阻塞，可以继续处理其他的事情，则称该程序在该操作上是非阻塞的。
非阻塞并不是在任何程序级别、任何情况下都可以存在的。仅当程序封装的级别可以囊括独立的子程序单元时，它才可能存在非阻塞状态。
非阻塞的存在是因为阻塞存在，正因为某个操作阻塞导致的耗时与效率低下，我们才要把它变成非阻塞的。
不同程序单元为了完成某个任务，在执行过程中需靠某种通信方式以协调一致，我们称这些程序单元是同步执行的。
例如购物系统中更新商品库存，需要用“行锁”作为通信信号，让不同的更新请求强制排队顺序执行，那更新库存的操作是同步的。
简言之，同步意味着有序。
为完成某个任务，不同程序单元之间过程中无需通信协调，也能完成任务的方式，不相关的程序单元之间可以是异步的。
简言之，异步意味着无序。
多进程就是利用CPU的多核优势，在同一时间并行地执行多个任务，可以大大提高执行效率。
协程，英文叫作Coroutine，又称微线程、纤程，协程是一种用户态的轻量级线程。
协程拥有自己的寄存器上下文和栈。协程调度切换时，将寄存器上下文和栈保存到其他地方，在切回来的时候，恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态，即所有局部状态的一个特定组合，每次过程重入时，就相当于进入上一次调用的状态。
协程本质上是个单进程，协程相对于多进程来说，无需线程上下文切换的开销，无需原子操作锁定及同步的开销，编程模型也非常简单。
多线程编程是比较困难的，因为调度程序任何时候都能中断线程，必须记住保留锁，去保护程序中重要部分，防止多线程在执行的过程中断。
而协程默认会做好全方位保护，以防止中断。我们必须显示产出才能让程序的余下部分运行。对协程来说，无需保留锁，而在多个线程之间同步操作，协程自身就会同步，因为在任意时刻，只有一个协程运行。总结下大概下面几点：
无需系统内核的上下文切换，减小开销；
无需原子操作锁定及同步的开销，不用担心资源共享的问题；
单线程即可实现高并发，单核CPU即便支持上万的协程都不是问题，所以很适合用于高并发处理，尤其是在应用在网络爬虫中。
接下来，我们来了解下协程的实现，从Python3.4开始，Python中加入了协程的概念，但这个版本的协程还是以生成器对象为基础的，在Python3.5则增加了async/await，使得协程的实现更加方便。
Python中使用协程最常用的库莫过于asyncio，所以本文会以asyncio为基础来介绍协程的使用。
首先我们需要了解下面几个概念。
event_loop：事件循环，相当于一个无限循环，我们可以把一些函数注册到这个事件循环上，当满足条件发生的时候，就会调用对应的处理方法。
coroutine：中文翻译叫协程，在Python中常指代为协程对象类型，我们可以将协程对象注册到事件循环中，它会被事件循环调用。我们可以使用async关键字来定义一个方法，这个方法在调用时不会立即被执行，而是返回一个协程对象。
task：任务，它是对协程对象的进一步封装，包含了任务的各个状态。
future：代表将来执行或没有执行的任务的结果，实际上和task没有本质区别。
另外我们还需要了解async/await关键字，它是从Python3.5才出现的，专门用于定义协程。其中，async定义一个协程，await用来挂起阻塞方法的执行。
协程就是一个函数，只是它满足以下几个特征：
它的作用就是对有大量I/O操作的程序进行加速。
Python协程属于可等待对象，因此可以在其他协程中被等待。
什么叫可等待对象？——await，如果前面被标记await就表明他是个协程，我们需要等待它返回一个数据。
举个例子，我从网络上下载某个数据文件下载到我的本地电脑上，这很显然是一个I/O操作。比方这个文件较大（2GB），可能需要耗时30min才能下载成功。而在这30min里面，它会卡在await后面。这个await标记了协程，那就意味着它可以被暂停，那既然该任务可以被暂停，我们就把它分离出去。我这个线程继续执行其它任务，它这个30min分出去慢慢的传输，我这个程序再运行其他操作。
上面的代码，Python3.6会给你报错。报错信息如下：
为什么会出现这样的报错呢？
因为从Python3.7+之后Python已经完全支持异步了，Python3.6之前只是支持部分异步，许多的方法是非常冗长的。
异步主要做得是I/O类型，CPU密集型就不需要使用异步。
我们使用代码验证一下，不加await调用试一试：
首先我们来定义一个协程，体验一下它和普通进程在实现上的不同之处，代码如下：
随后我们直接调用了这个方法，然而这个方法并没有执行，而是返回了一个coroutine协程对象。随后我们使用get_event_loop方法创建了一个事件循环loop，并调用了loop对象的run_until_complete方法将协程注册到事件循环loop中，然后启动。最后我们才看到了execute方法打印了输出结果。
可见，async定义的方法就会变成一个无法直接执行的coroutine对象，必须将其注册到事件循环中才可以执行。
上面我们还提到了task，它是对coroutine对象的进一步封装，它里面相比coroutine对象多了运行状态，比如running、finished等，我们可以用这些状态来获取协程对象的执行情况。
在上面的例子中，当我们将coroutine对象传递给run_until_complete方法的时候，实际上它进行了一个操作就是将coroutine封装成了task对象，我们也可以显式地进行声明，如下所示：
这里我们定义了loop对象之后，接着调用了它的create_task方法将coroutine对象转化为了task对象，随后我们打印输出一下，发现它是pending状态。接着我们将task对象添加到事件循环中得到执行，随后我们再打印输出一下task对象，发现它的状态就变成了finished，同时还可以看到其result变成了1，也就是我们定义的execute方法的返回结果。
另外定义task对象还有一种方式，就是直接通过asyncio的ensure_future方法，返回结果也是task对象，这样的话我们就可以不借助于loop来定义，即使我们还没有声明loop也可以提前定义好task对象，写法如下：
发现其运行效果都是一样的。
另外我们也可以为某个task绑定一个回调方法，比如我们来看下面的例子：
在这里我们定义了一个request方法，请求了百度，获取其状态码，但是这个方法里面我们没有任何print语句。随后我们定义了一个callback方法，这个方法接收一个参数，是task对象，然后调用print方法打印了task对象的结果。这样我们就定义好了一个coroutine对象和一个回调方法，我们现在希望的效果是，当coroutine对象执行完毕之后，就去执行声明的callback方法。
那么它们二者怎样关联起来呢？
实际上不用回调方法，直接在task运行完毕之后也可以直接调用result方法获取结果，如下所示：
运行结果是一样的：
上面的例子我们只执行了一次请求，如果我们想执行多次请求应该怎么办呢？我们可以定义一个task列表，然后使用asyncio的wait方法即可执行，看下面的例子：
这里我们使用一个for循环创建了五个task，组成了一个列表，然后把这个列表首先传递给了asyncio的wait()方法，然后再将其注册到时间循环中，就可以发起五个任务了。最后我们再将任务的运行结果输出出来，运行结果如下：
可以看到五个任务被顺次执行了，并得到了运行结果。
在上面，我们知道调用协程需要通过创建一个事件循环然后再去运行。这里我们需要了解的是如果在协程里想调用一个协程我们需要使用await关键字，就拿上面的例子来说在main函数里调用协程result1和result2。那么问题来了：await干了什么呢？
我们前面使用到了许多次await那它的作用到底是什么呢？
await的作用就是等待当前的协程运行结束之后再继续进行下面代码。因为我们执行result1的时间很短，所以在表面上看result1和result2是一起执行的。这就是await的作用。等待一个协程的执行完毕，如果有返回结果，那么就会接收到协程的返回结果，通过使用return可以返回协程的一个结果，这个和同步函数的return使用方法一样。
一系列的协程可以通过await链式调用，但是有的时候我们需要在一个协程里等待多个协程，比如我们在一个协程里等待1000个异步网络请求，对于访问次序没有要求的时候，就可以使用关键字wait来解决了。wait可以暂停一个协程，直到后台操作完成。
如果运行的话会发现首先会打印10次数字，但是并不是顺序执行的，这也说明asyncio.wait并发执行的时候是乱序的。如果想保证顺序只要使用gather把task写成解包的形式就行了，也就是上面的注释部分的代码。
我们知道在普通函数中调用普通函数之间，函数名加括号即可，像下面这样：
那么在协程中如何使用一个普通函数呢？在协程中可以通过一些方法去调用普通的函数。可以使用的关键字有call_soon等。
通过输出结果我们可以发现我们在协程中成功调用了一个普通函数，顺序地打印了1和2。
看过这些例子之后，也许你就有疑问了，协程没有缺点的么？
同样的总结下大概以下2点。
协程的本质是个单线程，它不能同时用上单个CPU的多个核，协程需要和进程配合才能运行在多CPU上。当然我们日常所编写的绝大部分应用都没有这个必要，就比如网络爬虫来说，限制爬虫的速度还有其他的因素，比如网站并发量、网速等问题都会是爬虫速度限制的因素。除非做一些密集型应用，这个时候才可能会用到多进程和协程。
写协程就意味着你要一值写一些非阻塞的代码，使用各种异步版本的库，比如后面的异步爬虫教程中用的aiohttp就是一个异步版本的request库等。不过这些缺点并不能影响到使用协程的优势。
上面想必你已经完全掌握了，接下来，我们用睡眠来模仿一下耗时的IO操作。
注意区别time.sleep()这个是不能使用到异步里面的sleep，如果你直接用time模块里面的说了sleep那代码是真正睡眠了，不会执行其他任务了。所以需要使用asyncio.sleep()的睡眠才可以。requests包也是同理，所以接下来我会给大家讲解一个新的包（aiohttp），我们将用aiohttp来代替requests。
这时候细心的小伙伴有可能会说，我们添加任务进去的时候是0、1、2、3，可是在执行的时候却是3、1、0、2这就是我上面说的异步是不可控，随机的。
我在使用异步的时候，上面一共说到了三种：
那到这里，同学们已经掌握了：多线程、多进程、线程池、进程池、异步。那有同学可能会问：可不可以把这几个方法结合起来呢？
那我告诉你们的是，异步只能用异步的方法执行，不过大家是否用过concurrent.future模块呢？这个模块是底层是异步，所以这也是我接下来所要说的。
Concurrent.futures这个模块可以和异步连接，具有线程池和进程池。管理并发编程，处理非确定性的执行流程，同步功能。
编写程序测试时间，建议不要同时运行，注释掉其他运行方法再运行：
我们来分析一下输出结果，我们会分析进程池花费的时间会比线程池更多，这是为什么呢？
多线程非常适合I/O密集型，不适合CPU密集型；
进程池创建销毁的资源开销大，创建一个进程所耗费的资源要比创建一个线程耗费的时间大很多，销毁它也需要很长的时间。（准备工作非常多）
对于协程的入门来说，这些知识已经够用了。当然协程涉及到的知识不止这些，这里只是为了大家提前对协程有一定的了解，后面将继续讲解协程的其他知识，一切的协程知识基础都是为后面的异步爬虫教程做准备，只有熟悉了使用协程才能在后面教程中快速上手操作。接下来将进一步提到本文没有提及的事件循环、Task、Future、Awaitable等一系列知识点，以及协程的高层API知识。敬请期待！
Python爬虫案例，腾讯动漫爬虫，步骤超详细解释，源
Python爬取下载m3u8加密视频，原来这么简单
如何通过Python批量生成测试报文？
python的5种数据结构，方法很多记不住吗？全在这