对python并发编程的思考

为了提升系统密集型运算的效率,咱们经常会使用到多个进程或者是多个线程,python中的Threading包实现了线程,multiprocessing 包则实现了多进程。而在3.2版本的python中,将进程与线程进一步封装成concurrent.futures 这个包,使用起来更加方便。咱们以请求网络服务为例,来实际测试一下加入多线程以后的效果。python

首先来看看不使用多线程花费的时间:程序员

import time
import requests

NUMBERS = range(12)
URL = 'http://httpbin.org/get?a={}'

# 获取网络请求结果
def fetch(a):
    r = requests.get(URL.format(a))
    return r.json()['args']['a']

# 开始时间
start = time.time()

for num in NUMBERS:
    result = fetch(num)
    print('fetch({}) = {}'.format(num, result))
# 计算花费的时间
print('cost time: {}'.format(time.time() - start))

执行结果以下:json

fetch(0) = 0
fetch(1) = 1
fetch(2) = 2
fetch(3) = 3
fetch(4) = 4
fetch(5) = 5
fetch(6) = 6
fetch(7) = 7
fetch(8) = 8
fetch(9) = 9
fetch(10) = 10
fetch(11) = 11
cost time: 6.952988862991333

再来看看加入多线程以后的效果:安全

import time
import requests
from concurrent.futures import ThreadPoolExecutor

NUMBERS = range(12)
URL = 'http://httpbin.org/get?a={}'

def fetch(a):
    r = requests.get(URL.format(a))
    return r.json()['args']['a']

start = time.time()
# 使用线程池(使用5个线程)
with ThreadPoolExecutor(max_workers=5) as executor:
  # 此处的map操做与原生的map函数功能同样
    for num, result in zip(NUMBERS, executor.map(fetch, NUMBERS)):
        print('fetch({}) = {}'.format(num, result))
print('cost time: {}'.format(time.time() - start))

执行结果以下:服务器

fetch(0) = 0
fetch(1) = 1
fetch(2) = 2
fetch(3) = 3
fetch(4) = 4
fetch(5) = 5
fetch(6) = 6
fetch(7) = 7
fetch(8) = 8
fetch(9) = 9
fetch(10) = 10
fetch(11) = 11
cost time: 1.9467740058898926

只用了近2秒的时间,若是再多加几个线程时间会更短,而不加入多线程须要接近7秒的时间。网络

不是说python中因为全局解释锁的存在,每次只能执行一个线程吗,为何上面使用多线程还快一些?多线程

确实,因为python的解释器(只有cpython解释器中存在这个问题)自己不是线程安全的,因此存在着全局解释锁,也就是咱们常常听到的GIL,致使一次只能使用一个线程来执行Python的字节码。可是对于上面的I/O操做来讲,一个线程在等待网络响应时,执行I/O操做的函数会释放GIL,而后再运行一个线程。并发

因此,执行I/O密集型操做时,多线程是有用的,对于CPU密集型操做,则每次只能使用一个线程。那这样说来,想执行CPU密集型操做怎么办?异步

答案是使用多进程,使用concurrent.futures包中的ProcessPoolExecutor 。这个模块实现的是真正的并行计算,由于它使用ProcessPoolExecutor 类把工做分配给多个 Python 进程处理。所以,若是须要作 CPU密集型处理,使用这个模块能绕开 GIL,利用全部可用的 CPU 核心。async

说到这里,对于I/O密集型,可使用多线程或者多进程来提升效率。咱们上面的并发请求数只有5个,可是若是同时有1万个并发操做,像淘宝这类的网站同时并发请求数能够达到千万级以上,服务器每次为一个请求开一个线程,还要进行上下文切换,这样的开销会很大,服务器压根承受不住。一个解决办法是采用分布式,大公司有钱有力,能买不少的服务器,小公司呢。

咱们知道系统开进程的个数是有限的,线程的出现就是为了解决这个问题,因而在进程之下又分出多个线程。因此有人就提出了能不能用同一线程来同时处理若干链接,再往下分一级。因而协程就出现了。

协程在实现上试图用一组少许的线程来实现多个任务,一旦某个任务阻塞,则可能用同一线程继续运行其余任务,避免大量上下文的切换,并且,各个协程之间的切换,每每是用户经过代码来显式指定的,不须要系统参与,能够很方便的实现异步。

协程本质上是异步非阻塞技术,它是将事件回调进行了包装,让程序员看不到里面的事件循环。说到这里,什么是异步非阻塞?同步异步,阻塞,非阻塞有什么区别?

借用知乎上的一个例子,假如你打电话问书店老板有没有《分布式系统》这本书,若是是同步通讯机制,书店老板会说,你稍等,”我查一下",而后开始查啊查,等查好了(多是5秒,也多是一天)告诉你结果(返回结果)。而异步通讯机制,书店老板直接告诉你我查一下啊,查好了打电话给你,而后直接挂电话了(不返回结果)。而后查好了,他会主动打电话给你。在这里老板经过“回电”这种方式来回调。

而阻塞与非阻塞则是你打电话问书店老板有没有《分布式系统》这本书,你若是是阻塞式调用,你会一直把本身“挂起”,直到获得这本书有没有的结果,若是是非阻塞式调用,你无论老板有没有告诉你,你本身先一边去玩了, 固然你也要偶尔过几分钟check一下老板有没有返回结果。在这里阻塞与非阻塞与是否同步异步无关。跟老板经过什么方式回答你结果无关。

总之一句话,阻塞和非阻塞,描述的是一种状态,而同步与非同步描述的是行为方式。

回到协程上。

相似于Threading 包是对线程的实现同样,python3.4以后加入的asyncio 包则是对协程的实现。咱们用asyncio改写文章开头的代码,看看使用协程以后能花费多少时间。

import asyncio
import aiohttp
import time

NUMBERS = range(12)
URL = 'http://httpbin.org/get?a={}'
# 这里的代码不理解不要紧
# 主要是为了证实协程的强大
async def fetch_async(a):
    async with aiohttp.request('GET', URL.format(a)) as r:
        data = await r.json()
    return data['args']['a']

start = time.time()
loop = asyncio.get_event_loop()
tasks = [fetch_async(num) for num in NUMBERS]
results = loop.run_until_complete(asyncio.gather(*tasks))

for num, results in zip(NUMBERS, results):
    print('fetch({}) = ()'.format(num, results))

print('cost time: {}'.format(time.time() - start))

执行结果:

fetch(0) = ()
fetch(1) = ()
fetch(2) = ()
fetch(3) = ()
fetch(4) = ()
fetch(5) = ()
fetch(6) = ()
fetch(7) = ()
fetch(8) = ()
fetch(9) = ()
fetch(10) = ()
fetch(11) = ()
cost time: 0.8582110404968262

不到一秒!感觉到协程的威力了吧。

asyncio的知识说实在的有点难懂,由于它是用异步的方式在编写代码。上面给出的asyncio示例不理解也没有关系,以后的文章会详细的介绍一些asyncio相关的概念。

相关文章
相关标签/搜索