2015-03-21 2 views
3

Рассмотрим следующую реализацию тривиального пула потоков, написанного на C++ 14.Безопасное уничтожение пула потоков

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

std::unique_lock<mutex> lock(this->instance_mutex_); 

this->cond_handle_task_.wait(lock, [this] { 
    return (this->destroy_ || !this->tasks_.empty()); 
}); 

Кроме того, обратите внимание, что объект ThreadPool использует элемент данных destroy_, чтобы определить, был ли он уничтожен - вызван деструктор. Переключение этого элемента данных на true будет уведомлять каждый рабочий поток о том, что пришло время завершить свою текущую задачу, а любая другая задача с очередью затем синхронизируется с потоком, который уничтожает этот объект; в дополнение к запрету функции-члена enqueue.

Для вашего удобства, реализация деструктора ниже:

ThreadPool::~ThreadPool() { 
    { 
    std::lock_guard<mutex> lock(this->instance_mutex_); // this line. 

    this->destroy_ = true; 
    } 

    this->cond_handle_task_.notify_all(); 

    for (auto &worker : this->workers_) { 
    worker.join(); 
    } 
} 

Q: Я не понимаю, почему это необходимо, чтобы заблокировать мьютекс объекта, а переключая destroy_ к true в деструкторе. Кроме того, необходимо ли это только для установки его значения или же оно необходимо для доступа к его стоимости?

BQ: Может ли эта реализация пула потоков быть улучшена или оптимизирована при сохранении ее первоначальной цели; пул потоков, который может объединить N количество потоков и распределить задачи для их выполнения одновременно? реализация бассейн


Этот поток разветвляется от Jakob Progsch's C++11 thread pool repository с основательным шагом кода через понимать конечную цель его реализации и некоторые субъективные изменения стиля.

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

+0

Флаг уничтожения является частью предиката *. Правило №1 протокола condition-var/mutex/предикат: никогда не изменяйте * и даже не проверяйте * данные предиката, если только они не защищены защитой мьютекса. Помните, что мьютекс защищает данные предиката; а не переменная условия. Если бы флаг уничтожения мог быть изменен атомарно, он был бы освобожден. Как показано, это не так. Показанный код правильный, и хотя он может казаться излишним, это правильный шаблон для изучения. – WhozCraig

+0

Я думаю, что основная путаница здесь в том, что мьютекс часто является излишне тяжелой операцией для того, что на самом деле является синхронизирующей памятью или атомной операцией чтения/записи, обе из которых доступны как часть стандарта C++ 11 ... IMHO, использующий мьютекс, был «безопасным» способом предотвращения доброкачественных гонок данных перед атомикой, но теперь это не самый оптимальный способ ... – Jason

+3

Я рекомендую книгу Энтони Уильямса * C++ Concurrency In Action * любому, кто изучает параллельное программирование на C++ , он охватывает параллелизм C++ 11, включая библиотеку потоков и атомов, модель памяти C++ и т. д. В разделе 9.1 есть код для простого потока threadpool. В ней эквивалентная переменная - это * std :: atomic_bool done *, и деструктор просто устанавливает * done = true *, позволяя компилятору решить, требуется ли что-либо, кроме хранилища в памяти (оно не находится на x86 из-за его Total Store Модель памяти заказа). см. http://stackoverflow.com/questions/388242/the-definitive-c-book-guide-and-list – amdn

ответ

2

Если владеющее нить ThreadPool объекта является единственной нитью, которая атомарно пишет в переменную destroy_, и рабочие потоки только атомарно считываемые с переменной destroy_, то нет, мьютекс не требуется, чтобы защитить переменную destroy_ в деструктор ThreadPool. Как правило, мьютекс необходим, когда должен выполняться атомный набор операций, который не может быть выполнен с помощью одной атомной инструкции на платформе (т. Е. Операций за пределами атомного обмена и т. Д.). При этом автор пула потоков может пытаться заставить некоторый тип семантики получения в переменной destroy_ без восстановления для атомных операций (т. Е. Операции забора памяти) и/или установки самого флага не считается (зависит от платформы) ... Некоторые другие варианты включают объявление переменной как volatile, чтобы предотвратить ее кэширование и т. д. Вы можете увидеть this thread для получения дополнительной информации.

Без какой-либо операции синхронизации на самом деле сценарий наихудшего случая может закончиться рабочим, который не завершится из-за того, что переменная destroy_ кэшируется в потоке. На платформах с моделями упорядочения слабее памяти, что всегда есть возможность, если вы позволили доброкачественному состоянию гонки памяти существовать ...

0

C++ определяет гоночный данные в нескольких потоков потенциально доступ к объекту одновременно, по меньшей мере, один из тех, доступ - запись. Программы с расчетом данных имеют неопределенное поведение. Если вы должны были написать destroy в своем деструкторе без использования мьютекса, ваша программа имела бы неопределенное поведение, и мы не можем предсказать, что произойдет.

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

Смежные вопросы