Простое использование windows windows, но возникают необъяснимые ошибки

Я играл с Windows-волокнами, реализуя свой собственный планировщик задач, когда происходили странные сбои и неопределенное поведение. Ради простоты я начал новый проект и написал простую программу, которая выполняет следующие операции:

  1. Основной поток создает пучок волокон, затем запускает два потока
  2. Основной поток ждет, пока вы не убьете программу
  3. Каждый рабочий поток превращается в волокно
  4. Каждый рабочий поток пытается найти свободное волокно, а затем переключается на это новое свободное волокно.
  5. Как только нить переключилась на новое волокно, она выталкивает свое предыдущее волокно в контейнер свободных волокон
  6. Каждый рабочий поток переходит к шагу 4

Если вы не знакомы с концепцией оптоволокна, этот разговор - хорошее начало.

Данные

Каждый поток имеет свою собственную структуру данных ThreadData для хранения своих предыдущих, текущих экземпляров волокна и индекса своего потока. Я пробовал несколько способов получить структуру данных ThreadData во время выполнения:

  • Я использовал локальное хранилище потока для хранения указателя ThreadData
  • Я использовал контейнер, который связывает thread_id со структурой ThreadData

Эта проблема

Когда волокно вводится впервые (посмотрите на функцию FiberFunc), нить, использующая это волокно, должна протолкнуть свое предыдущее волокно в контейнер свободных волокон. Но бывает так, что иногда предыдущее волокно имеет нулевое значение, что невозможно. Это невозможно, потому что перед переключением на новое волокно нить устанавливает свое предыдущее значение волокна с его текущим значением волокна (и оно устанавливает свое текущее значение волокна с новым значением волокна).

Таким образом, если поток входит в совершенно новое волокно с его предыдущим волокном, установленным в ноль, это будет означать, что он возник из ниоткуда (что не имеет никакого смысла).

Единственная причина, по которой для ThreadData его предыдущее значение волокна устанавливается как нулевое, когда он входит в совершенно новое волокно, заключается в том, что другой поток устанавливает его в нулевое значение или что компилятор переупорядочил инструкции под капотом.

Я проверил сборку, и кажется, что компилятор не несет ответственности.

Есть несколько ошибок, которые я не могу объяснить:

  1. Если я использую первую функцию GetThreadData() для извлечения структуры ThreadData, я могу получить экземпляр, индекс которого отличается от локального индекса потока (эти индексы были установлены при запуске потоков). Это заставит программу утверждать ( assert(threadData->index == localThreadIndex)).

  2. Если я использую любую другую функцию для извлечения структуры ThreadData, я буду утверждать в функции FiberFunc, потому что предыдущее значение волокна равно нулю (assert(threadData->previousFiber)).

У вас есть идея, почему этот код не работает? Я провел бесчисленные часы, пытаясь выяснить, что не так, но я не вижу своих ошибок.

Спецификация

ОС: Windows 10

IDE: Visual Studio 2015 и Visual Studio 2017

Компилятор: VC++

Конфигурация: релиз

Обратите внимание, что в конфигурации отладки нет ошибок.

Код

Вы можете попытаться запустить его несколько раз, прежде чем активировать огонь.

#include "Windows.h"
#include <vector>
#include <thread>
#include <mutex>
#include <cassert>
#include <iostream>
#include <atomic>

struct Fiber
{
    void* handle;
};

struct ThreadData
{
    Fiber*  previousFiber{ nullptr };
    Fiber*  currentFiber{ nullptr };
    Fiber   fiber{ };
    unsigned int index{};
};

//Threads
std::vector<std::pair<std::thread::id, unsigned int>> threadsinfo{};

//threads data container
ThreadData  threadsData[8];

//Fibers
std::mutex  fibersLock{};
std::vector<Fiber> fibers{};
std::vector<Fiber*> freeFibers{};

thread_local unsigned int localThreadIndex{};
thread_local Fiber* debug_localTheadLastFiber{};
thread_local ThreadData* localThreadData{};

using WindowsThread = HANDLE;
std::vector<WindowsThread> threads{};

//This is the first way to retrieve the current thread's ThreadData structure using thread_id
//ThreadData* GetThreadData()
//{
//  std::thread::id threadId( std::this_thread::get_id());
//  for (auto const& pair : threadsinfo)
//  {
//      if (pair.first == threadId)
//      {
//          return &threadsData[pair.second];
//      }
//  }
//
//  //It is not possible to assert
//  assert(false);
//  return nullptr;
//}

//This is the second way to retrieve the current thread's ThreadData structure using thread local storage
//ThreadData* GetThreadData()
//{
//  return &threadsData[localThreadIndex];
//}


//This is the third way to retrieve the current thread's ThreadData structure using thread local storage
ThreadData* GetThreadData()
{
    return localThreadData;
}


