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

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

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

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


person K-ballo    schedule 02.01.2013    source источник
comment
Часто задаваемые вопросы по C++, как правило, администрируются сообществом C++, и вы можете прийти и спросить у нас мнения в нашем чате.   -  person Puppy    schedule 02.01.2013
comment
@DeadMG: я не знал о C++-faq и его этикете, это было предложено в комментарии.   -  person K-ballo    schedule 02.01.2013
comment
Где вы узнали, что const означает потокобезопасность?   -  person Mark B    schedule 03.01.2013
comment
@Mark B: Херб Саттер и Бьерн Страуструп говорили об этом в Standard C++ Foundation, см. ссылку внизу ответа.   -  person K-ballo    schedule 03.01.2013
comment
ПРИМЕЧАНИЕ ДЛЯ ТЕХ, КТО ЗДЕСЬ: настоящий вопрос НЕ в том, является ли const означает потокобезопасным. Это было бы чепухой, поскольку в противном случае это означало бы, что вы должны просто перейти вперед и пометить каждый потокобезопасный метод как const. Скорее, вопрос, который мы действительно задаем, const ПРЕДПОЛАГАЕТ потокобезопасность, и это то, о чем идет речь в данном обсуждении.   -  person user541686    schedule 30.07.2019
comment
Людей, читающих этот вопрос, также может заинтересовать: Каково определение потокобезопасной функции в C++11?   -  person m7913d    schedule 19.04.2021


Ответы (2)


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

Это отчасти верно...

Вот что говорит Standard Language о безопасности потоков:

[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 или нет, это не повлияет на генерацию кода — по крайней мере, не в отношении гонок данных—.

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

Нет. Нисколько...

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

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< /em> и все).

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

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

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 - Херб Саттер

