Ориентированный на многопотоковое исполнение дизайн структуры данных

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

if(!data_structure.exists(element)) {
   data_structure.insert(element);
}

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

Что я в настоящее время имею в виду для решения, это выставляет блокировку через общедоступный API структуры данных. Таким образом, клиенты должны будут явно заблокировать вещи, но по крайней мере они не должны будут создавать свои собственные блокировки. Существует ли лучшее обычно известное решение этих видов проблем? И, пока мы в нем, можно ли советовать некоторой хорошей литературе по ориентированному на многопотоковое исполнение дизайну?

Править: У меня есть лучший пример. Предположим, что извлечение элемента возвращает или ссылку или указатель на сохраненный элемент, и не это - копия. Как вызывающая сторона может быть защищена для безопасного использования этого pointer\reference после возвратов вызова? Если Вы думаете, что не возврат копирует, проблема, то думайте о глубоких копиях, т.е. объектах, которые должны также скопировать, другой возражает, что они указывают на внутренне.

Спасибо.

8
задан Inso Reiges 19 March 2010 в 10:49
поделиться

5 ответов

Вы либо предоставляете механизм для внешней блокировки (плохо), либо переделываете API, как putIfAbsent. Последний подход, например, используется в параллельных структурах данных Java.

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

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

Одна, возможно важная, подсказка по производительности содержится в ответе Уилла ниже. [/edit]

[edit2]Учитывая ваш новый пример: В основном вы должны стараться максимально разделить синхронизацию коллекции и синхронизацию элементов. Если время жизни элемента привязано к его присутствию в одной коллекции, вы столкнетесь с проблемами; при использовании GC эта проблема становится проще. В противном случае вам придется использовать своего рода прокси вместо сырых элементов для нахождения в коллекции; в простейшем случае для C++ вы могли бы пойти и использовать boost::shared_ptr, который использует атомарный ref-count. Вставьте здесь обычное предупреждение о производительности. Если вы используете C++ (как я подозреваю, поскольку вы говорите об указателях и ссылках), комбинации boost::shared_ptr и boost::make_shared должно хватить на некоторое время. [/edit2]

4
ответ дан 5 December 2019 в 21:17
поделиться

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

  1. Структура данных и ее методы.
  2. Синхронизация потоков.

Я настоятельно рекомендую вам использовать интерфейс или виртуальный базовый класс, который представляет тип структуры данных, которую вы реализуете. Создайте простую реализацию, которая вообще не выполняет никаких блокировок. Затем создайте вторую реализацию, которая обертывает первую реализацию и добавляет к ней блокировку. Это позволит реализовать более производительную реализацию, в которой блокировка не требуется, и значительно упростит ваш код.

Похоже, вы реализуете какой-то словарь. Одна вещь, которую вы можете сделать, - это предоставить методы, семантика которых эквивалентна комбинированному оператору. Например, setdefault - это разумная функция, которая будет устанавливать значение только в том случае, если соответствующий ключ еще не существует в словаре.

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

0
ответ дан 5 December 2019 в 21:17
поделиться

А как насчет переноса проверки существования в метод .insert () ? Клиент вызывает его, и если он возвращает false , вы знаете, что что-то пошло не так. Очень похоже на то, что делает malloc () в простом старом C - верните NULL в случае неудачи, установите ERRNO .

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

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

0
ответ дан 5 December 2019 в 21:17
поделиться

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

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

В RAII-языке, таком как C++, вы можете использовать класс интеллектуального стека для инкапсуляции возвращаемого курсора, чтобы он автоматически откатывался назад, если вызывающий код не зафиксировал его. В Java это немного более отсрочено с finalize() методом, но все же может работать.

Другой подход заключается в том, чтобы вызывающая программа создавала объект, которого нет, но при этом иногда терпела неудачу при фактической вставке, если другой поток "выиграл гонку". Так, например, выполняется обновление memcache. Это может работать очень хорошо.

3
ответ дан 5 December 2019 в 21:17
поделиться

В стиле RAII вы могли бы создавать объекты-аксессоры/ручки (не знаю, как они называются, вероятно, существует такая схема), например, List:

template <typename T>
class List {
    friend class ListHandle<T>;
    // private methods use NO locking
    bool _exists( const T& e ) { ... }
    void _insert( const T& e ) { ... }
    void _lock();
    void _unlock();
public:
    // public methods use internal locking and just wrap the private methods
    bool exists( const T& e ) {
        raii_lock l;
        return _exists( e );
    }
    void insert( const T& e ) {
        raii_lock l;
        _insert( e );
    }
    ...
};

template <typename T>
class ListHandle {
    List<T>& list;
public:
    ListHandle( List<T>& l ) : list(l) {
        list._lock();
    }
    ~ListHandle() {
        list._unlock();
    }
    bool exists( const T& e ) { return list._exists(e); }
    void insert( const T& e ) { list._insert(e); }
};


List<int> list;

void foo() {
    ListHandle<int> hndl( list ); // locks the list as long as it exists
    if( hndl.exists(element) ) {
        hndl.insert(element);
    }
    // list is unlocked here (ListHandle destructor)
}

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

0
ответ дан 5 December 2019 в 21:17
поделиться
Другие вопросы по тегам:

Похожие вопросы: