协程必知必会-系列4-协程本地变量

协程本地变量

在上一篇文章中,我们介绍了如何通过协程来实现批量并发执行,本篇文章将向大家介绍如何在协程的基础之上,实现协程本地变量。

注意:「为了减轻大家的阅读负担,在文章中只展示必要的代码,和当前讲解内容无关的代码在代码块中采用…进行忽略」。

完整的代码,已经开源在github上,地址为:https://github.com/wanmuc/MyCoroutine

在开源库中协程本地变量涉及的代码文件如下所示,文章后续代码的出处就不再说明。

.
├── common.h  // LocalVariable结构体定义
├── localvariable.cpp  // 协程本地变量核心函数实现
├── localvariable.h  // 协程本地变量模版类封装
└── mycoroutine.h // 协程本地变量核心函数声明

相关结构体

协程本地变量该如何实现呢?其实实现原理和线程本地变量一样。

就是使用封装的变量的内存地址作为key,且key的值是唯一的。

虽然所有的协程中拿到的key的值都是相等的,但是在不同的协程中通过这个key可以读写到不同的值。

具体如何实现呢?先来看一下协程本地变量辅助结构体LocalVariable。

// 协程本地变量辅助结构体
typedef struct LocalVariable {
  void *data{nullptr};
  function<void(void *)> free{nullptr};  // 用于释放本地协程变量值的内存
} LocalVariable;

在LocalVariable结构体中只有两个成员变量,一个是data,一个是free,data是指向协程本地变量值的内存指针,free是用于释放协程本地变量值内存的函数。

单有LocalVariable是不够的,还需要在协程核心结构体Coroutine中新增一个成员变量local。

// 协程结构体
typedef struct Coroutine {
  ...
  unordered_map<void *, LocalVariable> local;  // 协程本地变量映射map,key是协程变量的内存地址
  ...
} Coroutine;

local变量是一个unordered_map,「它的key就是一个void*的通用指针,value则是LocalVariable类型的变量」。

实现原理

本小节来介绍一下实现原理,协程本地变量是通过this指针,去不同协程的local中索引不同的LocalVariable类型的变量。

然后再进行读写操作的,从而实现在不同的协程内操作协程本地变量的相互隔离。

下面的简图将使这个逻辑变得清晰易懂。

在这里插入图片描述

代码实现

协程本地变量的实现涉及到2个类,一个是Schedule类,一个CoroutineLocal模版类。

在Schedule类中新增了2个函数的声明。

// 协程调度器
class Schedule {
 public:
  ...
  void LocalVariableSet(void *key, const LocalVariable &local_variable);  // 设置协程本地变量
  bool LocalVariableGet(void *key, LocalVariable &local_variable);        // 获取协程本地变量
  ...
};

LocalVariableSet和LocalVariableGet函数分别用于设置和获取协程本地变量的值,是核心的底层函数。它们的实现如下所示。

void Schedule::LocalVariableSet(void* key, const LocalVariable& local_variable) {
  assert(not is_master_);
  auto iter = coroutines_[slave_cid_]->local.find(key);
  if (iter != coroutines_[slave_cid_]->local.end()) {
    iter->second.free(iter->second.data);  // 之前有值,则要先释放空间
  }
  coroutines_[slave_cid_]->local[key] = local_variable;
}

bool Schedule::LocalVariableGet(void* key, LocalVariable& local_variable) {
  assert(not is_master_);
  auto iter = coroutines_[slave_cid_]->local.find(key);
  if (iter == coroutines_[slave_cid_]->local.end()) {
    int32_t relate_bid = coroutines_[slave_cid_]->relate_bid;
    if (relate_bid == kInvalidBid) {  // 没有关联的Batch,直接返回false
      return false;
    }
    int32_t parent_cid = batchs_[relate_bid]->parent_cid;
    iter = coroutines_[parent_cid]->local.find(key);
    if (iter == coroutines_[parent_cid]->local.end()) {  // 父从协程中也没查找到,直接返回false
      return false;
    }
  }
  local_variable = iter->second;
  return true;
}

