|
7 | 7 | }, |
8 | 8 | "source": [ |
9 | 9 | "<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 </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 </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 </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 </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 </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 </span>用 asyncio 写服务器</a></span></li></ul></div>" |
11 | 11 | ] |
12 | 12 | }, |
13 | 13 | { |
|
187 | 187 | "13. 启动 supervisor 协程函数直到他终止并且获取其返回值." |
188 | 188 | ] |
189 | 189 | }, |
| 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 | + }, |
190 | 479 | { |
191 | 480 | "cell_type": "code", |
192 | 481 | "execution_count": null, |
|
0 commit comments