Как можно исправить ошибки, возникающие при вызове обработчика событий в другом потоке?

У меня есть таймер, который выполняется в отдельном потоке и имеет событие Elapsed (вы можете рассматривать это как <code class="cpp">std::list&lt;std::function&lt;T&gt;&gt;</code>), которое вызывается в том же потоке, где таймер работает. <br/> <pre><code class="cpp">int main()
{
	std::chrono::milliseconds duration(150);
	Timer t = duration;
	t.Elapsed += &amp;OnElapsed;
	t.Start();
	
	std::this_thread::sleep_for(duration);
}</code></pre> <br/> Существует вероятность возникновения ошибок, если установить одинаковое значение duration для таймера и для sleep_for. В других случаях я не обнаружил никаких ошибок. Не совсем ясно, почему возникает первая ошибка. Что касается вторых ошибок, полагаю, они связаны с тем, что функция в std::function могла быть удалена? <br/> Возможно ли вызвать обработчик, связанный с истечением времени таймера, из основного потока по завершении таймера, не выполняя специальную проверку на истечение времени?
  • 19 ноября 2024 г. 10:53
Ответы на вопрос 1
Ошибки, связанные с вызовом обработчиков событий в других потоках, возникают из-за того, что обработка состояния может изменяться между моментом, когда обработчик вызывается, и моментом, когда другой поток завершает работу. В вашем случае, если таймер и основной поток (где вы вызываете `sleep_for`) работают параллельно и используют одинаковые временные интервалы, существует вероятность того, что основной поток может завершиться раньше или в тот же момент, когда таймер вызывает обработчик события.

### Способы избежать ошибок:

1. **Используйте синхронизацию**: Вы можете использовать механизмы синхронизации, такие как мьютексы или семафоры, для обеспечения безопасного доступа к данным между потоками. Когда таймер вызывает свой обработчик, он может блокировать другой поток, пока обработчик завершает выполнение.

2. **Проверка состояния**: Как вы уже упомянули, возможно, стоит ввести проверку состояния, чтобы убедиться, что обработчики все еще существуют и корректны для использования. Это может быть реализовано через использование `std::shared_ptr` или `std::weak_ptr`. 

3. **Вызов обработчика в основном потоке**: Вы можете создать очередь для событий и затем обрабатывать события в основном потоке.

Пример, показывающий использование очереди событий:

```cpp
#include <iostream>
#include <chrono>
#include <thread>
#include <functional>
#include <queue>
#include <mutex>
#include <condition_variable>

class Timer {
public:
    using Callback = std::function<void()>;

    Timer(std::chrono::milliseconds duration) : duration_(duration), running_(false) {}

    void Start() {
        running_ = true;
        thread_ = std::thread([this]() {
            std::this_thread::sleep_for(duration_);
            if (running_) {
                std::unique_lock<std::mutex> lock(mutex_);
                callbacks_.push_back(OnElapsed); // Добавим в очередь
                cv_.notify_one(); // Оповестим основной поток
            }
        });
    }

    void Stop() {
        running_ = false;
        if (thread_.joinable()) {
            thread_.join();
        }
    }

    void Subscribe(const Callback& callback) {
        std::lock_guard<std::mutex> lock(mutex_);
        callbacks_.push_back(callback);
    }

    void ProcessEvents() {
        std::unique_lock<std::mutex> lock(mutex_);
        cv_.wait(lock, [this] { return !callbacks_.empty(); });
        // Вызов всех обработчиков
        for (const auto& callback : callbacks_) {
            callback();
        }
        callbacks_.clear();
    }

private:
    void OnElapsed() {
        std::cout << "Timer elapsed!" << std::endl;
    }

    std::chrono::milliseconds duration_;
    std::thread thread_;
    std::vector<Callback> callbacks_;
    bool running_;
    std::mutex mutex_;
    std::condition_variable cv_;
};

int main() {
    Timer t(std::chrono::milliseconds(150));
    t.Subscribe([]() { std::cout << "Event processed in main thread!" << std::endl; });
    t.Start();

    while (true) {
        t.ProcessEvents(); // Обработка событий в основном потоке
    }

    return 0;
}
```

В этом примере обработчик события `OnElapsed` добавляется в очередь, и события обрабатываются в основном потоке. Это гарантирует, что в случае истечения времени не будет проблем с доступом к вызовам обработчиков, которые могут быть удалены или недоступны.

Таким образом, мы можем избежать ошибок, связанных с многопоточностью, обеспечив возможность безопасного вызова обработчиков.
Похожие вопросы