Означает ли const потокобезопасность в C ++ 11?

116

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

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

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

K-Балло
источник
1
C ++ - faq обычно администрируется сообществом C ++, и вы можете прийти и спросить у нас мнение в нашем чате.
Puppy
@DeadMG: Я не знал о C ++ - faq и его этикете, это было предложено в комментарии.
K-балл
2
Где вы слышали, что const означает потокобезопасность?
Mark B
2
@Mark B: Херб Саттер и Бьярн Страуструп говорили об этом в Standard C ++ Foundation , см. Ссылку внизу ответа.
K-балл
ПРИМЕЧАНИЕ ДЛЯ ТЕМ, ЧТО ЗДЕСЬ: настоящий вопрос НЕ в том, const означает ли это поточно-ориентированное. Это было бы чепухой, иначе это означало бы, что вы могли бы просто продолжить и пометить каждый потокобезопасный метод как const. Скорее, вопрос, который мы действительно задаем, const ИМЕЕТСЯ поточно- ориентированным , и именно об этом идет речь.
user541686

Ответы:

132

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

Это несколько верно ...

Вот что стандартный язык говорит о безопасности потоков:

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

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

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

  1. Два или более действия выполняются одновременно с данным объектом; и
  2. По крайней мере, один из них написан.

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

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

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

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

  1. Состоит полностью из чтения - то есть нет записи -; или
  2. Внутренне синхронизирует записи.

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

Значит ли это 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вы получаете неопределенное поведение и все).

Так что же тогда это значит?

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

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может происходить как часть конструктора копирования другого объекта, такой конструктор мог быть вызван некоторой операцией в стандартном контейнере , и в этот момент стандартная библиотека ожидает, что эта операция будет вести себя как чтение в отношении гонок данных. , Но мы пишем!

Как только мы помещаем объект 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 - Херб Саттер

K-Балло
источник
6
@Ben Voigt: Насколько я понимаю, спецификация C ++ 11std::string сформулирована таким образом, что уже запрещает COW . Но я не помню подробностей ...
K-балл
3
@BenVoigt: Нет. Это просто предотвратит несинхронизацию таких вещей, т. Е. Небезопасные потоки. C ++ 11 уже явно запрещает COW - однако этот конкретный отрывок не имеет к этому никакого отношения и не запрещает COW.
Puppy
2
Мне кажется, здесь есть логический пробел. [17.6.5.9/3] запрещает «слишком много», говоря «это не должно прямо или косвенно изменять»; он должен сказать: «не должен прямо или косвенно вызывать гонку данных», если только где-то не определена атомарная запись, которая не является «изменением». Но я нигде не могу этого найти.
Энди Проул
1
Я, наверное, разъяснил всю свою точку зрения здесь: isocpp.org/blog/2012/12/… Спасибо за попытку помочь.
Энди Проул
1
иногда мне интересно, кто именно (или те, кто непосредственно участвовал) на самом деле отвечал за написание некоторых стандартных абзацев, таких как эти.
pepper_chico