Определение объема синхронизации? - PullRequest
5 голосов
/ 30 сентября 2010

, пытаясь улучшить мое понимание проблем параллелизма, я смотрю на следующий сценарий ( Редактировать: Я изменил пример с List на Runtime, что ближе к тому, что я пытаюсь):

public class Example {
    private final Object lock = new Object();
    private final Runtime runtime = Runtime.getRuntime();
    public void add(Object o) { 
        synchronized (lock) { runtime.exec(program + " -add "+o); } 
    }
    public Object[] getAll() { 
        synchronized (lock) { return runtime.exec(program + " -list "); }
    }
    public void remove(Object o) { 
        synchronized (lock) { runtime.exec(program + " -remove "+o); } 
    }
}

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

for (Object o : example.getAll()) {
    // problems if multiple threads perform this operation concurrently
    example.remove(b); 
}

Но, как уже было отмечено, нет никакой гарантии, что состояние будет согласованным между вызовами getAll() и вызовы для удаления ().Если несколько потоков вызовут это, у меня будут проблемы.Итак, мой вопрос - Как я должен позволить разработчику выполнить операцию потокобезопасным способом? В идеале я хотел бы обеспечить безопасность потока таким образом, чтобы разработчику было трудно избежать / пропустить,но в то же время не сложно достичь.До сих пор я могу представить три варианта:

A: Установите блокировку «this», чтобы объект синхронизации был доступен для вызывающего кода, который затем может обернуть блоки кода.Недостаток: Трудно реализовать во время компиляции:

synchronized (example) {
    for (Object o : example.getAll()) {
        example.remove(b);
    }
}

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

public class Example {
   ...
   public void removeAll() { 
       synchronized (lock) { Runtime.exec(program + " -clear"); } 
   }
}

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

public interface ExampleClosure {
    public void execute(Example example);
}
public Class Example {
    ...
    public void execute(ExampleClosure closure) { 
        synchronized (this) { closure.execute(this); } 
    }
}

example.execute(new ExampleClosure() {
        public void execute(Example example) { 
            for (Object o : example.getAll()) {
                example.remove(b);
            }
        }
    }
);

Есть что-то, чего мне не хватает?Как следует определить область синхронизации, чтобы гарантировать, что код является потокобезопасным?

Ответы [ 6 ]

3 голосов
/ 01 октября 2010

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

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

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

// pointer to function:
//      - takes Object by reference and can be safely altered 
//      - if returns true, Object will be removed from list

typedef bool (*callback_function)(Object *o);

public void editAll(callback_function func) {  
    synchronized (lock) {
          for each element o { if (callback_function(o)) {remove o} } }  
} 

Итак, ваш цикл становится следующим:

bool my_function(Object *o) {
 ...
     if (some condition) return true;
}

...
   editAll(my_function);
...

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

3 голосов
/ 30 сентября 2010

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

2 голосов
/ 30 сентября 2010

Я думаю, что всем не хватает его настоящей проблемы.При переборе нового массива Object и попытке удалить по одному за один раз проблема все еще технически небезопасна (хотя имплантация ArrayList не взорвалась, она просто не ожидала результатов)

Даже с CopyOnWriteArrayList существует вероятность того, что в текущем списке есть устаревшие данные, которые вы пытаетесь удалить.

Два предложенных вами предложения хороши (A и B).Мое общее предложение - Б. Сделать коллекцию потокобезопасной очень сложно.Хороший способ сделать это - предоставить клиенту как можно меньше функциональных возможностей (в пределах разумного).Поэтому было бы достаточно предложить метод removeAll и метод getAll.

Теперь вы можете в то же время сказать: «Ну, я хочу сохранить API таким, какой он есть, и позволить клиенту беспокоиться о дополнительной безопасности потоков».Если это так, документ потокобезопасности.Документируйте тот факт, что действие «искать и изменять» не является атомарным и не потокобезопасным.

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

2 голосов
/ 30 сентября 2010

Я думаю, j.u.c.CopyOnWriteArrayList - хороший пример подобной проблемы, которую вы пытаетесь решить.

У JDK была похожая проблема со списками - существовали различные способы синхронизации для произвольных методов, но не было синхронизации для нескольких вызовов (и это понятно).

Таким образом, CopyOnWriteArrayList фактически реализует тот же интерфейс, но имеет очень особый контракт, и тот, кто его вызывает, знает об этом.

Аналогично вашему решению - вам, вероятно, следует реализовать List (или какой-либо другой интерфейс) и в то же время определить специальные контракты для существующих / новых методов. Например, согласованность getAll не гарантируется, и вызовы .remove не завершаются ошибкой, если o имеет значение NULL или отсутствует в списке и т. Д. Если пользователям требуются как комбинированные, так и безопасные / согласованные параметры Ваш класс предоставит специальный метод, который делает именно это (например, safeDeleteAll), оставляя другие методы близкими к первоначальному контракту, насколько это возможно.

Итак, чтобы ответить на ваш вопрос - я бы выбрал вариант B, но также реализовал бы интерфейс, который реализует ваш исходный объект.

1 голос
/ 30 сентября 2010

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

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

1 голос
/ 30 сентября 2010

Из Javadoc для List.toArray ():

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

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

...