C ++: многопоточность и пересчет объекта - PullRequest
7 голосов
/ 03 октября 2008

В настоящее время я пытаюсь передать однопотоковую программу в многопоточность. Это программное обеспечение интенсивно использует объекты «refCounts», что приводит к некоторым проблемам в многопоточности. Я ищу какой-то шаблон дизайна или что-то, что может решить мою проблему.

Основная проблема - удаление объекта между потоками, обычно удаление только уменьшает счетчик ссылок, а когда refcount равен нулю, тогда объект удаляется. Это хорошо работает в монопоточной программе и позволяет значительно улучшить производительность при копировании большого объекта.

Однако в многопоточности два потока могут захотеть удалить один и тот же объект одновременно, поскольку объект защищен мьютексом, только один поток удаляет объект и блокирует другой. Но когда он освобождает мьютекс, другой поток продолжает выполнение с недопустимым (освобожденным объектом), что приводит к повреждению памяти.

Вот пример с этим классом RefCountingObject

class RefCountedObject
{
public:
RefCountedObject()
:   _refCount( new U32(1) )
{}

RefCountedObject( const RefCountedObject& obj )
:   _refCount( obj._refCount )
{
    ACE_Guard< ACE_Mutex > guard( _refCountMutex );
    ++(*_refCount);
}

~RefCountedObject()
{
    Destroy();
}

RefCountedObject& operator=( const RefCountedObject& obj )
{
    if( this != &obj )
    {
        Destroy();
        ACE_Guard< ACE_Mutex > guard( _refCountMutex );
        _refCount = obj._refCount;
        ++(*_refCount);
    }

    return *this;
}

private:
    void Destroy()
    {
        ACE_Guard< ACE_Mutex > guard( _refCountMutex );  // thread2 are waiting here
        --(*_refCount);         // This cause a free memory write by the thread2
        if( 0 == *_refCount )
            delete _refCount;
    }

private:
    mutable U32* _refCount;
    mutable ACE_Mutex _refCountMutex; // BAD: this mutex only protect the refCount pointer, not the refCount itself
};

Предположим, что два потока хотят удалить один и тот же RefCountsObject, оба находятся в ~ RefCountingObject и вызывают Destroy (), первый поток заблокировал мьютекс, а другой ожидает. После удаления объекта первым потоком второй продолжит выполнение и вызовет свободную запись в память.

Кто-нибудь сталкивался с подобной проблемой и нашел решение?


Спасибо всем за помощь, я осознаю свою ошибку: Мьютекс защищает только указатель refCount, а не сам refCount! Я создал класс RefCount, который защищен мьютексом. Теперь мьютекс распределяется между всеми объектами refCounts.

Теперь все отлично работает.

Ответы [ 7 ]

4 голосов
/ 03 октября 2008

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

Одним из вариантов будет использование boost::shared_ptr (см. Документы) . Вы можете использовать бесплатные функции atomic_load, atomic_store, atomic_exchange и atomic_compare_exchange (которые явно отсутствуют в документации), чтобы обеспечить подходящую защиту при доступе к глобальным указателям на общие объекты. Как только ваш поток получил shared_ptr, ссылающийся на определенный объект, вы можете использовать обычные неатомарные функции для доступа к нему.

Другой вариант - использовать атомный указатель с пересчитанным числом Джо Сая из его проекта atomic_ptr_plus

3 голосов
/ 03 октября 2008

Конечно, каждому потоку просто необходимо правильно управлять счетчиками ссылок ... То есть, если ThreadA и ThreadB оба работают с Obj1, тогда ОБА ThreadA и ThreadB должны иметь ссылку на объект, а ОБА должны вызывать release, когда они сделано с объектом.

В однопоточном приложении вполне вероятно, что у вас есть точка, в которой создается объект с подсчетом ссылок, затем вы работаете с этим объектом и в конце концов вызываете release. В многопоточной программе вы должны создать объект и затем передать его своим потокам (как бы вы это ни делали). Перед передачей объекта в поток вы должны вызвать AddRef () для вашего объекта, чтобы дать потоку собственный счетчик ссылок. Поток, который выделил объект, может затем вызвать release, как это было сделано с объектом. Потоки, работающие с объектом, будут вызывать release по завершении и после освобождения последней ссылки, объект будет очищен.

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

1 голос
/ 03 октября 2008

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

Проблемы с подсчетом рефлексов, как правило, сохраняются, поэтому в долгосрочной перспективе инвестиции в ваш тестовый комплект окупятся

1 голос
/ 03 октября 2008

немного подумав о вашей проблеме ... вы говорите, что у вас есть 1 объект (если refcount равен 1), и все же 2 потока оба вызывают delete (). Я думаю, что именно в этом ваша проблема.

Другой способ обойти эту проблему, если вам нужен многопоточный объект, который вы можете безопасно использовать между потоками, - это проверить, что refcount больше 1, прежде чем освобождать внутреннюю память. В настоящее время вы освобождаете его, а затем проверяете, равен ли этот счет 0. 0. 1003 *

0 голосов
/ 03 октября 2008

Я верю, что что-то в этом духе решит вашу проблему:

<code>private:
    void Destroy()
    {<br>
        ACE_Guard< ACE_Mutex > guard( _refCountMutex );  // thread2 are waiting here
        if (_refCount != 0) {
            --(*_refCount);         // This cause a free memory write by the thread2
            if( 0 == *_refCount ) {
                delete _refCount;
                _refcount = 0;
            }
        }
    }
private:
    mutable U32* _refCount;
    mutable ACE_Mutex _refCountMutex;
0 голосов
/ 03 октября 2008

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

Библиотека Intel Thread Building Blocks (TBB) предоставляет атомарные значения.

Также, библиотека ACE в шаблоне ACE_Atomic_Op.

Библиотека Boost предоставляет библиотеку интеллектуальных указателей для подсчета ссылок, которая уже реализует это.

http://www.dre.vanderbilt.edu/Doxygen/Current/html/ace/a00029.html http://www.boost.org/doc/libs/release/libs/smart_ptr/shared_ptr.htm

0 голосов
/ 03 октября 2008

Любой объект, которым вы делитесь между потоками, должен быть защищен мьютексом, и то же самое относится к refcount handles ! Это означает, что вы никогда не будете удалять последний дескриптор объекта из двух потоков. Возможно, вы одновременно удаляете два разных дескриптора, которые указывают на один объект.

В Windows вы можете использовать InterlockedDecrement. Это гарантирует, что точно один из двух декрементов вернет 0. Только этот поток удалит пересчитанный объект.

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

...