Логирование из колбеков winapi

Вопрос или проблема

Я разрабатываю 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 может быть связана с особенностями работы с потоками и системными буферами при выводе в консоль или файлы. Давайте разберем ваш код и предложим, как можно улучшить процесс логирования в вашем приложении.

Основные проблемы

  1. Асинхронная природа работы с потоками: Вы вызываете SubmitThreadpoolWork, что помещает вашу задачу в пул потоков. Однако основной поток программы может завершиться до того, как колбек будет выполнен. Это и объясняет, почему вы видите только "Hello from Test".

  2. Завершение работы программы: После вызова CloseThreadpool, ваш основной поток может завершить выполнение, что приводит к отсутствию вызовов колбеков, так как программа уже закрылась.

  3. Работа с консолью из разных потоков: В Windows выполнение консольного ввода/output может работать не так, как ожидается, когда речь идет о нескольких потоках. Когда основной поток (или поток, который создал консоль) завершает выполнение, другие потоки теряют доступ к консоли.

Решение

Для решения этих проблем, можно использовать несколько подходов:

  1. Ожидание завершения работы: Убедитесь, что основной поток ожидает завершения всех задач перед тем, как завершить программу. Для этого можно использовать WaitForThreadpoolWorkCallbacks.

  2. Использование файлов для логирования: Как вы уже заметили, вывод в файл может оказаться более надежным для многопоточного контекста. Однако вам нужно убедиться, что операции записи выполнены корректно и контекст записи потокобезопасен.

  3. Использование синхронизации: В случае записи в общий ресурс (в данном случае файл), используйте механизмы синхронизации, такие как мьютексы, для предотвращения проблем с конкурентным доступом.

Переработанный код

Вот пример того, как можно изменить ваш оригинальный код:

#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, для обеспечения удобного и надежного механизма логирования.

Оцените материал
Добавить комментарий

Капча загружается...