Какой уровень детализации блокировки хорош в параллельных структурах данных?

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

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

Итак, я прихожу к вам с объявлением класса и примером функции, и этот вопрос: это слишком мелко? Недостаточно мелкозернистый? Плохо продумано?

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 — это то, что я собрал, используя мьютекс и пару условных переменных.

4

Решение

Замки неэффективны независимо от того, насколько они мелкозернисты, поэтому избегайте их любой ценой.

И очередь, и вектор могут быть легко реализованы без блокировки с помощью 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)

3

Другие решения

Хорошее начало — вы блокируете вещи при необходимости и освобождаете их, как только закончите.

Ваш 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();
}

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

1

По вопросам рекламы [email protected]