Чи означає const безпечний для потоків у C ++ 11?


115

Я чую, що це constозначає, що безпечно для потоків у C ++ 11 . Це правда?

Чи означає це constтепер еквівалент Java «з synchronized?

У них не вистачає ключових слів ?


1
C ++ - faq, як правило, керується спільнотою C ++, і ви можете люб'язно завітати і попросити нас думок у чаті.
Щеня

@DeadMG: Мені не було відомо про C ++ - faq та його етикет, це було запропоновано в коментарі.
К-балон

2
Де ви чули, що const означає захист від потоку?
Марк Б

2
@Mark B: Herb Sutter і Бйорн Страуструп говорили так в Standard Foundation C ++ , см посилання в нижній частині відповіді.
К-балон

ПРИМІТКА ДО ТОМУ, ЩО БУДЕ ТУТ: справжнє питання НЕ const означає, чи означає це безпечно для потоків. Це було б нісенітницею, оскільки в іншому випадку це означатиме, що ви повинні мати можливість просто рухатися вперед і відзначати кожен безпечний метод як const. Скоріше, питання, яке ми насправді задаємо, - це const IMPLIES, захищений від потоку, і саме про це йдеться в цій дискусії.
користувач541686

Відповіді:


131

Я чую, що це 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не захищені mutex.

Якби ми справді хотіли захистити нитку rect , використовували б примітивність синхронізації, щоб захистити захист від потоку rect .

У них не вистачає ключових слів ?

Так, вони є. У них не вистачає ключових слів з першого дня.


Джерело : Ви не знаєте constіmutable - Герб Саттер


6
@Ben Voigt: Наскільки я розумію, специфікація C ++ 11std::string формулюється таким чином, що вже забороняє ТРВ . Я не пам’ятаю конкретики, хоча ...
K-ball

3
@BenVoigt: Ні. Це просто запобіжить несинхронізації таких речей, тобто не захищених від потоку. C ++ 11 вже прямо забороняє КРВ - цей конкретний уривок не має нічого спільного з цим, однак і не забороняє корову.
Щеня

2
Мені здається, що є логічний розрив. [17.6.5.9/3] забороняє "занадто багато", кажучи, що "це не може прямо чи опосередковано змінюватися"; він повинен сказати "не повинен прямо чи опосередковано вводити перегони даних", якщо тільки атомна запис десь не визначена і не є "модифікацією". Але я ніде цього не можу знайти.
Енді Проул

1
Я, напевно, зробив тут всю свою думку трохи зрозумілішою: isocpp.org/blog/2012/12/… Дякую за те, що намагаєтесь допомогти.
Енді Проул

1
інколи мені цікаво, хто саме (або ті, хто безпосередньо бере участь) насправді відповідав за запис деяких стандартних абзаців, таких як цей.
pepper_chico
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.