什么是并发和并行的区别
并发是多个任务同时进行,但并不意味着同时并行处理。
举个例子,并发好比是1个面包师同时在做2个或以上的面包,面包师并不会限制自己必须第一个面包完成后再从头开始做第二个面包。他可以在预热烤箱的同时,进行面包的发酵等处理工作。
而并行就好比是两个面包师在同时制作两个面包,所有的任务都是同时并行的在处理
所以,并发本质上就是可以同时处理相对独立的多个任务。并行意味着必须同时执行多个任务,需要多核的CPU才可以。
什么是GIL(全局解释器)
GIL(Global Interpreter Lock)是CPython解释器中的一种机制,是为了解决CPython线程安全的问题,但同时也限制了同时只有1个Python的进程存在,限制了Python在执行CPU密集型任务时的多线程能力。
通过下面的代码来演示,有以下一个计算斐波那契的函数
def print_fib(number: int) -> None:
def fib(n: int) -> int:
if n == 1:
return 0
elif n == 2:
return 1
else:
return fib(n - 1) + fib(n - 2)
print(f'fib({number}) is {fib(number)}')
我们串行的执行,并计算执行时间
def fibs_no_threading():
print_fib(40)
print_fib(41)
start = time.time()
fibs_no_threading()
end = time.time()
print(f'Completed in {end - start:.4f} seconds.')
输出结果如下
fib(40) is 63245986
fib(41) is 102334155
Completed in 37.7836 seconds.
再改用Thread多线程执行
def fibs_with_threads():
fortieth_thread = threading.Thread(target=print_fib, args=(40,))
forty_first_thread = threading.Thread(target=print_fib, args=(41,))
fortieth_thread.start()
forty_first_thread.start()
fortieth_thread.join()
forty_first_thread.join()
start_threads = time.time()
fibs_with_threads()
end_threads = time.time()
print(f'Threads took {end_threads - start_threads:.4f} seconds.')
输出结果如下,可以看到执行时间并没有明显提高
fib(40) is 63245986
fib(41) is 102334155
Threads took 37.6750 seconds.
那Python就不可以多线程了吗?
对于I/O密集型任务来说,由于线程在等待I/O操作时(比如请求接口,读取文件等)会释放GIL,因此就可以通过多线程提升执行效率
使用asyncio和aiohttp请求接口
什么是asyncio
- asyncio是python3.4内置的标准库
- asyncio利用I/O操作释放GIL提供并发性,当使用asyncio时会自动创建协程。
- 协程是一个轻量级的线程,简单的说就是实现了线程内代码执行互相切换
async和await
通过async将普通函数标记为协程,因此该函数会返回1个协程对象
async def say_hello():
print('hello')
hello = say_hello()
print(f'Function result is {hello} and the type is {type(hello)}')
async标记的函数不可直接运行,直接运行会提示需要使用await
RuntimeWarning: coroutine ‘say_hello’ was never awaited
await关键字
- await之后通常会调用协程,准确的说是1个awaitable对象
- await关键字会暂时运行的协程或任务
asyncio如何运行协程
- asyncio通过事件循环去执行任务,任务是协程的包装。
- 可以将事件循环想象为一个监听执行协程的While循环
- 事件循环每次运行一个loop时会检查运行的任务或协程,当任务遇到IO操作暂停时,指示操作系统去执行IO,并同时执行下一个任务或协程
以下是执行1个协程的示例代码
import asyncio
import time
async def sleep(number: int):
print(f'sleep {number} seconds')
await asyncio.sleep(number)
async def say_hello():
print('hello')
await sleep(2)
await sleep(1)
start = time.time()
asyncio.run(say_hello())
end = time.time()
print(f'Completed in {end - start:.4f} seconds.')
运行以上代码输出
hello
sleep 2 seconds
sleep 1 seconds
Completed in 3.0045 seconds.
asyncio.run方法会创建一个事件,然后把我们创建的协程对象也就是say_hello()返回的协程扔到此事件循环中并执行。所以可以看到代码也是按顺序执行完成,因为我们没有创建任务。那稍微修改一下代码,显示的创建2个任务去执行
import asyncio
import time
async def sleep(number: int):
print(f'sleep {number} seconds')
await asyncio.sleep(number)
print(f'sleep {number} sec done')
async def say_hello():
print('hello')
task1 = asyncio.create_task(sleep(2))
task2 = asyncio.create_task(sleep(1))
await task1
await task2
start = time.time()
asyncio.run(say_hello())
end = time.time()
print(f'Completed in {end - start:.4f} seconds.')
运行之后得到以下结果,2秒运行完成!
hello
sleep 2 seconds
sleep 1 seconds
sleep 1 sec done
sleep 2 sec done
Completed in 2.0036 seconds.
协程的陷阱-阻塞IO
使用协程或asyncio常见的错误就是运行阻塞型的I/O,比如request,time.sleep。因为这会直接阻塞主线程.
import asyncio
import requests
import time
async def get_example_status() -> int:
t = time.time()
status = requests.get('https://www.baidu.com').status_code
print(f'request coast:{time.time() - t:.4f}s')
return status
async def main():
t = time.time()
task_1 = asyncio.create_task(get_example_status())
task_2 = asyncio.create_task(get_example_status())
task_3 = asyncio.create_task(get_example_status())
await task_1
await task_2
await task_3
print(f'total coast:{time.time() - t:.4f}s')
asyncio.run(main())
执行后打印的结果 , 可以看到并没有提高
request coast:0.0611s
request coast:0.0584s
request coast:0.0524s
total coast:0.1721s
使用aiohttp库
之前已经了解到requests库是阻塞型I/O,在asyncio上表现并不好。aiohttp就是非阻塞web请求的库,可以很好的和asyncio一起使用。
接着就把requests换成aiohttp的请求方式。代码如下
import asyncio
import time
import aiohttp
async def get_example_status():
t = time.time()
async with aiohttp.ClientSession() as session:
async with session.get('https://www.baidu.com') as result:
print(result.status)
print(f'request coast:{time.time() - t:.4f}s')
async def main():
t = time.time()
task_1 = asyncio.create_task(get_example_status())
task_2 = asyncio.create_task(get_example_status())
task_3 = asyncio.create_task(get_example_status())
await task_1
await task_2
await task_3
print(f'total coast:{time.time() - t:.4f}s')
asyncio.run(main())
运行后结果如下,可以看到运行时间明显提高。
200
request coast:0.0566s
200
request coast:0.0620s
200
request coast:0.0591s
total coast:0.0622s