Вопрос или проблема
Я разрабатываю C-приложение в Visual Studio 2022, используя WinAPI. У меня есть некоторые проблемы с логированием (для отладки) из обратных вызовов при использовании пула потоков WinAPI. Ниже приведен минимальный пример (без обработки ошибок, для краткости кода):
VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE instance, PVOID parameter, PTP_WORK work)
{
UNREFERENCED_PARAMETER(instance);
UNREFERENCED_PARAMETER(parameter);
UNREFERENCED_PARAMETER(work);
printf("Привет из WorkCallback\n\r");
}
VOID Test()
{
printf("Привет из Test\n\r");
TP_CALLBACK_ENVIRON callbackEnviron;
InitializeThreadpoolEnvironment(&callbackEnviron);
PTP_POOL pool = CreateThreadpool(NULL);
SetThreadpoolThreadMinimum(pool, 1);
SetThreadpoolThreadMaximum(pool, 1);
SetThreadpoolCallbackPool(&callbackEnviron, pool);
PTP_CLEANUP_GROUP cleanupGroup = CreateThreadpoolCleanupGroup();
SetThreadpoolCallbackCleanupGroup(&callbackEnviron, cleanupGroup, NULL);
PTP_WORK work = CreateThreadpoolWork(WorkCallback, NULL, &callbackEnviron);
SubmitThreadpoolWork(work);
CloseThreadpool(pool);
CloseThreadpoolCleanupGroupMembers(cleanupGroup, FALSE, NULL);
CloseThreadpoolCleanupGroup(cleanupGroup);
}
int CDECL
main()
{
Test();
getchar();
return 0;
}
Когда я выполняю эту команду без отладки или с отладкой, но без точек останова, единственный вывод, который я получаю, это Привет из Test. Единственный способ, который я нашел, чтобы распечатать Привет из WorkCallback, это поставить точку останова перед SubmitThreadpoolWork
, еще одну в WorkCallback
, чтобы шагнуть в SubmitThreadpoolWork
при отладке и использовать окно Параллельные стекки (Отладка->Окна->Параллельные стекки).
Что странно для меня, так это то, что если я просто поставлю точку останова в WorkCallback
, этого недостаточно. И это поведение в определенных условиях отладки отличается от обычного выполнения. В некоторых других вопросах на SO я видел, как люди указывали, что разные потоки могут не иметь возможности выводить данные в консоль, поэтому я пробовал писать в файлы, но наблюдал то же самое поведение, о котором я описал ранее.
Неужели я что-то упустил, связанное с пулами потоков? Почему это происходит?
Ответ или решение
Логирование из колбеков WinAPI
Ваша проблема с отсутствием вывода сообщений из колбеков при использовании пула потоков WinAPI может быть связана с особенностями работы с потоками и системными буферами при выводе в консоль или файлы. Давайте разберем ваш код и предложим, как можно улучшить процесс логирования в вашем приложении.
Основные проблемы
-
Асинхронная природа работы с потоками: Вы вызываете
SubmitThreadpoolWork
, что помещает вашу задачу в пул потоков. Однако основной поток программы может завершиться до того, как колбек будет выполнен. Это и объясняет, почему вы видите только "Hello from Test". -
Завершение работы программы: После вызова
CloseThreadpool
, ваш основной поток может завершить выполнение, что приводит к отсутствию вызовов колбеков, так как программа уже закрылась. -
Работа с консолью из разных потоков: В Windows выполнение консольного ввода/output может работать не так, как ожидается, когда речь идет о нескольких потоках. Когда основной поток (или поток, который создал консоль) завершает выполнение, другие потоки теряют доступ к консоли.
Решение
Для решения этих проблем, можно использовать несколько подходов:
-
Ожидание завершения работы: Убедитесь, что основной поток ожидает завершения всех задач перед тем, как завершить программу. Для этого можно использовать
WaitForThreadpoolWorkCallbacks
. -
Использование файлов для логирования: Как вы уже заметили, вывод в файл может оказаться более надежным для многопоточного контекста. Однако вам нужно убедиться, что операции записи выполнены корректно и контекст записи потокобезопасен.
-
Использование синхронизации: В случае записи в общий ресурс (в данном случае файл), используйте механизмы синхронизации, такие как мьютексы, для предотвращения проблем с конкурентным доступом.
Переработанный код
Вот пример того, как можно изменить ваш оригинальный код:
#include <windows.h>
#include <stdio.h>
VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE instance, PVOID parameter, PTP_WORK work)
{
UNREFERENCED_PARAMETER(instance);
UNREFERENCED_PARAMETER(parameter);
UNREFERENCED_PARAMETER(work);
// Открытие файла для логирования
FILE *file;
fopen_s(&file, "log.txt", "a");
if (file)
{
fprintf(file, "Hello from WorkCallback\n");
fclose(file);
}
}
VOID Test()
{
printf("Hello from Test\n");
TP_CALLBACK_ENVIRON callbackEnviron;
InitializeThreadpoolEnvironment(&callbackEnviron);
PTP_POOL pool = CreateThreadpool(NULL);
SetThreadpoolThreadMinimum(pool, 1);
SetThreadpoolThreadMaximum(pool, 1);
SetThreadpoolCallbackPool(&callbackEnviron, pool);
PTP_CLEANUP_GROUP cleanupGroup = CreateThreadpoolCleanupGroup();
SetThreadpoolCallbackCleanupGroup(&callbackEnviron, cleanupGroup, NULL);
PTP_WORK work = CreateThreadpoolWork(WorkCallback, NULL, &callbackEnviron);
SubmitThreadpoolWork(work);
// Ожидание завершения работы
WaitForThreadpoolWorkCallbacks(work, FALSE);
CloseThreadpool(pool);
CloseThreadpoolCleanupGroupMembers(cleanupGroup, FALSE, NULL);
CloseThreadpoolCleanupGroup(cleanupGroup);
}
int CDECL main()
{
Test();
getchar();
return 0;
}
Заключение
Использование пула потоков может усложнять процесс логирования, поскольку управление жизненным циклом потоков отличается от традиционного подхода. Обязательно учитывайте синхронизацию и завершение потоков, чтобы избежать потери данных.
В этом контексте также стоит рассмотреть использование более продвинутых систем логирования или библиотек, таких как spdlog
или log4c
, для обеспечения удобного и надежного механизма логирования.