Я слышал, что const
означает потокобезопасность в С++ 11. Это правда?
Означает ли это, что const
теперь эквивалентно Java synchronized
?
У них заканчиваются ключевые слова?
Я слышал, что const
означает потокобезопасность в С++ 11. Это правда?
Означает ли это, что const
теперь эквивалентно Java synchronized
?
У них заканчиваются ключевые слова?
Я слышал, что
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
теперь эквивалентно Javasynchronized
?
Нет. Совсем нет...
Рассмотрим следующий чрезмерно упрощенный класс, представляющий прямоугольник:
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