Сопоставляет ли потокобезопасность в С++ 11?

Я слышал, что const означает потокобезопасность в С++ 11. Это правда?

Означает ли это, что const теперь эквивалентно Java synchronized?

У них заканчиваются ключевые слова?

Ответ 1

Я слышал, что const означает потокобезопасность в С++ 11. Это правда?

Это несколько true...

Это то, что должен сказать стандартный язык при обеспечении безопасности потоков:

[1.10/4]Две оценки выражений противоречат друг другу, если один из них изменяет расположение памяти (1.7), а другой получает или изменяет одну и ту же ячейку памяти.

[1.10/21]Выполнение программы содержит гонку данных, если она содержит два конфликтующих действия в разных потоках, по крайней мере один из которых не является атомарным, и не происходит до другого. Любая такая гонка данных приводит к поведению undefined.

что является не чем иным, как достаточным условием для гонки данных:

  • В настоящее время выполняется одно или несколько действий по данной вещи; и
  • По крайней мере один из них - это запись.

Стандартная библиотека основывается на этом, идя немного дальше:

[17.6.5.9/1]В этом разделе определяются требования, которые должны выполняться для предотвращения сбоев данных (1.10). Каждая стандартная библиотечная функция должна отвечать каждому требованию, если не указано иное. Реализации могут препятствовать распространению данных в случаях, отличных от указанных ниже.

[17.6.5.9/3]Стандартная библиотечная функция С++ не должна прямо или косвенно изменять объекты (1.10), доступные для потоков, отличных от текущего потока, если только объекты не получают прямого или косвенного доступа через аргументы функции const, включая this.

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

  • Состоит исключительно из чтения - то есть нет записей...; или
  • Внутренняя синхронизация записей.

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

Значит ли это, что const теперь эквивалентно Java synchronized?

Нет. Совсем нет...

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

class rect {
    int width = 0, height = 0;

public:
    /*...*/
    void set_size( int new_width, int new_height ) {
        width = new_width;
        height = new_height;
    }
    int area() const {
        return width * height;
    }
};

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

Обратите внимание, что это не означает, что rect является потокобезопасным. Фактически, его легко увидеть, как если бы вызов area должен происходить одновременно с вызовом set_size на заданный rect, тогда area мог бы вычислить свой результат на основе старого ширину и новую высоту (или даже искаженные значения).

Но все в порядке, rect не const, поэтому даже не ожидается, что он будет потокобезопасным. С другой стороны, объект, объявленный const rect, был бы потокобезопасным, поскольку невозможна запись (и если вы рассматриваете const_cast -в что-то изначально объявленного const, то вы получаете undefined -behavior и что это).

Итак, что это значит?

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

class rect {
    int width = 0, height = 0;

    mutable int cached_area = 0;
    mutable bool cached_area_valid = true;

public:
    /*...*/
    void set_size( int new_width, int new_height ) {
        cached_area_valid = ( width == new_width && height == new_height );
        width = new_width;
        height = new_height;
    }
    int area() const {
        if( !cached_area_valid ) {
            cached_area = width;
            cached_area *= height;
            cached_area_valid = true;
        }
        return cached_area;
    }
};

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

Функция-член area уже не является потокобезопасной, она делает запись сейчас и не внутренне синхронизирована. Это проблема? Вызов area может выполняться как часть конструктора-копии другого объекта, такой конструктор может быть вызван некоторой операцией в стандартном контейнере, и в этот момент стандартная библиотека ожидает, что эта операция будет вести себя как прочитанная в отношении к гонкам данных. Но мы делаем записи!

Как только мы помещаем a rect в стандартный контейнер - прямо или косвенно - мы заключаем контракт со стандартной библиотекой. Чтобы делать записи в функции const, все еще соблюдая этот контракт, нам необходимо внутренне синхронизировать эти записи:

class rect {
    int width = 0, height = 0;

    mutable std::mutex cache_mutex;
    mutable int cached_area = 0;
    mutable bool cached_area_valid = true;

public:
    /*...*/
    void set_size( int new_width, int new_height ) {
        if( new_width != width || new_height != height )
        {
            std::lock_guard< std::mutex > guard( cache_mutex );

            cached_area_valid = false;
        }
        width = new_width;
        height = new_height;
    }
    int area() const {
        std::lock_guard< std::mutex > guard( cache_mutex );

        if( !cached_area_valid ) {
            cached_area = width;
            cached_area *= height;
            cached_area_valid = true;
        }
        return cached_area;
    }
};

Обратите внимание, что мы сделали функцию area безопасной для потоков, но rect по-прежнему не является потокобезопасной. Вызов area происходит в то же время, когда вызов set_size может по-прежнему заканчивать вычисление неправильного значения, поскольку присвоения width и height не защищены мьютексом.

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

У них заканчиваются ключевые слова?

Да, они есть. У них заканчиваются ключевые слова с первого дня.


Источник: Вы не знаете const и mutable - Herb Sutter