Nano Hash - криптовалюты, майнинг, программирование

Параллельная неупорядоченная карта TBB вызывает segfault

Мне нужно реализовать огромную хеш-таблицу, которая поддерживает несколько потоков для вставки и получения одновременно. Ключи — int, а вторые элементы — векторы объекта T.

class T { 
        //class definitions here
}

В настоящее время реализации помогает tbb::concurrent_unordered_map. В документации кажется, что вставка и обход одновременно. Однако работа с несколькими потоками приведет к ошибке сегментации, хотя последовательный тест вполне подойдет. Обратите внимание, что определенно нет операций стирания или извлечения, т.е. хеш-таблица может только расти.

std::vector<T*> get(int key) {
        //Note that the hash table hashTable is shared by multiple threads
        tbb::concurrent_unordered_map<int, std::vector<T*>>::iterator it = hashTable.find(key);
        if (it != hashTable.end())
                return it->second;
        else {
                std::vector<T*> newvector;
                return newvector;
        }
}

void insert(int key, T *t) {
        tbb::concurrent_unordered_map<int, std::vector<T*>>::iterator it = hashTable.find(key);
        if (it != hashTable.end())
                it->second.push_back(t);
        else {
                std::vector<T*> newTs;
                newTs.push_back(t);
                hashTable.insert(it, makepair(key, newTs));
        }
}

Чтобы отладить произошедшее, я сначала изменил std::vector на tbb::concurrent_vector, а затем изменил функции get() и insert() следующим образом.

std::vector<T*> get_test(int key) {
        std::vector<T*> test;
        //Note that the hash table hashTable is shared by multiple threads
        tbb::concurrent_unordered_map<int, tbb::concurrent_vector<T*>>::iterator it = hashTable.find(key);
        if (it != hashTable.end())
                test.insert(test.end(), it->second.begin(), it->second.end());
        for (T* _t : test)
                if (!_t)
                        printf("Bug happens here!\n"); //Segfault is originated here because a NULL is returned in the vector  
        return test;
}

void insert_test(int key, T *t) {
        //Here t is guaranteed to be not NULL
        if(!t)
                std::terminate();
        tbb::concurrent_unordered_map<int, tbb::concurrent_vector<T*>>::iterator it = hashTable.find(key);
        if (it != hashTable.end())
                it->second.push_back(t);
        else {
                std::vector<T*> newTs;
                newTs.push_back(t);
                hashTable.insert(it, makepair(key, newTs));
        }
}

Теперь мы можем видеть причину сбоя параллельной программы в том, что какой-то указатель NULL возвращается в функцию get_test(). Напомним, что в функции insert_test() NULL никогда не вставляется в качестве вторых элементов.

Ниже приведены вопросы, которые следует задать.

(1) Я где-то читал, что одновременная вставка в tbb::concurrent_unordered_map может привести к сбою некоторых попыток вставки, а затем временные объекты будут уничтожены. Является ли это причиной того, что в функции get_test() наблюдается NULL?

(2) Может ли TBB действительно разрешить вставку и обход одновременно? Это означает, что пока одни потоки выполняют вставку, другие могут одновременно вызывать get().

(3) Есть ли лучшая реализация, т. е. лучшая параллельная хеш-таблица, поддерживающая одновременную вставку и чтение?


Как предложил @for_stack, я проверил, что вторые элементы являются concurrent_vectors, и вся программа запускается. Дальнейшее испытание проводится следующим образом:

tbb::concurrent_vector<T*> get_test(int key) {
            tbb::concurrent_vector<T*> test;
            //Note that the hash table hashTable is shared by multiple threads
            tbb::concurrent_unordered_map<int, tbb::concurrent_vector<T*>>::iterator it = hashTable.find(key);
            if (it != hashTable.end())
                    test = it->second;
            int i = 0;
            for (T* _t : test)
                    if (!_t)
                            i++;
            if (i > 0)
                    printf("%d of %lu Ts are NULL\n", i, test.size()); //Segfault is originated here because a NULL is returned in the vector  
            return test;
}

void insert_test(int key, T *t) {
        //Here t is guaranteed to be not NULL
        if(!t)
                std::terminate();
        tbb::concurrent_unordered_map<int, tbb::concurrent_vector<T*>>::iterator it = hashTable.find(key);
        if (it != hashTable.end())
                it->second.push_back(t);
        else {
                tbb::concurrent_vector<T*> newTs;
                newTs.push_back(t);
                hashTable.insert(it, make_pair(key, newTs));
        }
}

Выход

1 of 2 Ts are NULL

Это означает, что не все объекты (T), возвращаемые в get(), имеют значение NULL.


Опять же, последовательный (или даже 1 поток) запуск в порядке.


Ответы:


1

TBB CAN поддерживает одновременную вставку и обход для concurrent_xxx контейнеров. Однако ваш исходный код имеет условия гонки:

std::vector<T*> get(int key) {
    // other code
    return it->second;  # race condition 1
    // other code
}

Функция get пытается вернуть копию vector<T*> (чтение), в то время как другие потоки могут вызывать insert для изменения vector<T*> (запись).

