Skip to content

Commit ff66726

Browse files
committed
整理文件
1 parent bd33724 commit ff66726

5 files changed

Lines changed: 419 additions & 14 deletions

File tree

Fluent-Python/.ipynb_checkpoints/18. Concurrency with asyncio-checkpoint.ipynb

Lines changed: 290 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
},
88
"source": [
99
"<h1>Table of Contents<span class=\"tocSkip\"></span></h1>\n",
10-
"<div class=\"toc\"><ul class=\"toc-item\"><li><span><a href=\"#Thread-Versus-Coroutine:-A-Comparison\" data-toc-modified-id=\"Thread-Versus-Coroutine:-A-Comparison-1\"><span class=\"toc-item-num\">1&nbsp;&nbsp;</span>Thread Versus Coroutine: A Comparison</a></span></li></ul></div>"
10+
"<div class=\"toc\"><ul class=\"toc-item\"><li><span><a href=\"#Thread-Versus-Coroutine:-A-Comparison\" data-toc-modified-id=\"Thread-Versus-Coroutine:-A-Comparison-1\"><span class=\"toc-item-num\">1&nbsp;&nbsp;</span>Thread Versus Coroutine: A Comparison</a></span></li><li><span><a href=\"#download_flag-with-asyncio\" data-toc-modified-id=\"download_flag-with-asyncio-2\"><span class=\"toc-item-num\">2&nbsp;&nbsp;</span>download_flag with asyncio</a></span><ul class=\"toc-item\"><li><span><a href=\"#downlaod_flags2:-增加-error-handling\" data-toc-modified-id=\"downlaod_flags2:-增加-error-handling-2.1\"><span class=\"toc-item-num\">2.1&nbsp;&nbsp;</span>downlaod_flags2: 增加 error handling</a></span></li><li><span><a href=\"#download_flags3:-处理多个请求\" data-toc-modified-id=\"download_flags3:-处理多个请求-2.2\"><span class=\"toc-item-num\">2.2&nbsp;&nbsp;</span>download_flags3: 处理多个请求</a></span></li></ul></li><li><span><a href=\"#用-asyncio-写服务器\" data-toc-modified-id=\"用-asyncio-写服务器-3\"><span class=\"toc-item-num\">3&nbsp;&nbsp;</span>用 asyncio 写服务器</a></span></li></ul></div>"
1111
]
1212
},
1313
{
@@ -187,6 +187,295 @@
187187
"13. 启动 supervisor 协程函数直到他终止并且获取其返回值."
188188
]
189189
},
190+
{
191+
"cell_type": "markdown",
192+
"metadata": {},
193+
"source": [
194+
"<center>重点:asyncio.Future 与 concurrent.future.Future 的不同</center>\n",
195+
"\n",
196+
"1. futures 对象是 results of scheduling something for execution. 在 asyncio 中, BaseEventLoop.creat_task() 接收一个协程函数, 调度它运行, 并且立刻返回一个 asyncio.Task 实例, Task 是 Future 的子类.\n",
197+
"2. 在 asyncio.Future 的 result() 方法没有参数, 你不能指定 timeout, 并且如果 future 没有完成, 它不会像 concurrent 中的 Future 那样阻塞主线程, 而是抛出\n",
198+
" asyncio.InvalidStateError 异常.\n",
199+
"3. 但是如果使用 yield from 语句而不是 result() 方法, 那么 future 会自动等待完成, 并将结果赋值给 yield from 左边的表达式. **事件循环**不会阻塞; 运行 yield from 的 协程函数会挂起, 直到 yield from 完成.\n",
200+
"\n",
201+
"有两种方法来创建 Task 对象:\n",
202+
"\n",
203+
"1. asyncio.async(coro_or_future, * , loop=None). 如果你传递了协程对象, async 将调用 loop.create_task() 来创建 Task 对象. 你亦可以指定事件循环, 如果不指定, 会调用 asyncio.get_event_loop() 获得**唯一的**事件循环.\n",
204+
"2. BaseEventLoop.creat_task(coro) 调度协程函数并且立刻返回一个 Task 对象."
205+
]
206+
},
207+
{
208+
"cell_type": "markdown",
209+
"metadata": {},
210+
"source": [
211+
"##### download_flag with asyncio\n",
212+
"下面给出 donwload_flag 的 asyncio 版本, 注意:\n",
213+
"\n",
214+
"1. 需要 python 3.7\n",
215+
"2. 无法在 jupyter notebook 中运行, 需要在命令行下运行"
216+
]
217+
},
218+
{
219+
"cell_type": "code",
220+
"execution_count": 3,
221+
"metadata": {
222+
"ExecuteTime": {
223+
"end_time": "2019-08-06T06:19:22.903533Z",
224+
"start_time": "2019-08-06T06:19:22.900072Z"
225+
}
226+
},
227+
"outputs": [],
228+
"source": [
229+
"import os\n",
230+
"import time\n",
231+
"import sys\n",
232+
"\n",
233+
"import asyncio # 1\n",
234+
"import aiohttp # 2\n",
235+
"\n",
236+
"POP20_CC = ('CN IN US ID BR PK NG BD RU JP '\n",
237+
" 'MX PH VN ET EG DE IR TR CD FR').split()\n",
238+
"BASE_URL = 'http://flupy.org/data/flags'\n",
239+
"DEST_DIR = 'downloads/'\n",
240+
"\n",
241+
"\n",
242+
"def save_flag(img, filename):\n",
243+
" path = os.path.join(DEST_DIR, filename)\n",
244+
" with open(path, 'wb') as fp:\n",
245+
" fp.write(img)\n",
246+
"\n",
247+
"\n",
248+
"async def get_flag(session, cc): # 3 async 关键字 等同于 @asyncio.coroutine\n",
249+
" url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())\n",
250+
" # 4 需要在 with 前加上 async, 因为 session.get是 IO bound 的,\n",
251+
" async with session.get(url) as resp:\n",
252+
" # 这一句等价于 resp = yield from aiohttp.request('GET', url)\n",
253+
" return await resp.read() # 5 等价于 return yield from resp.read()\n",
254+
"\n",
255+
"\n",
256+
"def show(text):\n",
257+
" print(text, end=' ')\n",
258+
" sys.stdout.flush()\n",
259+
"\n",
260+
"\n",
261+
"async def download_one(session, cc): # 6 download_one 也是一个协程函数, 因为它调用了 get_flag 协程\n",
262+
" # 7 等价于 yield from get_flag(session, cc)\n",
263+
" image = await get_flag(session, cc)\n",
264+
" show(cc)\n",
265+
" save_flag(image, cc.lower() + '.gif')\n",
266+
" return cc\n",
267+
"\n",
268+
"\n",
269+
"async def download_many(cc_list):\n",
270+
" async with aiohttp.ClientSeesion() as session: # 8 aiohttp 的 异步 session\n",
271+
" res = await asyncio.gather( # 9 你无须显示的调用事件循环了.\n",
272+
" *[asyncio.creat_task(download_one(session, cc)) for cc in sorted(cc_list)]\n",
273+
" )\n",
274+
" return len(res)\n",
275+
"\n",
276+
"\n",
277+
"def main():\n",
278+
" t0 = time.time()\n",
279+
" count = asyncio.run(download_many(POP20_CC))\n",
280+
" elapsed = time.time() - t0\n",
281+
" msg = '\\n{} flags downloaded in {:.2f}s'\n",
282+
" print(msg.format(count, elapsed))"
283+
]
284+
},
285+
{
286+
"cell_type": "code",
287+
"execution_count": 14,
288+
"metadata": {
289+
"ExecuteTime": {
290+
"end_time": "2019-08-06T06:43:39.003691Z",
291+
"start_time": "2019-08-06T06:43:39.001286Z"
292+
}
293+
},
294+
"outputs": [],
295+
"source": [
296+
"# main()"
297+
]
298+
},
299+
{
300+
"cell_type": "markdown",
301+
"metadata": {},
302+
"source": [
303+
"对比一下老版本的 asyncio 实现, 注意:\n",
304+
"\n",
305+
"1. 仍然需要在命令行下实现\n",
306+
"2. asyncio.wait 函数返回一个迭代器. wait 函数可以指定 **timeout 和 return_when** ;迭代器作为 loop.run_until_complete的参数后, run_until_complete\n",
307+
" 将返回一个元组: (已完成的futures, 未完成的futures)---默认行为是等待所有 futures 完成"
308+
]
309+
},
310+
{
311+
"cell_type": "code",
312+
"execution_count": 17,
313+
"metadata": {
314+
"ExecuteTime": {
315+
"end_time": "2019-08-06T07:27:48.581705Z",
316+
"start_time": "2019-08-06T07:27:48.574172Z"
317+
}
318+
},
319+
"outputs": [],
320+
"source": [
321+
"import asyncio, aiohttp\n",
322+
"\n",
323+
"@asyncio.coroutine\n",
324+
"def get_flag(cc):\n",
325+
" url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())\n",
326+
" resp = yield from aiohttp.request('GET', url)\n",
327+
" image = yield from resp.read()\n",
328+
" return image\n",
329+
"\n",
330+
"@asyncio.coroutine\n",
331+
"def download_one(cc):\n",
332+
" image = yield from get_flag(cc)\n",
333+
" show(cc)\n",
334+
" save_flag(image, cc.lower() + '.gif')\n",
335+
" return cc\n",
336+
"\n",
337+
"def download_many(cc_list):\n",
338+
" \"\"\"这一版本的download_many需要显式地处理事件循环\n",
339+
" \"\"\"\n",
340+
" loop = asyncio.get_event_loop() #1 创建当前线程事件循环的引用\n",
341+
" to_do = [download_one(cc) for cc in sorted(cc_list)] #2 创建生成器函数的list\n",
342+
" wait_coro = asyncio.wait(to_do) #3 这不是一个阻塞函数. 他是一个协程函数, 接受可迭代的对象, 可迭代的对象里储存了 futures或协程函数 ,\n",
343+
" # 当这些协程函数完成时, 它也完成.\n",
344+
" res, _ = loop.run_until_complete(wait_coro) #4 执行事件循环直到 wait_coro 终止\n",
345+
" loop.close() #5 关闭事件循环\n",
346+
"\n",
347+
"def main(download_many): \n",
348+
" t0 = time.time()\n",
349+
" count = download_many(POP20_CC)\n",
350+
" elapsed = time.time() - t0\n",
351+
" msg = '\\n{} flags downloaded in {:.2f}s'\n",
352+
" print(msg.format(count, elapsed))"
353+
]
354+
},
355+
{
356+
"cell_type": "markdown",
357+
"metadata": {},
358+
"source": [
359+
"<center>总结</center>\n",
360+
"\n",
361+
"1. yield from 会使得当前协程挂起, 控制流回到事件循环.\n",
362+
"2. 生成器函数调用链的顶层必须是一个非协程的调用函数. 在 asyncio 里, 它调用了 loop.run_until_complete(); 无需显示调用 next 和 send.\n",
363+
"3. 生成器函数调用链的底层必须是一个最简单的协程函数. 在 asyncio 里, 他要么是 asyncio 函数(比如 asyncio.sleep) 或者是 第三方库提供的协程函数(比如 aiohttp.request 和 resp.read()), 这些函数执行了 IO 操作.\n",
364+
"4. save\\_flag 操作也是IO-bound 的, 但是当时没有提供异步文件操作的API."
365+
]
366+
},
367+
{
368+
"cell_type": "markdown",
369+
"metadata": {},
370+
"source": [
371+
"###### downlaod_flags2: 增加 error handling\n",
372+
"请查看 flags2_\\*.py 系列代码"
373+
]
374+
},
375+
{
376+
"cell_type": "markdown",
377+
"metadata": {},
378+
"source": [
379+
"###### download_flags3: 处理多个请求\n",
380+
"现在你不仅仅需要保存国旗图片, 你还需要保存相应的contry code. 因此在 get\\_flag 函数中需要做两件事:\n",
381+
"\n",
382+
"1. 获得国旗图片\n",
383+
"2. 获得 metadata.json 文件\n",
384+
"\n",
385+
"下面实现了多个请求的异步并发的代码"
386+
]
387+
},
388+
{
389+
"cell_type": "code",
390+
"execution_count": 2,
391+
"metadata": {
392+
"ExecuteTime": {
393+
"end_time": "2019-08-09T04:03:45.578381Z",
394+
"start_time": "2019-08-09T04:03:45.566879Z"
395+
}
396+
},
397+
"outputs": [],
398+
"source": [
399+
"import asyncio\n",
400+
"import aiohttp\n",
401+
"from aiohttp import web\n",
402+
"\n",
403+
"\n",
404+
"async def http_get(url):\n",
405+
" \"\"\"处理 http 请求的协程函数.\n",
406+
" 如果 response 的 status 是 200, 那么根据 response\n",
407+
" 的 header 来判断 返回的是 metadata 还是 image\n",
408+
" \"\"\"\n",
409+
" res = await aiohttp.request('GET', url)\n",
410+
" if res.status == 200:\n",
411+
" ctype = res.headers.get('Content-type', '').lower()\n",
412+
" if 'json' in ctype or url.endswith('json'):\n",
413+
" data = await res.json() # 1\n",
414+
" else:\n",
415+
" data = await res.head() # 2\n",
416+
" return data\n",
417+
" elif res.status == 404:\n",
418+
" raise web.HTTPNotFound()\n",
419+
" else:\n",
420+
" raise aiohttp.errors.HttpProcessingError(\n",
421+
" code=res.status,\n",
422+
" message=res.reason,\n",
423+
" headers=res.headers)\n",
424+
"\n",
425+
"\n",
426+
"async def get_country(base_url, cc):\n",
427+
" url = '{}/{cc}/metadata.json'.format(base_url, cc=cc.lower())\n",
428+
" metadata = await http_get(url) # 3\n",
429+
" return metadata['country']\n",
430+
"\n",
431+
"\n",
432+
"async def get_flag():\n",
433+
" url = '{}/{cc}/{cc}.gif'.format(base_url, cc=cc.lower())\n",
434+
" return (await http_get(url)) \n",
435+
"\n",
436+
"\n",
437+
"async def download_one(cc, base_url, semaphore, verbose):\n",
438+
" try:\n",
439+
" async with semaphore: # 4\n",
440+
" image = await get_flag(base_url, cc)\n",
441+
" async with semaphore:\n",
442+
" country = await get_country(base_url, cc)\n",
443+
" except web.HTTPNotFound:\n",
444+
" status = HTTPStatus.not_found\n",
445+
" msg = 'not found'\n",
446+
" except Exception as exc:\n",
447+
" raise FetchError(cc) from exc\n",
448+
" else:\n",
449+
" country = country.replace(' ', '_')\n",
450+
" filename = '{}-{}.gif'.format(country, cc)\n",
451+
" loop = asyncio.get_event_loop()\n",
452+
" loop.run_in_executor(None, save_flag, image, filename)\n",
453+
" status = HTTPStatus.ok\n",
454+
" msg = 'OK'\n",
455+
" if verbose and msg:\n",
456+
" print(cc, msg)\n",
457+
" return Result(status, cc)"
458+
]
459+
},
460+
{
461+
"cell_type": "markdown",
462+
"metadata": {},
463+
"source": [
464+
"<center>总结</center>\n",
465+
"\n",
466+
"1. 如果 `content type` 里有 'json', 或者 `url` 参数以 '.json' 结尾, 那么 yield from response.json(), 返回一个字典.\n",
467+
"2. 否则, yield from response.read().\n",
468+
"3. metadata 得到一个 json dict.\n",
469+
"4. 将 get\\_flag 和 get\\_country 写在两个 semaphore 的块下, 这样每次 acquire semaphore 的时间尽量短."
470+
]
471+
},
472+
{
473+
"cell_type": "markdown",
474+
"metadata": {},
475+
"source": [
476+
"##### 用 asyncio 写服务器"
477+
]
478+
},
190479
{
191480
"cell_type": "code",
192481
"execution_count": null,

0 commit comments

Comments
 (0)