LocalVariableSet函数的逻辑如下:

  • 在local中查询,如果已经存在值,则先调用free函数来释放空间。
  • 最后把协程本地变量最新的值,保存在local中。

LocalVariableGet函数的逻辑如下:

  • 在local中查询,如果查询到了,则直接返回值。
  • 查询不到,则判断当前协程是否为批量并发执行的子从协程。
  • 如果是子从协程,再在父从协程中查询,查询到了,则直接返回值。
  • 父从协程中查询不到,则返回不存在。

注意:「想了解批量并发执行,可以移步阅读本系列的第三篇文章」

从易用性的角度出发,协程本地变量做了易用性封装,相关的代码如下所示。

// 协程本地变量模版类封装
template <typename Type> 
class CoroutineLocal {
public:
  CoroutineLocal(Schedule &schedule) : schedule_(schedule) {}
  static void free(void *data) {
    if (data)
      delete (Type *)data;
  }

  Type &Get() {
    MyCoroutine::LocalVariable local_variable;
    bool result = schedule_.LocalVariableGet(this, local_variable);
    assert(result == true);
    return *(Type *)local_variable.data;
  }
  // 重载类型转换操作符,实现协程本地变量直接给Type类型的变量赋值的功能
  operator Type() {
    return Get();
  }
  // 重载赋值操作符,实现Type类型的变量直接给协程本地变量赋值的功能
  CoroutineLocal &operator=(const Type &value) {
    Set(value);
    return *this;
  }

private:
  void Set(Type value) {
    Type *data = new Type(value);
    MyCoroutine::LocalVariable local_variable;
    local_variable.data = data;
    local_variable.free = free;
    schedule_.LocalVariableSet(this, local_variable);
  }

private:
  Schedule &schedule_;
};

CoroutineLocal是一个模版类,只有一个schedule_成员变量,是指向Schedule类对象的引用。

CoroutineLocal类中的Get和Set函数,是对LocalVariableGet和LocalVariableSet函数调用的简单封装。

CoroutineLocal类还重载了类型转换操作符和赋值操作符,「从而实现CoroutineLocal类对象和Type类型变量的相互赋值」。

代码示例

最后我们来看一下协程本地变量使用的一个示例。

#include "mycoroutine.h"
#include "localvariable.h"
#include <iostream>

using namespace std;
using namespace MyCoroutine;

void LocalVar1(Schedule &schedule, CoroutineLocal<int32_t> &local_var,
               int &sum) {
  local_var = 100;
  schedule.CoroutineYield();
  assert(100 == local_var);
  sum += local_var;
}
void LocalVar2(Schedule &schedule, CoroutineLocal<int32_t> &local_var,
               int &sum) {
  local_var = 200;
  schedule.CoroutineYield();
  assert(200 == local_var);
  sum += local_var;
}

int main() {
  // 创建一个协程调度对象,并自动生成大小为1024的协程池
  Schedule schedule(1024);
  int sum = 0;
  CoroutineLocal<int32_t> local_var(schedule);
  schedule.CoroutineCreate(LocalVar1, ref(schedule), ref(local_var), ref(sum));
  schedule.CoroutineCreate(LocalVar2, ref(schedule), ref(local_var), ref(sum));
  schedule.Run();  // Run函数完成从协程的自行调度,直到所有的从协程都执行完
  cout << "sum = " << sum << endl;
  return 0;
}

上面的代码中,在main函数中创建了一个协程本地变量local_var,然后分别在两个不同的协程中对local_var进行读写操作,在两个不同的协程中分别读写到了不同的值。

思考题

如果实现的是协程互斥锁,该如何实现呢?在评论区给出你的想法。

本文是大厂后端技术专家万木春原创。作者更多技术干货,见下方的书籍。

在这里插入图片描述

