Bezdepbonusu7Программирование: статьи и ответы

Вопрос

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

Автор вопроса: K-balloОтветов: 2

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

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

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

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

c++thread-safetyconstantsc++11c++-faq

Ответы

Ответ 1 · рейтинг 139 · K-ballo

Я слышал, что 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 и все).

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

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

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

Ответ 2 · m7913d

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

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

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

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

Перевод вопроса и ответов сообщества Stack Overflow, лицензия CC BY-SA. Оригинал на Stack Overflow.

Похожие материалы