Я довольно новичок в многопоточности, у меня есть однопоточное приложение для анализа данных, которое имеет большой потенциал для распараллеливания, и, хотя наборы данных большие, это не близко к насыщению чтения / записи на жестком диске, поэтому я думаю, Я должен воспользоваться поддержкой потоков, которая сейчас есть в стандарте, и попытаться ускорить работу зверя.
После некоторого исследования я решил, что потребитель-производитель является хорошим подходом для чтения данных с диска и его обработки, и я начал писать пул объектов, который станет частью кольцевого буфера, где производители будут помещать данные, а потребители — данные. Когда я писал класс, мне казалось, что я слишком хорошо разбираюсь в том, как я управляю блокировкой и освобождением членов данных. Такое ощущение, что половина кода блокируется и разблокируется, и как будто существует безумное количество объектов синхронизации, плавающих вокруг.
Итак, я прихожу к вам с объявлением класса и примером функции, и этот вопрос: это слишком мелко? Недостаточно мелкозернистый? Плохо продумано?
struct PoolArray
{
public:
Obj* arr;
uint32 used;
uint32 refs;
std::mutex locker;
};
class SegmentedPool
{
public: /*Construction and destruction cut out*/
void alloc(uint32 cellsNeeded, PoolPtr& ptr);
void dealloc(PoolPtr& ptr);
void clearAll();
private:
void expand();
//stores all the segments of the pool
std::vector< PoolArray<Obj> > pools;
ReadWriteLock poolLock;
//stores pools that are empty
std::queue< int > freePools;
std::mutex freeLock;
int currentPool;
ReadWriteLock currentLock;
};
void SegmentedPool::dealloc(PoolPtr& ptr)
{
//find and access the segment
poolLock.lockForRead();
PoolArray* temp = &(pools[ptr.getSeg()]);
poolLock.unlockForRead();
//reduce the count of references in the segment
temp->locker.lock();
--(temp->refs);
//if the number of references is now zero then set the segment back to unused
//and push it onto the queue of empty segments so that it can be reused
if(temp->refs==0)
{
temp->used=0;
freeLock.lock();
freePools.push(ptr.getSeg());
freeLock.unlock();
}
temp->locker.unlock();
ptr.set(NULL,-1);
}
Несколько объяснений:
First PoolPtr — это глупый маленький указатель, похожий на объект, который хранит указатель и номер сегмента в пуле, из которого поступил указатель.
Во-вторых, все это «шаблонизировано», но я взял эти строки, чтобы попытаться уменьшить длину блока кода
Третий ReadWriteLock — это то, что я собрал, используя мьютекс и пару условных переменных.
Замки неэффективны независимо от того, насколько они мелкозернисты, поэтому избегайте их любой ценой.
И очередь, и вектор могут быть легко реализованы без блокировки с помощью compare-swap
примитивный.
Есть ряд работ по теме
Блокировка свободной очереди:
Блокировка свободный вектор:
В статье Страуструпа также говорится о распределителе без блокировок, но не стоит сразу на него ссылаться, стандартные распределители довольно хороши в наши дни.
UPD
Если вы не хотите писать свои собственные контейнеры, используйте библиотеку Intel Threading Building Blocks, она обеспечивает как потокобезопасный вектор, так и очередь. Они НЕ свободны от блокировки, но оптимизированы для эффективного использования кэша ЦП.
UPD
относительно PoolArray
Вам также не нужен замок. Если вы можете использовать C ++ 11, используйте std::atomic
для атомарных приращений и перестановок, в противном случае используйте встроенные компиляторы (функции InterLocked * в MSVC и _синхронизировать* в gcc http://gcc.gnu.org/onlinedocs/gcc-4.1.1/gcc/Atomic-Builtins.html)
Хорошее начало — вы блокируете вещи при необходимости и освобождаете их, как только закончите.
Ваш ReadWriteLock
в значительной степени CCriticalSection
объект — в зависимости от ваших потребностей это может улучшить производительность, чтобы использовать его вместо этого.
Я бы сказал, что позвони temp->locker.lock();
функционировать, прежде чем снимать блокировку с пула poolLock.unlockForRead();
в противном случае вы выполняете операции над объектом пула, когда он не находится под контролем синхронизации — в этот момент он может использоваться другим потоком. Незначительный момент, но с многопоточностью — это второстепенные моменты, которые в конце концов сбивают вас с толку.
Хороший подход к многопоточности заключается в том, чтобы обернуть любые контролируемые ресурсы в объекты или функции, которые выполняют блокировку и разблокировку внутри них, так что любому, кто хочет получить доступ к данным, не нужно беспокоиться о том, какую блокировку блокировать или разблокировать, и когда это сделать. например:
...
if(temp->refs==0)
{
temp->used=0;
freeLock.lock();
freePools.push(ptr.getSeg());
freeLock.unlock();
}
...
было бы…
...
if(temp->refs==0)
{
temp->used=0;
addFreePool(ptr.getSeg());
}
...
void SegmentedPool::addFreePool(unsigned int seg)
{
freeLock.lock();
freePools.push(seg);
freeLock.unlock();
}
Существует множество инструментов для многопоточного тестирования. Вы можете поэкспериментировать с управлением своими ресурсами по-разному, запустить его с помощью одного из инструментов и посмотреть, где есть узкие места, если вы чувствуете, что производительность становится проблемой.