Нужна ли для функции-получателя мьютекс?

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

Действительно ли нужны блокировки для функций получения? Если так, почему?

class foo {
public:
    void setCount (int count) {
        boost::lock_guard<boost::mutex> lg(mutex_);
        count_ = count;
    }

    int count () {
        boost::lock_guard<boost::mutex> lg(mutex_); // mutex needed?
        return count_;
    }

private:
    boost::mutex mutex_;
    int count_;
};

Ответ 1

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

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

Ответ 2

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

Ответ 3

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

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

Кроме того, остерегайтесь, чтобы у вас не было такого кода:

void func(foo &f) {
  int temp = f.count();
  ++temp;
  f.setCount(temp);
}

Это не потокобезопасно, независимо от того, используете ли вы мьютекс или нет. Если вам нужно что-то сделать, защита мьютекса должна быть вне функций setter/getter.

Ответ 4

в вашем случае, вероятно, нет, если ваш процессор 32 бит, однако, если счетчик является сложным объектом или процессор требует более одной инструкции для обновления своего значения, тогда да

Ответ 5

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

Ответ 6

Это зависит от точной реализации заблокированного объекта. Однако, в общем, вы не хотите, чтобы кто-то менял (устанавливал?) Объект, пока кто-то еще находится в процессе чтения (получения?). Самый простой способ предотвратить это - заставить читателя заблокировать его.

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

Ответ 7

Они действительно нужны.

Представьте, что у вас есть экземпляр класса foo, полностью локальный для некоторого фрагмента кода. И у вас есть что-то вроде этого:

{
    foo j;
    some_func(j); // this stashes a reference to j where another thread can find it
    while (j.count() == 0)
        bar();
}

Предположим, оптимизатор внимательно смотрит на код bar и видит, что он не может изменить j.count_. Это позволяет оптимизатору переписать код следующим образом:

{
    foo j;
    some_func(j); // this stashes a reference to j where another thread can find it
    if (j.count() == 0)
    {
        while (1)
            bar();
    }
}

Очевидно, это катастрофа. Другой поток может вызвать j.setCount(5), и поток не сможет выйти из цикла.

Компилятор может доказать, что bar не может изменить возвращаемое значение j.count(). Если требовалось предположить, что другой поток может изменить каждое значение памяти, к которому он обращается, он никогда не сможет спрятать что-либо в регистр, что, безусловно, было бы недопустимой ситуацией.

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

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

Ответ 8

Проблема синхронизации уже описана в других ответах (в частности, у Дэвида Шварца).

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

Рассмотрим пример кода Дэвида, предполагая, что у нас есть правильно синхронизированная версия foo

{
    foo j;
    some_func(j);
    while (j.count() == 0)
    {
        // do we still expect (j.count() == 0) here?
        bar();
    }
}

Код предполагает, что условие while все еще выполняется в теле. В конце концов, так работает однопоточный код.

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

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

Иногда это имеет смысл, например,

while (foo.shouldKeepRunning())
{
    // foo event loop or something
}

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

{
    auto guard = j.lock_guard();
    while (j.count(guard) == 0) // prove to count that we're locked
    {
        // now we _know_ count is zero in the body
        // (but bar should release and re-acquire the lock or that can never change)
        bar(j);
    }
} // guard goes out of scope and unlocks