Bug: There is a data race in the `test_old` test
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
- 难度
- 4/5
- 预计耗时
- 3-5 天
- 新手友好度
- 45/100
- Issue 类型
- 缺陷
- 描述清晰度
- 基本清楚
- 活跃度
- 停滞
- 技术栈
- cpp
- 领域
- testing-qa
调研方向
从 test/old_tests/UnitTests/async.cpp 中失败的测试开始,检查 strings/base_coroutine_foundation.h 和 strings/base_coroutine_threadpool.h 中的协程行为。重现间歇性失败,并跟踪取消和 final_suspend 周围的完成回调顺序。完成的标准是测试不再存在无序访问,并且在不破坏现有行为的情况下验证所选择的回调语义。
由索引模型根据 Issue 内容生成。
描述
While running the CI for my own fork of C++/WinRT, I observed that test_old fails randomly (typically requiring 100-500 runs to reproduce). The failure line is https://github.com/microsoft/cppwinrt/blob/129c9258fedbae96a0155f634bf56b68f6f75053/test/old_tests/UnitTests/async.cpp#L1411, and I have identified the root cause.
The following is a simplified version of the code, stripped of all unrelated distractions. It is equivalent to the content in async.cpp, with some comments added to aid understanding.
struct signal_done
{
HANDLE signal;
~signal_done()
{
SetEvent(signal);
}
};
IAsyncOperationWithProgress<std::uint64_t, std::uint64_t> AutoCancel_IAsyncOperationWithProgress(HANDLE go)
{
signal_done d{ go };
co_await resume_on_signal(go); // switches to the thread pool due to suspension.
co_await std::suspend_never{}; // at this point, an exception is thrown due to cancellation being detected
REQUIRE(false);
co_return 0;
}
TEST_CASE("async, AutoCancel_IAsyncOperationWithProgress, 2")
{
handle event { CreateEvent(nullptr, false, false, nullptr)}; // # 1 auto-reset event and initialized as unset
IAsyncOperationWithProgress<std::uint64_t, std::uint64_t> async = AutoCancel_IAsyncOperationWithProgress(event.get());
REQUIRE(async.Status() == AsyncStatus::Started);
bool completed = false; // # 2 not atomic and not protected by a mutex
bool objectMatches = false;
bool statusMatches = false;
async.Completed([&](const IAsyncOperationWithProgress<std::uint64_t, std::uint64_t> & sender, AsyncStatus status)
{
completed = true; # 3
objectMatches = (async == sender);
statusMatches = (status == AsyncStatus::Canceled);
});
async.Cancel();
SetEvent(event.get()); // #4 signal async to run
REQUIRE(WaitForSingleObject(event.get(), INFINITE) == WAIT_OBJECT_0); // # 5 wait for async to be canceled
REQUIRE(async.Status() == AsyncStatus::Canceled);
REQUIRE_THROWS_AS(async.GetResults(), hresult_canceled); # 6
REQUIRE(completed); # 7
REQUIRE(objectMatches);
REQUIRE(statusMatches);
}
A simplified execution flow of this test is as follows:
- An auto-reset event is create.
WaitForSingleObjectis responsible for resetting it. - Execute the coroutine and its body (C++/WinRT coroutine's
initial_awaiter::suspenddoes not suspend the coroutine). https://github.com/microsoft/cppwinrt/blob/129c9258fedbae96a0155f634bf56b68f6f75053/strings/base_coroutine_foundation.h#L582 signal_doneis initialized; it will signal the event upon destruction.- Execute
co_await resume_on_signal(go);. This causes the coroutine to switch to the thread pool and suspend. https://github.com/microsoft/cppwinrt/blob/129c9258fedbae96a0155f634bf56b68f6f75053/strings/base_coroutine_threadpool.h#L486-L498 - The coroutine returns to the test function.
- Set the completion callback.
- Set the coroutine state to canceled.
- Signal the event.
- The coroutine resumes.
- A.
co_await std::suspend_never{};detects that the coroutine has been canceled and throws acanceledexception.
B. The test function is suspended at step # 5, waiting for the signal. - A.
signal_donedestructs and signals the event.
B. The test function resumes execution. - A. The coroutine executes
final_suspend, which invokes the completion callback. https://github.com/microsoft/cppwinrt/blob/129c9258fedbae96a0155f634bf56b68f6f75053/strings/base_coroutine_foundation.h#L585-L610
B. The test function checks thecompletedvariable at step # 7.
The problem lies in step 4. The coroutine is resumed on the thread pool. Consequently, the completed callback is set to true on a thread pool thread (# 3). Simultaneously, the test thread checks the variable at step # 7. The setting of the completed variable and the checking of it are unordered. This causes the test to fail randomly. The failure is not always observed because step # 7 throws and catches an exception, which often slows down the test function, creating a timing window that allows the test to pass more often than it fails.
I believe the key to this issue lies in two points:
- Is this test correct?
- Does the completion callback have to be executed in
final_suspend? Can it be executed by theCancelfunction? By modifyingbasic_coroutine_foundation.has follows, the test can also succeed.
void Cancel() noexcept
{
winrt::delegate<> cancel;
async_completed_handler_t<AsyncInterface> completed; // NB
{
slim_lock_guard const guard(m_lock);
if (m_status.load(std::memory_order_relaxed) == AsyncStatus::Started)
{
m_status.store(AsyncStatus::Canceled, std::memory_order_relaxed);
if (cancellable_promise::originate_on_cancel())
{
m_exception = std::make_exception_ptr(hresult_canceled());
}
else
{
m_exception = std::make_exception_ptr(hresult_canceled(hresult_error::no_originate));
}
cancel = std::move(m_cancel);
completed = std::move(this->m_completed); // NB
}
}
if (cancel)
{
cancel();
}
cancellable_promise::cancel();
if (completed) // NB
{
winrt::impl::invoke(completed, *this, AsyncStatus::Canceled); //NB
}
}
This change gives the completion callback a chance to execute on the thread that calls Cancel. I'm not sure if this is a good idea or if it will break existing code. The documentation currently doesn't specify this in detail.
- 主要语言
- C++
- 星标
- 1.9k
- 派生
- 281
- PR 合并指标
- 30 天内没有已合并 PR
环境准备
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
microsoft/cppwinrt 的其他 Issue
-
难度 2/5 1-3 小时 新手友好度 68/100
microsoft/cppwinrt#1554 · 2 条评论 ·
维护者通常 1 天内回复
-
难度 1/5 1 小时以内 新手友好度 20/100
维护者通常 1 天内回复
-
难度 5/5 一周以上 新手友好度 25/100
microsoft/cppwinrt#1620 · 1 条评论 ·
维护者通常 1 天内回复
-
难度 4/5 3-5 天 新手友好度 48/100
维护者通常 1 天内回复
-
难度 4/5 3-5 天 新手友好度 30/100
microsoft/cppwinrt#1610 · 3 条评论 · 2 个 reaction ·
维护者通常 1 天内回复
查看 microsoft/cppwinrt 的全部 Issue
相似的 Issue
-
bug chart-audit
难度 1/5 1 小时以内 新手友好度 92/100
维护者通常 1 天内回复
-
HasBacktrace Priority-Critical
难度 2/5 1-3 小时 新手友好度 78/100
azerothcore/azerothcore-wotlk#27921 ·
维护者通常 1 天内回复
-
难度 1/5 1 小时以内 新手友好度 88/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 68/100
维护者通常 1 天内回复
-
难度 1/5 1 小时以内 新手友好度 88/100
yhirose/cpp-peglib#344 ·