@Andrey_Epifantsev

Как использовать потоки в современном C++ в приложении на основе цикла событий?

Я пишу интерактивное GUI приложение. Мне понадобилось выполнить длительную по времени операцию - сканирование диска. Я хочу запустить эту операцию в отдельном потоке. Часть кода для работы с диском я пишу на чистом C++, чтобы не зависеть от платформы. Я реализовал запуск сканирования в отдельном потоке с использованием std::thread и std::mutex, но очень многие, кого я просил посмотреть этот код отвечают, что использование голых потоков это не правильно и правильно мыслить категориями задач. И использовать std::future или std::async или другие подобные средства. Но я не смог найти никаких примеров использования std::future или std::async в приложениях на основе обработки событий. Во всех примерах после запуска длительного задания в главном потоке нужно вызывать wait. А я не могу останавливать главный поток, так как он должен обрабатывать события от пользователя, чтобы приложение не зависало. Мне нужно, чтобы по завершению сканирования генерировалось событие об окончании сканирования.

Можете подсказать: как использовать возможности работы с потоками в современном C++ в приложении на основе обработки событий, без вызова wait?

Текущую версию моей реализации и часть советов по её улучшению можно посмотреть здесь.
  • Вопрос задан
  • 279 просмотров
Пригласить эксперта
Ответы на вопрос 3
@Mercury13
Программист на «си с крестами» и не только
Если вы пишете работу на чистом Си++ — придумайте некий интерфейс (в объектном смысле) на чистом Си++ для связи с интерфейсом (пользовательским, написанным на каком-то Qt): передать состояние, выяснить, хочет ли юзверь чего-то и так далее.

Я считаю, что лучше использовать механизмы наподобие почтовых ящиков, при этом для передачи информации из потока в интерфейс использовать PostMessage и обёртки над ним вроде Qt QueuedConnection, возможно, с дополнительной структурой данных наподобие atomic<unique_ptr<SomeData>>).

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

А future нужно использовать только при взаимодействии потоков-работников, чтобы один подождал, если ему позарез нужен результат второго.
Ответ написан
Комментировать
@code_panik
В сущности, async - это thread + promise/future с удобствами: его не нужно join-нить руками, не нужно создавать промежуточный promise, а возвращенный функцией результат и исключения можно получить в вызывающем коде. При этом деструктор объекта future, полученного в результате вызова async, блокирует вызывающий поток до завершения нашей async-функции.
В нашем случае разницы с использованием thread в части отправки сигналов нет, ведь нам по-прежнему придется сигнализировать виджету о завершении сканирования. Это можно сделать в самой функции сканирования. Если я правильно понял, раньше мы это делали в функции scanFinished, которая вызывалась в конце функции сканирования.
Под задачами обычно понимают независимые потоки выполнения (необязательно параллельно), которые можно запустить и забыть, а в конце забрать результат. Потоки сканирования директорий, судя по коду из вопроса, не зависят друг от друга, и мы наш ThreadManager можем разбить на две переменные типа ScanTask

#include <iostream>
#include <future>
#include <atomic>
#include <memory>
#include <string>

class ScanTask {
public:
	struct Result {
		enum {
			Success, Failure, Canceled
		} status;
		std::string message;
	};

	ScanTask(const std::string& dir)
		: canceled_{ false },
		result_{std::async(&ScanTask::scan, this, dir)}
	{ }

	void cancel() {
		canceled_ = true;
	}

	Result result() { 
		return result_.get(); // result_.valid() == false
	}

	Result scan(const std::string& dir) {
		struct Finalize {
			~Finalize() {
				// emit signal scan completed
			}
		} finalizeSignal;

		while (!canceled_ /* && not done */) {
			// .. scan subdirectories
			/*
			if (something failed)
				return {Result::Failure, "Failure"};
			*/
		}
		if (canceled_)
			return { Result::Canceled, std::string("[Canceled] ") + dir };
		return { Result::Success, std::string("[Success] ") + dir };
	}

private:
	std::atomic<bool> canceled_;
	std::future<Result> result_;
};

int main() {
	auto localScanTask = std::make_unique<ScanTask>("a");
	localScanTask->cancel();
	std::cout << localScanTask->result().message << std::endl;

	auto sharedScanTask = std::make_unique<ScanTask>("b");
	sharedScanTask->cancel();
	std::cout << sharedScanTask->result().message << std::endl;
	return 0;
}

Можно было даже не создавать отдельный класс, а в функции-обработчике нажатия кнопки запуска присваивать future члену виджета результат вызова async от анонимной лямбды, как предлагали с thread в ревью. При этом atomic canceled_ можно захватить по ссылке, она была бы общей для всех.
Ответ написан
Комментировать
@OnePride
Делаю красиво
Чтобы не плодить велосипедов с std::async, не наводить хаос с количеством одновременно живущих потоков, и в будущем сразу иметь возможность строить граф задача - присмотритесь к taskflow
Ответ написан
Комментировать
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы