Легкие спин-блоки, построенные из атомных операций GCC?

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

Атомные операции, о которых я говорю, хорошо описаны здесь: http://gcc.gnu.org/onlinedocs/gcc-4.4.1/gcc/Atomic-Builtins.html

Вот пример, иллюстрирующий то, о чем я говорю. Представьте себе RB-дерево с несколькими читателями и писателями. RBTree:: exists() доступен только для чтения и потокобезопасен, RBTree:: insert() потребует исключительного доступа одним автором (и без чтения), чтобы быть в безопасности. Некоторый код:

class IntSetTest
{
private:
    unsigned short lock;
    RBTree<int>* myset;

public:
    // ...

    void add_number(int n)
    {
        // Aquire once locked==false (atomic)
        while (__sync_bool_compare_and_swap(&lock, 0, 0xffff) == false);

        // Perform a thread-unsafe operation on the set
        myset->insert(n);

        // Unlock (atomic)
        __sync_bool_compare_and_swap(&lock, 0xffff, 0);
    }

    bool check_number(int n)
    {
        // Increment once the lock is below 0xffff
        u16 savedlock = lock;
        while (savedlock == 0xffff || __sync_bool_compare_and_swap(&lock, savedlock, savedlock+1) == false)
            savedlock = lock;

        // Perform read-only operation    
        bool exists = tree->exists(n);

        // Decrement
        savedlock = lock;
        while (__sync_bool_compare_and_swap(&lock, savedlock, savedlock-1) == false)
            savedlock = lock;

        return exists;
    }
};

(предположим, что это не должно быть безопасным исключением)

Является ли этот код действительно потокобезопасным? Есть ли какие-либо плюсы и минусы этой идеи? Любой совет? Является ли использование спинлоков подобным образом плохой идеей, если потоки не являются действительно параллельными?

Спасибо заранее.;)

Ответ 1

Для lock вам нужен определитель volatile, и я также сделаю его sig_atomic_t. Без квалификатора volatile этот код:

    u16 savedlock = lock;
    while (savedlock == 0xffff || __sync_bool_compare_and_swap(&lock, savedlock, savedlock+1) == false)
        savedlock = lock;

может не перечитываться lock при обновлении savedlock в теле цикла while. Рассмотрим случай, когда lock равно 0xffff. Тогда savedlock будет иметь значение 0xffff до проверки условия цикла, поэтому условие while будет короткозамкнуто перед вызовом __sync_bool_compare_and_swap. Поскольку __sync_bool_compare_and_swap не был вызван, компилятор не сталкивается с барьером памяти, поэтому он может разумно предположить, что значение lock не изменилось под вами и не перезагружать его в savedlock.

Re: sig_atomic_t, там достойное обсуждение здесь. Те же соображения, которые применимы к обработчикам сигналов, также будут применяться к потокам.

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

Ответ 2

Возможно, стоит отметить, что если вы используете мьютексы Win32, то из Vista и далее вам предоставляется пул потоков. В зависимости от того, для чего вы используете дерево RB, вы можете заменить его.

Кроме того, вы должны помнить, что атомарные операции не особенно быстры. Microsoft заявила, что они состоят из нескольких сотен циклов.

Вместо того, чтобы пытаться "защищать" функцию таким образом, было бы намного эффективнее просто синхронизировать потоки, либо изменить подход к SIMD/потоку, либо просто использовать мьютекс.

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