Асинхронная очередь отправки

Задний план:

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

Код:

#include <condition_variable>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
#include <utility>

class WorkerThread final {
 public:
  using Task = std::function<void(void)>;

 private:
  /* this mutex must be locked before
   * modifying state of this class */
  std::mutex _mutex;

  /* list of tasks to be executed */
  std::queue<Task> _toDo;

  /* The thread waits for this signal when
   * there are no tasks to be executed.
   * `notify_one` should be called to
   * wake up the thread and have it go
   * through the tasks. */
  std::condition_variable _signal;

  /* This flag is checked by the thread
   * before going to sleep. If it's set,
   * thread exits the event loop and terminates. */
  bool _stop = false;

  /* the thread is constructed at the
   * end so everything is ready by
   * the time it executes. */
  std::thread _thread;

 private:
  /* entry point for the thread */
  void ThreadMain() noexcept {
    /* Main event loop. */
    while (true) {
      /* not locked yet */
      std::unique_lock lock{_mutex, std::defer_lock_t{}};  // noexcept

      /* Consume all tasks */
      while (true) {
        /* locked while we see if
         * there are any tasks left */
        lock.lock();  // won't throw

        if (_toDo.empty()) {  // noexcept
          // Finished tasks. Mutex stays locked
          break;
        }

        // Pop the front task
        // move shouldn't throw
        auto const task = std::move(_toDo.front());
        _toDo.pop();

        // Allow other tasks to
        // be added while we're executing one
        lock.unlock();  // won't throw

        try {
          // execute task
          task();  // May throw. Will be caught.
        } catch (...) {
          // log if throws
        }
      }

      // queue is empty (and mutex is still locked)

      /* if `_stop` is set, unlock
       * mutex (in lock destructor)
       * and stop the thread */
      if (_stop) return;

      // wait for further notice (and unlock the mutex)
      _signal.wait(lock);  // won't throw
    }
  }

 public:
  template <class Func>
  void Schedule(Func&& func) {
    // lock the mutex so we can add a new task
    std::lock_guard<std::mutex> guard{_mutex};

    // push the task
    // May throw. RAII lock will be unlocked. State is valid
    _toDo.push(std::forward<Func>(func));

    // notify the worker thread in case it's sleeping
    _signal.notify_one();
  }

  WorkerThread() : _thread(&WorkerThread::ThreadMain, this) {}

  ~WorkerThread() {
    std::unique_lock lock{_mutex};  // won't throw
    // tell the thread to finish up
    _stop = true;

    // wake up the thread in case it's sleeping
    _signal.notify_one();  // noexcept

    lock.unlock();  // won't throw

    // wait for the thread to finish up
    _thread.join();  // won't throw since ThreadMain is noexcept
  }

  WorkerThread(WorkerThread const&) = delete;
  WorkerThread& operator=(WorkerThread const&) = delete;
  WorkerThread(WorkerThread&&) = delete;
  WorkerThread& operator=(WorkerThread&&) = delete;
};

// Example driver code

#include <chrono>
#include <iostream>

int main() {
  using namespace std::chrono_literals;

  int constexpr sz = 100;

  int vars[sz];

  {
    WorkerThread thread;
    for (int i = 0; i < sz; ++i) {
      thread.Schedule([&vars, i] {
        std::this_thread::sleep_for(1ms);
        vars[i] = i;
      });
    }
  }

  for (auto const var : vars) std::cout << var << 'n';
}

Детали, отмеченные // won't throw Я считаю, что не могу бросить, даже если они не отмечены noexcept.

1 ответ
1

Удалите лишние комментарии

Вы добавили в код много комментариев, но многие из них не очень полезны. Комментарии должны использоваться для объяснения того, что делает код, если это не ясно из чтения самого кода. Но например:

std::queue<Task> _toDo;

Я вижу только из этой строки кода, что это очередь задач, которые нужно выполнить, поэтому написанный вами комментарий не добавляет к этому никакой новой информации. Добавление ненужных комментариев просто увеличивает отношение шума к сигналу и фактически делает код меньше удобочитаемый.

Избегайте начинать имена с подчеркивания

Стандарт C ++ резервирует некоторые имена, начинающиеся с подчеркивания. Если вы не хотите изучать точные правила, я рекомендую вам не начинать имена с подчеркивания, а вместо этого использовать префикс m_ или одинарное подчеркивание в качестве суффикса.

Избегайте блокировки и разблокировки мьютексов вручную

Я рекомендую вам просто использовать защитный замок без std::defer_lock_t чтобы заблокировать те области кода, которым требуется монопольный доступ к структурам данных. Так что в MainThread(), Я бы написал:

while (true) {
    std::function<void(void)> task;

    {
        std::unique_lock lock{_mutex};
        _signal.wait_for(lock, []{ return _stop || !_toDo.empty(); });

        if (_stop && _toDo.empty())
            break;

        task = std::move(_toDo.front());
        _toDo.pop();        
    }

    task();
}

Уведомить без удержания блокировки

Хотя ваш код правильный, немного эффективнее вызывать notify_one() если ты не держишь замок. Так, например, в деструкторе вы можете написать:

~WorkerThread() {
    {
        std::unique_lock lock{_mutex};
        _stop = true;
    }

    _signal.notify_one();
    _thread.join();
}

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

    Ваш адрес email не будет опубликован. Обязательные поля помечены *