Lab4d 构建基础协程同步组件mutex

tinyCoroLab4d 实验简介

本节我们将正式开始 tinyCoroLab4d,即构建最为重要的基础协程同步组件 mutex,相较于 lab4 前三个实验,本节实验难度略微提高。

预备知识

⚠️预备知识即在实验开始前你应该已经掌握的知识,且在知识铺垫章节中均有涉及

  • C++ 协程 awaiter 的概念

  • C++ 模板编程

📖lab4d 任务书

实验前置讲解

mutex 作为概念上最为人熟知、使用上最为广泛也最为重要的线程同步手段,我想实验者一定不会陌生,而 tinyCoro 的协程同步组件大家庭也一定不会缺席这位最重要的成员。

本节实验涉及到的核心文件为include/coro/comp/mutex.hppsrc/comp/mutex.cpp,实验者需要预先打开文件浏览大致代码结构,下面针对该文件内容进行讲解。

include/coro/comp/event.hpp中给出了一个非常简单的 mutex 的定义,注意该定义仅仅是一个形式,不具备 mutex 的正确功能,但是其类以及函数声明形式是正确的

另外在include/coro/comp/mutex_guard.hpp中定义了lock_guard用来自动加锁和释放锁,这部分实验者不需要修改。

⚠️注意事项

  • 请确保已阅读过tinyCoroLab Introduce章节。

  • 为了确保正确实现目标函数,实验者可能需要做一些额外操作:新增类、修改现有类的实现、补充现有类的方法和成员变量等操作,请遵循free-design 实验原则

  • 你需要仔细评估待实现的接口是否需要是线程安全的。

  • 任何导致测试卡住、崩溃等无法使测试顺利通过的情况都表明你的代码存在问题。

实验任务书

🧑‍💻Task #1 - 实现 mutex

任务目标

tinyCoro 的 mutex 功能与 C++ 的 std::mutex 功能类似,其核心功能是确保多个协程对共享变量操作的安全性,lab4d 要求实现者为 mutex 添加下列功能:

auto try_lock() noexcept -> bool; // 普通调用
auto lock() noexcept ->awaiter; // 协程调用,awaiter 的 await_resume 返回 void
auto unlock() noexcept -> void; // 普通调用
auto lock_guard() noexcept -> awaiter; // 协程调用,awaiter 的 await_resume 返回 lock_guard

try_lock即尝试获取锁,返回值表示是否成功获取锁。lock作为协程调用需要通过co_await mutex.lock()的形式调用。unlock即释放锁,并唤醒一个 suspend awaiter(如果存在的话)。lock_guard封装了一系列复合操作,通过co_await mutex.lock_guard()的形式调用来获取锁并返回 lock_guard,而 lock_guard 在生命周期结束后会自动释放锁。lock_guard 已经实现,实验者需要做的是将其通过 awaiter 的 await_resume 返回给调用者。

对比 lab4 前三节实验构建 mutex 的难点在于调用unlock后如果不存在 suspend awaiter 那么应该将锁置于未加锁状态,如果存在那么只能唤醒一个 suspend awaiter,这个过程的状态转换是需要实验者认真思考的,而且实验者要确保 mutex 内部操作的线程安全性

这时候实验者肯定会产生一个问题:如果unlock只能唤醒一个 suspend awaiter,那么该唤醒哪一个呢?这个不做要求,不管策略是 FIFO 还是 LIFO,只需要保证所有协程可以正确的获取以及释放锁就行了。

下面给出 mutex 的使用场景便于实验者理解:

mutex mtx;
task<> func() { // 手动加锁和解锁
  co_await mtx.lock();
  // codes....
  mtx.unlock();
}
task<> func() { // 自动加锁和解锁
  auto guard = co_await mtx.lock_guard();
  // codes....
  // auto unlock mutex
}

涉及文件

待实现函数

  • coro::mutex::try_lock

  • coro::mutex::lock

  • coro::mutex::unlock

  • coro::mutex::lock_guard

补充说明

  • 你的实现必须包含任务目标中描述的函数且函数声明形式必须一致,不然无法正常编译

  • 协程函数的返回类型可以被修改,但必须是 awaiter 或者 awaitable 类型且 await_resume 返回类型与任务书规定一致

  • 请务必考虑多线程安全性问题

  • 因为 tinyCoro 设计问题,所以协程组件恢复 suspend awaiter 时最好将其派发到其原本运行的 context

🔖测试

功能测试

功能测试场景主要针对:

  • mutex 的 lock 与 unlock 常规测试

  • mutex 与 event 的混合测试

  • mutex 与 latch 的混合测试

  • mutex 与 wait_group 的混合测试

完成本节实验后,实验者请在构建目录下执行下列指令来构建以及运行测试程序:

make build-lab4d # 构建
make test-lab4d # 运行

内存安全测试

在构建目录下运行下列指令来执行内存安全测试:

make memtest-lab4d

测试通过会提示 pass,不通过会给出 valgrind 的输出文件位置,请实验者根据该文件排查内存故障。

性能测试

💡tinyCoroLab预置了用于性能调优的火焰图生成脚本哦!详情请查看scripts/README.MD

tinyCoroLab Introduce章节中提到性能测试的三种模型:

  • thread_pool_stl_XX: 使用简单的线程池和 stl 组件。

  • coro_stl_XX: 使用 coro 调度器和 stl 组件。

  • coro_XX: 使用 coro 调度器和 coro 组件。

对于 lab4d 的性能测试,coro 组件即实验者实现的 mutex,stl 组件即 C++ std::mutex,测试场景为多个函数对同一个资源lockunlock,并测量执行总耗时。

实验者只要重点关注coro_stl_XXcoro_XX模型输出的结果差异即可,该结果反映的实验者的实现与 stl 实现的性能差异。由于线程在受线程同步组件影响而陷入阻塞态时并不会选择执行其他任务,但 tinyCoro 执行引擎会,因此为了保证公平性,每个线程只会被派发一个任务,性能测试也仅仅是想重点关注实验者的实现与 stl 实现的性能差异。

在构建目录下运行下列指令来构建和运行性能测试:

make benchbuild-lab4d # 构建
make benchtest-lab4d # 运行

Last updated