void insert(int key, T *t) {
    // other code
    it->second.push_back(t);   # race condition 2
    // other code
}

Функция insert пытается изменить vector<T*> без защиты блокировки. Если есть несколько потоков, вызывающих insert одновременно (множественная запись), OOPS!

concurrent_unordered_map имеет безопасную гарантию только для операций с контейнером, а не для операций с mapped_value. Вы должны сделать это самостоятельно.

Как вы уже пробовали, вы можете заменить vector<T*> на concurrent_vector<T*>. Однако новый код, который вы разместили, не компилируется, вам нужно изменить реализацию insert_test:

void insert_test(int key, T *t) {
    //Here t is guaranteed to be not NULL
    if(!t)
            std::terminate();
    tbb::concurrent_unordered_map<int, tbb::concurrent_vector<T*>>::iterator it = hashTable.find(key);
    if (it != hashTable.end())
            it->second.push_back(t);
    else {
            // std::vector<T*> newTs;   # this is wrong!
            tbb::concurrent_vector<T*> newTs;
            newTs.push_back(t);
            hashTable.insert(it, make_pair(key, newTs));  // it should be make_pair not makepair
    }
}
22.08.2016
  • Ценю ваше объяснение. Раньше я думал, что TBB автоматически применит блокировку для одновременной вставки того же ключа. Однако приведенный выше код больше похож на псевдокод, и в реальной реализации вторым элементом действительно является tbb::concurrent_vector, и вся программа вылетает после более длительного времени (чем при использовании std::vector). Причина указана выше, так как некоторые указатели NULL возвращаются в get(), и я не думаю, что изменение типа возврата get() на concurrent_vector даже поможет. Может быть, мне следует отредактировать код, чтобы не вызывать путаницы. 22.08.2016

  • 2

    «TBB МОЖЕТ поддерживать одновременную вставку и обход для контейнеров concurrent_xxx». - не совсем. Обход — сложная штука, когда нет поддержки высвобождения памяти, как в TBB, а параллельное стирание поддерживается контейнером (concurrent_hash_map). Однако concurrent_unordered_map не поддерживает потокобезопасный erase(), поэтому поддерживается потокобезопасный обход.

    22.08.2016
  • Не могли бы вы на нескольких примерах показать, почему Traversal — сложная штука, когда нет поддержки высвобождения памяти, как в TBB? 22.08.2016

  • 3

    @Антон, мой друг, контейнеры concurrent_unordered поддерживают параллельный обход и вставку; они реализованы в виде списков пропуска. В немножественном случае проверяется результат качания указателя, и если он не удается, поиск начинается снова с точки вставки.

    Теперь C++, возможно, изменился за последние несколько недель с тех пор, как я работал в Intel, но я думаю, что в исходном коде есть серьезные ошибки:

    if (it != hashTable.end())
            return it->second;          // return a copy???
    else {
            std::vector<T*> newvector;  // this is stack-allocated
            return newvector;           // return a copy??
    }
    

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

    23.08.2016
  • Крис, приятно снова видеть тебя здесь! :) Мы можем сказать, что контейнеры concurrent_unordered_xxx поддерживают параллельный обход и вставку, но мы не можем сказать, что контейнеры concurrent_xxx поддерживают параллельный обход и вставку - разница находится в этой неупорядоченной части, потому что concurrent_hash_map не поддерживает безопасный обход, по крайней мере, не полностью/официально: ) Пс разделенный упорядоченный список 27.08.2016
  • Упс! Совершенно верно, Антон. Моя оплошность. Ага, раздельный заказ. Это был мозговой пердеж. 28.08.2016
  • Новые материалы

    Кластеризация: более глубокий взгляд
    Кластеризация — это метод обучения без учителя, в котором мы пытаемся найти группы в наборе данных на основе некоторых известных или неизвестных свойств, которые могут существовать. Независимо от..

    Как написать эффективное резюме
    Предложения по дизайну и макету, чтобы представить себя профессионально Вам не позвонили на собеседование после того, как вы несколько раз подали заявку на работу своей мечты? У вас может..

    Частный метод Python: улучшение инкапсуляции и безопасности
    Введение Python — универсальный и мощный язык программирования, известный своей простотой и удобством использования. Одной из ключевых особенностей, отличающих Python от других языков, является..

    Как я автоматизирую тестирование с помощью Jest
    Шутка для победы, когда дело касается автоматизации тестирования Одной очень важной частью разработки программного обеспечения является автоматизация тестирования, поскольку она создает..

    Работа с векторными символическими архитектурами, часть 4 (искусственный интеллект)
    Hyperseed: неконтролируемое обучение с векторными символическими архитектурами (arXiv) Автор: Евгений Осипов , Сачин Кахавала , Диланта Хапутантри , Тимал Кемпития , Дасвин Де Сильва ,..

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

    Обеспечение масштабируемости LLM: облачный анализ с помощью AWS Fargate и Copilot
    В динамичной области искусственного интеллекта все большее распространение получают модели больших языков (LLM). Они жизненно важны для различных приложений, таких как интеллектуальные..