//Try to pop a free fiber from the container, thread safe due to mutex usage
bool  TryPopFreeFiber(Fiber*& fiber)
{
    std::lock_guard<std::mutex> guard(fibersLock);
    if (freeFibers.empty()) { return false; }
    fiber = freeFibers.back();
    assert(fiber);
    assert(fiber->handle);
    freeFibers.pop_back();
    return true;
}


//Try to push a free fiber to the container, thread safe due to mutex usage
bool PushFreeFiber(Fiber* fiber)
{
    std::lock_guard<std::mutex> guard(fibersLock);
    freeFibers.push_back(fiber);
    return true;
}


//the __declspec(noinline) is used to inspect code in release mode, comment it if you want
__declspec(noinline) void  _SwitchToFiber(Fiber* newFiber)
{
    //You want to switch to another fiber
    //You first have to save your current fiber instance to release it once you will be in the new fiber
    {
        ThreadData* threadData{ GetThreadData() };
        assert(threadData->index == localThreadIndex);
        assert(threadData->currentFiber);
        threadData->previousFiber = threadData->currentFiber;
        threadData->currentFiber = newFiber;
        debug_localTheadLastFiber = threadData->previousFiber;
        assert(threadData->previousFiber);
        assert(newFiber);
        assert(newFiber->handle);
    }

    //You switch to the new fiber
    //this call will either make you enter in the FiberFunc function if the fiber has never been used
    //Or you will continue to execute this function if the new fiber has been already used (not that you will have a different stack so you can't use the old threadData value)
    ::SwitchToFiber(newFiber->handle);

    {
        //You must get the current ThreadData* again, because you come from another fiber (the previous statement is a switch), this fiber could have been used by any other thread
        ThreadData* threadData{ GetThreadData() };

        //THIS ASSERT WILL FIRES IF YOU USE THE FIRST GetThreadData METHOD, WHICH IS IMPOSSIBLE....
        assert(threadData->index == localThreadIndex);

        assert(threadData);
        assert(threadData->previousFiber);

        //We release the previous fiber
        PushFreeFiber(threadData->previousFiber);
        debug_localTheadLastFiber = nullptr;
        threadData->previousFiber = nullptr;
    }

}


void ExecuteThreadBody()
{
    Fiber*  newFiber{};

    if (TryPopFreeFiber(newFiber))
    {
        _SwitchToFiber(newFiber);
    }
}


DWORD __stdcall ThreadFunc(void* data)
{
    int const index{ *static_cast<int*>(data)};

    threadsinfo[index] = std::make_pair(std::this_thread::get_id(), index);

    //setting up the current thread data
    ThreadData* threadData{ &threadsData[index] };
    threadData->index = index;

    void*   threadAsFiber{ ConvertThreadToFiber(nullptr) };
    assert(threadAsFiber);

    threadData->fiber = Fiber{ threadAsFiber };
    threadData->currentFiber = &threadData->fiber;

    localThreadData = threadData;
    localThreadIndex = index;

    while (true)
    {
        ExecuteThreadBody();
    }

    return DWORD{};
}


//The entry point of all fibers
void __stdcall FiberFunc(void* data)
{
    //You enter to the fiber for the first time

    ThreadData* threadData{ GetThreadData() };

    //Making sure that the thread data structure is the good one
    assert(threadData->index == localThreadIndex);

    //Here you will assert
    assert(threadData->previousFiber);

    PushFreeFiber(threadData->previousFiber);
    threadData->previousFiber = nullptr;

    while (true)
    {
        ExecuteThreadBody();
    }
}


__declspec(noinline) void main()
{
    constexpr unsigned int threadCount{ 2 };
    constexpr unsigned int fiberCount{ 20 };

    threadsinfo.resize(threadCount);

    fibers.resize(fiberCount);
    for (auto index = 0; index < fiberCount; ++index)
    {
        fibers[index] = { CreateFiber(0, FiberFunc, nullptr) };
    }

    freeFibers.resize(fiberCount);
    for (auto index = 0; index < fiberCount; ++index)
    {
        freeFibers[index] = std::addressof(fibers[index]);
    }

    threads.resize(threadCount);

    std::vector<int>    threadParamss(threadCount);



    for (auto index = 0; index < threadCount; ++index)
    {
        //threads[index] = new std::thread{ ThreadFunc, index };
        threadParamss[index] = index;
        threads[index] = CreateThread(NULL, 0, &ThreadFunc, &threadParamss[index], 0, NULL);
        assert(threads[index]);
    }

    while (true);

    //I know, it is not clean, it will leak
}

2 ответа

Вам нужно использовать параметр /GT, если вы хотите локальное хранилище потока.

https://learn.microsoft.com/en-us/cpp/build/reference/gt-support-fiber-safe-thread-local-storage?view=msvc-170

Ну, через несколько месяцев. Я понял, что виновной была переменная, объявленная как thread_local. Если вы используете fiber, забудьте о переменных thread_local и используйте память для каждого волокна, выделенную при их создании. Теперь я храню свой текущий индекс потока в экземпляре структуры для каждого волокна.

Другие вопросы по тегам