Bug: There is a data race in the `test_old` test
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 45/100
- Tipo de issue
- Error
- Claridad
- Bastante claro
- Estado de actividad
- Estancado
- Stack tecnológico
- cpp
- Área
- testing-qa
Línea de trabajo
Comienza con test/old_tests/UnitTests/async.cpp en la prueba que falla y revisa el comportamiento de las corrutinas en strings/base_coroutine_foundation.h y strings/base_coroutine_threadpool.h. Reproduce el fallo intermitente y rastrea el orden de los callbacks de finalización en torno a la cancelación y final_suspend. La tarea estará terminada cuando la prueba ya no tenga un acceso desordenado y la semántica de callbacks elegida esté validada sin romper el comportamiento existente.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
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.
- Lenguaje dominante
- C++
- Estrellas
- 1.9k
- Forks
- 281
- Métricas de merge de PR
- Sin PR fusionados en 30 d
Guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de microsoft/cppwinrt
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
-
base_macros.h disables warnings without push/pop, leaking them into consumer translation units Abierto
Dificultad 1/5 Menos de una hora Aptitud para principiantes 20/100
-
Dificultad 5/5 Más de una semana Aptitud para principiantes 25/100
-
Dificultad 4/5 3-5 días Aptitud para principiantes 48/100
-
Dificultad 4/5 3-5 días Aptitud para principiantes 30/100
Todos los issues de microsoft/cppwinrt
Issues similares
-
[meshoptimizer] update to 1.3 Abiertocategory:port-update
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
-
MicroInterpreter fails to compile with -Werror=address due to &context_ in TF_LITE_ENSURE_OK Abierto
Dificultad 1/5 1-3 horas Aptitud para principiantes 88/100
tensorflow/tflite-micro#3784 ·
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
-
agentic-workflows automation
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
tenstorrent/tt-metal#57946 ·
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
aristocratos/btop#1857 ·