双线程交替(乒乓式)执行的简化实现方案问询
核心问题
能否在不使用两个原子量(或信号量等其他工具)的情况下,实现双线程的可靠交替执行?要求确保两个线程都能执行,若某一线程处于等待状态,另一线程完成当前迭代后应优先让等待线程执行。
简单的std::mutex无法满足需求,因为单个线程可能长时间占用锁,导致另一线程无法及时获取;即使使用std::atomic_flag::wait也存在同样问题。
目前已通过两个原子量实现该需求,示例代码及运行输出如下:
#include <thread> #include <iostream> #include <vector> std::atomic_flag ping_lock_flag = ATOMIC_FLAG_INIT; std::atomic_flag pong_lock_flag = ATOMIC_FLAG_INIT; std::atomic_int idx = 0; std::vector<int> v(10); void update() { ping_lock_flag.wait(false, std::memory_order_relaxed); ping_lock_flag.clear(std::memory_order_release); std::cout << "update start\n"; std::size_t index = idx.load(); if (index < v.size()) { v[index] = 1; idx.fetch_add(1); } std::cout << "update end\n"; pong_lock_flag.test_and_set(std::memory_order_acquire); pong_lock_flag.notify_one(); } void render() { // 加锁前的繁重操作(前置处理) // ... pong_lock_flag.wait(false, std::memory_order_relaxed); pong_lock_flag.clear(std::memory_order_release); std::cout << "render start\n"; std::size_t index = idx.load(); if (index < v.size()) { v[index] = 2; idx.fetch_add(1); } std::cout << "render end\n"; ping_lock_flag.test_and_set(std::memory_order_acquire); ping_lock_flag.notify_one(); // 加锁后的繁重操作(后置处理) // ... } int main() { std::jthread update_thread([&]() { while (idx.load() < v.size()) { update(); } }); std::jthread render_thread([&]() { while (idx.load() < v.size()) { render(); } }); ping_lock_flag.test_and_set(std::memory_order_acquire); update_thread.join(); render_thread.join(); for (auto& value : v) { std::cout << value << ", "; } std::cout << '\n'; }
运行输出
update start update end render start render end update start update end render start render end update start update end render start render end update start update end render start render end update start update end render start render end update start update end 1, 2, 1, 2, 1, 2, 1, 2, 1, 2,
现询问:是否可借助现代C++多线程工具实现更简洁的等效方案?若不能,原因是什么?
需求背景
将update和render操作放在独立线程中但保持同步,核心原因是同步仅覆盖内部数据操作,而框架渲染操作(如清空设备、将结果复制到显存等)可与update并行执行(对应render代码中的前置/后置处理部分)。
若不采用乒乓式同步,很可能出现仅执行update或仅执行render的情况。同时允许极少量虚假唤醒或唤醒丢失,只要不导致线程完全停滞即可。
更新1:简化实现的安全性疑问
我想到了以下简化实现,但不确定是否安全(需符合上述需求背景中的约束):
#include <thread> #include <iostream> #include <vector> std::atomic_flag ping_lock_flag = ATOMIC_FLAG_INIT; std::atomic_int idx = 0; std::vector<int> v(10); void update() { ping_lock_flag.wait(false, std::memory_order_relaxed); std::cout << "update start\n"; std::size_t index = idx.load(); if (index < v.size()) { v[index] = 1; idx.fetch_add(1); } std::cout << "update end\n"; ping_lock_flag.clear(std::memory_order_release); ping_lock_flag.notify_one(); } void render() { // 加锁前的繁重操作(前置处理) // ... ping_lock_flag.wait(true, std::memory_order_relaxed); std::cout << "render start\n"; std::size_t index = idx.load(); if (index < v.size()) { v[index] = 2; idx.fetch_add(1); } std::cout << "render end\n"; ping_lock_flag.test_and_set(std::memory_order_acquire); ping_lock_flag.notify_one(); // 加锁后的繁重操作(后置处理) // ... } int main() { std::jthread update_thread([&]() { while (idx.load() < v.size()) { update(); } }); std::jthread render_thread([&]() { while (idx.load() < v.size()) { render(); } }); ping_lock_flag.test_and_set(std::memory_order_acquire); update_thread.join(); render_thread.join(); for (auto& value : v) { std::cout << value << ", "; } std::cout << '\n'; }
该实现可运行,但是否存在我未发现的问题?
更新2:需求调整
我发现上述方案存在问题:它强制了“严格乒乓式”执行,即render必须在update之后执行,反之亦然。若update比render快,无法出现update-update-render的执行顺序。因此我需要调整需求:当update线程比render线程快N倍时,希望update的执行次数约为render的N倍,同时保证render就绪时能获得执行机会。
更新3:新方案的潜在问题
以下方案应该可行,但尚未充分验证,可能存在潜在问题:
#include <chrono> #include <thread> #include <iostream> #include <vector> std::atomic_flag render_update_lock = ATOMIC_FLAG_INIT; std::atomic_flag render_thread_ready = ATOMIC_FLAG_INIT; std::atomic_int idx = 0; std::vector<int> v(10); using namespace std::chrono_literals; void update() { if (!render_thread_ready.test()) { render_update_lock.wait(true, std::memory_order_relaxed); render_update_lock.test_and_set(std::memory_order_acquire); std::cout << "update start\n"; std::size_t index = idx.load(); if (index < v.size()) { v[index] = 1; idx.fetch_add(1); } std::cout << "update end\n"; render_update_lock.clear(std::memory_order_release); render_update_lock.notify_one(); } std::this_thread::sleep_for(1us); } void render() { render_thread_ready.test_and_set(std::memory_order_acquire); render_update_lock.wait(true, std::memory_order_relaxed); render_update_lock.test_and_set(std::memory_order_acquire); render_thread_ready.clear(std::memory_order_release); std::cout << "render start\n"; std::size_t index = idx.load(); if (index < v.size()) { v[index] = 2; idx.fetch_add(1); } render_update_lock.clear(std::memory_order_release); render_update_lock.notify_one(); std::cout << "render end\n"; std::this_thread::sleep_for(2us); } int main() { std::jthread update_thread([&]() { while (idx.load() < v.size()) { update(); } }); std::jthread render_thread([&]() { while (idx.load() < v.size()) { render(); } }); update_thread.join(); render_thread.join(); for (auto& value : v) { std::cout << value << ", "; } std::cout << '\n'; }
请问该方案是否存在潜在问题?
内容的提问来源于stack exchange,提问作者Damir Tenishev