person K-ballo    schedule 02.01.2013
comment
Спасибо за форматирование этого выступления, возможно, стоит пометить его как c++-faq. - person Matthieu M.; 02.01.2013
comment
@Матье М.: Готово. Я не знал об этом теге. Спасибо. - person K-ballo; 02.01.2013
comment
Очень информативно. Это, по-видимому, запрещает реализации std::string копирования при записи с подсчетом ссылок, безопасность потоков которых обсуждалась в недавнем вопросе, поскольку std::string::string(const std:string& other) не разрешено изменять счетчик использования в other. Вы согласны? Какие последствия есть для std::shared_ptr? - person Ben Voigt; 02.01.2013
comment
(Это, вероятно, заслуживает отдельного вопроса или двух, которые я опубликую, когда у меня будет время.) - person Ben Voigt; 02.01.2013
comment
@Ben Voigt: насколько я понимаю, спецификация C++11 для std::string сформулирована таким образом, что уже запрещает COW. Хотя подробностей не помню... - person K-ballo; 02.01.2013
comment
@BenVoigt: Нет. Это просто предотвратило бы несинхронизацию таких вещей, т. Е. Не потокобезопасность. C++11 уже явно запрещает COW — этот конкретный отрывок не имеет к этому никакого отношения и не будет запрещать COW. - person Puppy; 02.01.2013
comment
@DeadMG: я не понимаю, как нельзя прямо или косвенно изменять разрешение на синхронизированную модификацию. Да, есть бит доступности из других потоков, но синхронизация в одной функции не делает ее недоступной из других. - person Ben Voigt; 02.01.2013
comment
@K-ballo удалил мой комментарий, потому что я понял, что вызов двух методов (r.width() * r.height()) никогда не будет потокобезопасным, даже если объект внутренне потокобезопасен. - person StackedCrooked; 02.01.2013
comment
@Ben: Modify определяется со ссылкой на 1.10, что позволяет правильно синхронизировать запись. - person Puppy; 02.01.2013
comment
Забавно, сегодня днем ​​я смотрел выступление Херба Саттера и надеялся найти место для обсуждения. Спасибо за исполнение моего желания. При этом есть еще кое-что, что я не совсем понимаю в формулировке [17.6.5.9/3]: вместо того, чтобы писать, не должен прямо или косвенно модифицировать, не должен ли он писать, не должен прямо или косвенно вводите данные гонки? Я имею в виду, что если функция const изменяет переменную mutable, то она действительно изменяет значение, хранящееся в этой ячейке памяти. Просто эта модификация не получается создавать скачки данных по [1.10/21], потому что она атомарна. - person Andy Prowl; 03.01.2013
comment
@Andy Prowl: DeadMG рассказал об этом в нескольких комментариях, это изменить, как описано в 1.10. - person K-ballo; 03.01.2013
comment
@K-ballo: пункт 1.10 содержит 25 абзацев. Быстро прочитав их, я не смог выяснить, какой из них объясняет, что операция, не являющаяся modify, может включать записи, отличные от блокирующих мьютексов и т. д. Но я допускаю, что, возможно, просто проглядел это. Я продолжу поиск, но если у вас есть время, не могли бы вы указать мне нужный абзац? - person Andy Prowl; 03.01.2013
comment
@K-ballo: может быть, я просто слишком глуп, чтобы понять это, но мне нужна помощь. p21 определяет гонку данных и, в частности, утверждает, что гонки данных нет, когда одно из действий является атомарным. Однако в нем не говорится, что атомарное действие не является модификацией, в то время как параграф STL явно запрещает модификации. Что я считаю слишком много. Он должен только запрещать гонки данных. Я чушь пишу? - person Andy Prowl; 03.01.2013
comment
@Энди Проул: кажется, я неправильно понял твой комментарий. Спецификация не гарантирует, что она не создает гонок, и не запрещает модификаций. Он просто считает, что записи могут быть введены только вызовами неконстантных объектов, и если это правда, то это не приведет к гонкам данных. - person K-ballo; 03.01.2013
comment
@K-ballo: Но если вы пишете синхронизированную изменяемую переменную, это запись. И [17.6.5.9/3] прямо говорит, что не должен прямо или косвенно модифицировать. Кстати, вы можете продолжать здесь, или мы перейдем к чату? Или у вас нет на это времени? - person Andy Prowl; 03.01.2013
comment
@Andy Prowl: В данный момент я не могу уделить этому все свое внимание, но я хотел бы продолжить эту дискуссию сегодня в чате. - person K-ballo; 03.01.2013
comment
@K-ballo: сейчас 2 часа ночи, так что я не знаю, как долго буду здесь. есть ли способ написать мне сообщение, если меня не будет здесь позже? В любом случае, я собираюсь уточнить свою точку зрения в следующем комментарии (надеюсь, места будет достаточно) - person Andy Prowl; 03.01.2013
comment
если я правильно понял, суть в том, что внутри функции const вы можете изменять переменную mutable, если вы делаете это атомарно (т.е. с правильной синхронизацией). Однако [17.6.5.9/3] говорит: [функция STL] не должна прямо или косвенно модифицировать [свои аргументы, (среди прочего) вызывая одну из своих const функций-членов]. Но поскольку эти const функции-члены могут изменять mutable переменные, это означает, что изменение mutable переменной не рассматривается как модификация согласно [17.6.5.9/3] до тех пор, пока вы делаете это атомарно. И я не могу найти, где это указано. - person Andy Prowl; 03.01.2013
comment
Мне кажется, что есть логический пробел. [17.6.5.9/3] запрещает слишком многое, говоря, что он не должен прямо или косвенно изменяться; он должен сказать, что не должен прямо или косвенно вводить гонку данных, если атомарная запись где-то определена не как модификация. Но я не могу найти это нигде. - person Andy Prowl; 03.01.2013
comment
@Andy Prowl: Думаю, я понимаю вашу точку зрения ... Модификации через const не запрещены, но реализация стандартной библиотеки не должна этого делать, чтобы избежать гонок данных (прочитайте этот абзац в контексте с абзацем 1). Если этого недостаточно, то вам решать, как избежать гонок данных. - person K-ballo; 03.01.2013
comment
@K-ballo Я не знаю, насколько я понимаю дух и предполагаемый смысл всего этого - что, в конце концов, имеет значение - я все еще верю, что технически существует логическое пробел в формулировке, который не позволяет ему выразить предполагаемое значение. Даже с грамматической точки зрения простого присутствия номера абзаца (1.10) в [17.6.5.9/3] после изменения объектов недостаточно для того, чтобы квалифицировать это выражение как изменение объектов таким образом, который вводит гонку данных в соответствии с (1.10.4), хотя, скорее всего, имеется в виду именно это. - person Andy Prowl; 03.01.2013
comment
Я, вероятно, изложил свою мысль немного яснее здесь: isocpp.org/blog/2012/12/ Спасибо за попытку помочь. - person Andy Prowl; 03.01.2013
comment
иногда я задаюсь вопросом, кто был тем (или теми, кто принимал непосредственное участие) на самом деле ответственным за написание некоторых стандартных абзацев, таких как этот. - person pepper_chico; 31.05.2013
comment
Требуется ли std::lock_guard внутри set_size, чтобы сделать функцию area потокобезопасной (т. е. соответствовать требованиям стандартной библиотеки)? - person m7913d; 18.04.2021
comment
Правильно ли говорить, что area является потокобезопасным, даже если он обращается к памяти, которая может быть изменена set_size одновременно, что приводит к гонке данных? См. это связанный вопрос. - person m7913d; 18.04.2021

Это дополнение к ответу K-ballo.

В этом контексте злоупотребляют термином потокобезопасный. Правильная формулировка: константная функция подразумевает потокобезопасную побитовую константу или внутреннюю синхронизацию, как указано в Херб Саттер (29:43) ) сам

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

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

person m7913d    schedule 21.04.2021