<think>我们正在讨论如何调试Python中的异步协程代码。用户之前的问题是关于协程间双向通信,现在转向调试技巧。根据引用,我们已知异步编程通过任务切换减少等待时间[^3],但调试异步代码比同步代码更复杂,因为执行流程非顺序性。 调试异步协程的挑战: 1. 非确定性:由于事件循环调度顺序不确定,问题可能难以复现 2. 挂起点追踪:协程在await处挂起,传统断点可能破坏并发时序 3. 并发错误:如竞争条件、死锁在异步环境中同样存在[^1] 解决方案: 一、使用日志记录(最可靠方法) 在关键位置添加详细日志,特别是await前后: ```python import logging logging.basicConfig(level=logging.INFO) async def request(url): logging.info(f"开始下载 {url}") await asyncio.sleep(2) # 模拟IO logging.info(f"完成下载 {url}") ``` 二、专用调试工具 1. `asyncio`内置调试模式: ```python # 启动事件循环前设置 asyncio.run(main(), debug=True) ``` 开启后: - 未await的协程会触发警告 - 慢回调检测(默认>100ms会输出警告) - 异常时显示创建协程的堆栈 2. 使用`aioconsole`进行交互式调试: ```bash pip install aioconsole ``` 在代码中插入: ```python from aioconsole import aconsole await aconsole.terminal() # 启动交互终端 ``` 此时可执行: - 查看当前运行任务:`tasks` - 检查协程状态:`where <task_id>` 三、IDE集成调试 PyCharm专业版/VSCode支持异步断点: 1. 在await语句前设置断点 2. 使用"Python Async Debug"配置运行 3. 支持协程堆栈查看(与传统线程堆栈分离) 四、结构化异常处理 使用`try/except`捕获协程内异常: ```python async def safe_task(): try: await risky_operation() except Exception as e: logging.error(f"任务失败: {e}", exc_info=True) ``` 五、死锁检测模式 在事件循环策略中启用: ```python from asyncio import set_event_loop_policy, WindowsSelectorEventLoopPolicy import sys if sys.platform == "win32": set_event_loop_policy(WindowsSelectorEventLoopPolicy()) ``` 六、可视化工具 使用`trio`的`trio-viz`或`asyncio`的`taskgraph`生成任务依赖图: ![异步任务依赖图](https://trio.readthedocs.io/en/stable/_images/trio-viz-screenshot.png)[^2] 调试步骤示例: 假设一个多协程下载程序卡死: 1. 开启调试模式运行:`asyncio.run(main(), debug=True)` 2. 发现日志停在"开始下载www.baidu.com"后无输出 3. 检查是否忘记`await`: ```python # 错误写法:缺少await asyncio.sleep(2) # 应改为 await asyncio.sleep(2) ``` 4. 使用`aioconsole`连接: ```python from aioconsole import aconsole await aconsole.terminal() # 在卡死前插入 ``` 5. 在终端输入`tasks`查看所有任务状态,发现某任务状态为`PENDING` 6. 检查该任务的`exception()`方法获取异常信息 常见问题解决: | 问题现象 | 可能原因 | 解决方案 | |------------------------|--------------------------|----------------------------| | 程序卡死无响应 | 协程未正确await | 检查所有阻塞操作前加await | | 部分任务未执行 | 未将任务加入事件循环 | 用asyncio.create_task()创建 | | 数据竞争 | 共享变量未加锁 | 使用asyncio.Lock() | | 队列阻塞 | 生产者/消费者数量不匹配 | 检查queue.join()调用 | 高级技巧:使用`asyncio`的`call_soon`注入调试代码: ```python loop = asyncio.get_running_loop() loop.call_soon(lambda: logging.info(f"当前任务数: {len(asyncio.all_tasks())}")) ``` > 注意:避免在协程中使用`time.sleep()`,这会阻塞整个事件循环[^2] 通过以上方法,可系统化诊断异步程序问题。对于复杂系统,建议采用日志+结构化异常处理为主,交互式工具为辅的策略。 §§相关问题§§ 1. 如何检测异步程序中的内存泄漏? 2. 异步协程中的异常应该如何处理? 3. 如何对异步代码进行单元测试?</think>### 如何在Python中调试异步协程代码 调试异步协程代码比调试同步代码更具挑战性,因为需要处理并发执行、任务切换和非阻塞操作。以下是实用的调试方法: #### 一、启用asyncio调试模式(基础必备) 在运行事件循环时开启调试模式,会暴露常见错误: ```python import asyncio async def main(): # 你的协程代码 # 关键:添加debug=True参数 asyncio.run(main(), debug=True) ``` 开启后你将获得: 1. **未等待协程警告**:检测忘记加`await`的情况 2. **慢回调警告**:默认超过100ms的任务会输出警告 3. **详细异常追踪**:显示协程创建位置而非仅挂起点[^3] #### 二、结构化日志记录(生产环境推荐) 使用`logging`模块添加协程感知的日志: ```python import logging import asyncio logging.basicConfig( format='%(asctime)s | %(name)s | %(levelname)s | %(message)s', level=logging.DEBUG ) async def request(url): logging.info(f"开始下载 {url}") try: await asyncio.sleep(2) # 模拟IO操作 # 故意制造错误 if "error" in url: raise ValueError("测试异常") except Exception as e: logging.error(f"下载失败: {url} | 错误: {str(e)}") raise logging.info(f"完成下载 {url}") return url ``` #### 三、交互式调试技巧 ##### 1. 在协程内设置断点 ```python async def critical_task(): import pdb; pdb.set_trace() # 传统pdb # 或使用更强大的ipdb # import ipdb; ipdb.set_trace() await asyncio.sleep(1) ``` 运行后会在断点处暂停,可使用命令: - `n`(ext):执行下一行 - `c`(ontinue):继续执行 - `w`(here):显示调用栈 ##### 2. 实时任务监控 ```python async def debug_monitor(): while True: tasks = [t for t in asyncio.all_tasks() if t is not asyncio.current_task()] print(f"\n活动任务: {len(tasks)}") for i, t in enumerate(tasks[:3]): # 显示前3个 print(f" 任务{i}: {t.get_name()}[{t.get_coro()}]") await asyncio.sleep(2) # 每2秒更新 # 在main中启动 async def main(): asyncio.create_task(debug_monitor()) # ...其他协程 ``` #### 四、可视化工具链 | 工具 | 用途 | 安装命令 | |------|------|----------| | **trio-viz** | 交互式任务可视化 | `pip install trio-viz` | | **aioconsole** | 交互式异步REPL | `pip install aioconsole` | | **PyCharm Pro** | 异步调试器(图形界面) | - | 使用aioconsole示例: ```python from aioconsole import aconsole async def debug_task(): await aconsole.terminal() # 启动交互式控制台 # 此时可输入命令如:tasks, await expr, etc. ``` #### 五、错误注入测试 模拟异常场景验证健壮性: ```python async def test_failure(): queue = asyncio.Queue() # 注入错误 async def poisoned_worker(): await queue.put("正常数据") await queue.put(ValueError("注入错误")) # 故意发送错误对象 async def safe_worker(): while True: try: item = await queue.get() if isinstance(item, Exception): raise item print(f"处理: {item}") finally: queue.task_done() # 运行测试 await asyncio.gather( poisoned_worker(), safe_worker() ) ``` #### 六、异步感知的单元测试 使用`pytest-asyncio`插件: ```python import pytest @pytest.mark.asyncio async def test_request(): result = await request("https://example.com") assert "example" in result # 测试超时场景 @pytest.mark.asyncio async def test_timeout(): with pytest.raises(asyncio.TimeoutError): await asyncio.wait_for(request("slow-site"), timeout=0.1) ``` #### 调试流程图 ```mermaid graph TD A[发现异常行为] --> B{是否可重现?} B -->|是| C[启用debug=True模式运行] B -->|否| D[添加详细日志] C --> E[分析控制台警告] D --> F[重现后转至C] E --> G{定位问题类型?} G --> H[未等待协程] --> I[检查所有async调用前是否有await] G --> J[任务阻塞] --> K[用asyncio.sleep(0)主动释放控制权] G --> L[竞态条件] --> M[使用asyncio.Lock()同步] G --> N[死锁] --> O[检查queue.join/Event.wait是否匹配] ``` > 经验提示:遇到复杂并发问题时,**逐步减少并发度**(如将100个协程减至2个)能快速定位问题[^1]。生产环境建议使用Sentry等工具捕获异步异常。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值