快速回答:共享异步客户端应该由谁关闭?
在本文约定的借用模式中,外层拥有者创建并关闭客户端;接受客户端参数的批量函数只负责等待自己创建的任务,不关闭传入的对象。TaskGroup 退出时会等待组内任务完成;这不等于证明真实网络连接已释放。
| 角色 | 本例职责 | 什么时候结束 |
|---|---|---|
| 资源拥有者 | 创建假客户端并调用关闭 | 任务组退出后 |
| 批量函数 | 借用客户端并等待组内任务 | 所有组内任务退出后返回 |
| 单个任务 | 使用客户端,不关闭共享资源 | 自己结束或退出时 |
适用环境:Python 3.11 及以上,标准库,无需密钥或网络。输入是两个合成名称;输出为转换后的字符串和用于测试的事件顺序。本例不适用于推断厂商 SDK 线程安全、远程取消、真实连接池或计费行为。
本文给出标准库可运行示例,覆盖资源拥有、借用与异常退出,并附完整测试和排错。
问题:谁创建,谁关闭?
多个协程共用客户端时,如果其中一个请求顺手关闭它,其余请求可能还在使用连接。相反,所有人都只借用而没有所有者,又可能一直不释放。先定义合同:创建资源的外层负责关闭;接收外部 client 的函数只借用,不负责关闭;批量任务全部退出后,才能退出拥有者作用域。
本例仅使用 Python 3.11+ 标准库和 FakeClient,不连接网络;它演示本地协程顺序,不是对任何 SDK、连接池、远程取消或计费的验证。上次资源文章侧重模拟传输和关闭顺序,本文专门讨论拥有者与借用者的 API 合同。
可复制的完整离线示例
保存为 ownership_demo.py,运行 python ownership_demo.py:
"""Offline ownership demonstration; no HTTP client or network requests."""
import asyncio
from contextlib import asynccontextmanager
import unittest
class FakeClient:
def __init__(self, events):
self.events = events
self.closed = False
async def request(self, name):
if self.closed:
raise RuntimeError('client already closed')
self.events.append(f'{name}:start')
try:
await asyncio.sleep(0)
return name.upper()
finally:
self.events.append(f'{name}:done')
async def aclose(self):
self.closed = True
self.events.append('client:close')
@asynccontextmanager
async def owned_client(events):
client = FakeClient(events)
try:
yield client
finally:
await client.aclose()
async def run_batch(client, names):
"""Borrowed client: tasks finish before return; caller owns close."""
async with asyncio.TaskGroup() as group:
tasks = [group.create_task(client.request(name)) for name in names]
return [task.result() for task in tasks]
async def run_owned(names, events):
async with owned_client(events) as client:
return await run_batch(client, names)
class OwnershipTests(unittest.IsolatedAsyncioTestCase):
async def test_owned_closes_after_tasks(self):
events = []
self.assertEqual(await run_owned(['a', 'b'], events), ['A', 'B'])
self.assertEqual(events[-1], 'client:close')
self.assertLess(events.index('a:done'), events.index('client:close'))
self.assertLess(events.index('b:done'), events.index('client:close'))
async def test_borrowed_is_not_closed(self):
events = []
client = FakeClient(events)
self.assertEqual(await run_batch(client, ['a']), ['A'])
self.assertFalse(client.closed)
await client.aclose()
async def test_close_when_body_raises(self):
events = []
with self.assertRaisesRegex(ValueError, 'failed'):
async with owned_client(events):
raise ValueError('failed')
self.assertEqual(events, ['client:close'])
if __name__ == '__main__':
unittest.main(verbosity=2)
这次本地运行了三个 unittest 方法,输出 Ran 3 tests 和 OK。第一个测试检查两个任务各自的 done 出现在 client:close 之前;它不要求两个任务之间固定顺序。第二个检查借用者返回后客户端仍打开;第三个检查作用域内直接抛出异常时,演示客户端的关闭方法仍被调用。
关键边界
run_owned创建并拥有资源;run_batch接受外部资源,只等待其 TaskGroup 中的任务,不关闭传入对象。调用者要为外部资源安排生命周期。asyncio.TaskGroup在上下文退出时等待组内任务;遇到任务异常时会处理其余任务并传播异常。生产应用仍需按 Python 版本和实际客户端语义处理异常组和取消。- 示例
aclose是无阻塞的本地假实现。真实关闭可能抛错、阻塞或被再次取消;需要明确超时、异常保留、幂等和服务退出策略。不能由本例推断真实网络连接一定释放。 - 不要在单个子任务中关闭共享 client;也不要把仍在运行的后台任务留在资源作用域外。需要长生命周期客户端时,应由应用生命周期层创建与关闭,并按实际 SDK 文档评估并发安全。
常见问题与排查
提示 TaskGroup 不存在? 检查 python --version,该示例需要 Python 3.11 或更新版本。
运行顺序与例子不完全相同? 任务之间的执行顺序不固定;只断言每个任务完成早于关闭,不断言 a 与 b 的先后。
为何测试通过仍出现连接问题? 假客户端没有 socket、连接池或超时。对真实 SDK 应在授权环境以本地服务或 mock 传输补充集成验证,不能把本例当成线上证明。
小练习
增加空任务列表场景:在 owned 模式下仍应关闭一次,在 borrowed 模式下不能关闭。然后为真实接入写一张所有权表:资源在哪里创建、谁借用、在哪退出、异常如何记录。下一篇将讨论如何为多模型比较固定输入和评价标准。
参考:Python TaskGroup 官方文档;asynccontextmanager 官方文档。
本文由 AI 辅助整理;本地示例与测试结果已核对。
623



被折叠的 条评论
为什么被折叠?



