Чи безпечне виконання Майєром потоку шаблону Singleton?


145

Чи Singletonбезпечна наступна реалізація з використанням ледачої ініціалізації потоку (Меєрса Синглтон)?

static Singleton& instance()
{
     static Singleton s;
     return s;
}

Якщо ні, то чому і як зробити його безпечним?


Чи можете мені хтось пояснити, чому це не безпечно для потоків. Статті, згадані у посиланнях, обговорюють безпеку потоку з використанням альтернативної реалізації (використовуючи змінну вказівника, тобто статичну Singleton * pInstance).
Анкур



Відповіді:


168

У C ++ 11 це безпечно для потоків. Згідно стандарту , §6.7 [stmt.dcl] p4:

Якщо управління вводить декларацію одночасно під час ініціалізації змінної, одночасне виконання повинно чекати завершення ініціалізації.

Підтримка функції GCC та VS ( Динамічна ініціалізація та знищення з одночасністю , також відома як Magic Statics на MSDN ) полягає в наступному:

Дякуємо @Mankarse та @olen_gam за їх коментарі.


У C ++ 03 цей код не був безпечним для потоків. Існує стаття Майєрса під назвою "C ++ та небезпека подвійного перевірки блокування", в якій обговорюються безпечні реалізації потоку шаблону, і висновок, більш-менш, що (в C ++ 03) повне блокування навколо методу інстанції По суті, це найпростіший спосіб забезпечити належну конкурентоспроможність на всіх платформах, хоча більшість форм подвійно перевірених варіантів шаблону блокування можуть страждати від перегонових умов у певних архітектурах , якщо інструкції не переплетені із стратегічно встановленими бар'єрами пам’яті.


3
Існує також велика дискусія щодо одинарного шаблону (термін експлуатації та безпека ниток) Олександреску в сучасному дизайні C ++. Дивіться сайт Локі: loki-lib.sourceforge.net/index.php?n=Pattern.Singleton
Матьє М.

1
Ви можете створити безпечний для потоків синглтон з boost :: call_once.
CashCow

1
На жаль, ця частина стандарту не реалізована в компіляторі Visual Studio 2012 C ++. В таблиці "Основні мовні функції C ++ 11: Параметри
валюти

Фрагмент зі стандарту стосується побудови, але не знищення. Чи запобігає стандарт руйнуванню об'єкта на одному потоці, коли (або раніше) інший потік намагається отримати доступ до нього при завершенні програми?
тушканчик

IANA (мова C ++) L, але розділ 3.6.3 [basic.start.term] p2 дозволяє припустити, що не можна визначити поведінку, намагаючись отримати доступ до об'єкта після його знищення?
тушканчик

21

Щоб відповісти на ваше запитання про те, чому це не безпечно для потоків, це не тому, що перший дзвінок до instance()повинен викликати конструктор для Singleton s. Щоб бути безпечним для потоків, це повинно відбуватися в критичному розділі, але в стандарті немає вимоги приймати критичний розділ (стандарт на сьогоднішній день повністю замовчується в потоках). Компілятори часто реалізують це за допомогою простої перевірки та збільшення статичного булевого, але не в критичному розділі. Щось на зразок наступного псевдокоду:

static Singleton& instance()
{
    static bool initialized = false;
    static char s[sizeof( Singleton)];

    if (!initialized) {
        initialized = true;

        new( &s) Singleton(); // call placement new on s to construct it
    }

    return (*(reinterpret_cast<Singleton*>( &s)));
}

Отож ось простий для потоків безпечний Singleton (для Windows). Він використовує просту обгортку класу для об’єкта Windows CRITICAL_SECTION, щоб ми могли змусити компілятор автоматично ініціалізувати CRITICAL_SECTIONпопередній main()виклик. В ідеалі застосовується справжній клас критичних розділів RAII, який може вирішувати винятки, які можуть виникнути, коли критичний розділ проводиться, але це виходить за межі цієї відповіді.

Фундаментальна операція полягає в тому, що коли Singletonзапитується екземпляр , робиться замок, Singleton створюється, якщо це потрібно, тоді блокування звільняється і повертається посилання Singleton.

#include <windows.h>

class CritSection : public CRITICAL_SECTION
{
public:
    CritSection() {
        InitializeCriticalSection( this);
    }

    ~CritSection() {
        DeleteCriticalSection( this);
    }

private:
    // disable copy and assignment of CritSection
    CritSection( CritSection const&);
    CritSection& operator=( CritSection const&);
};


class Singleton
{
public:
    static Singleton& instance();

private:
    // don't allow public construct/destruct
    Singleton();
    ~Singleton();
    // disable copy & assignment
    Singleton( Singleton const&);
    Singleton& operator=( Singleton const&);

    static CritSection instance_lock;
};

CritSection Singleton::instance_lock; // definition for Singleton's lock
                                      //  it's initialized before main() is called


Singleton::Singleton()
{
}


Singleton& Singleton::instance()
{
    // check to see if we need to create the Singleton
    EnterCriticalSection( &instance_lock);
    static Singleton s;
    LeaveCriticalSection( &instance_lock);

    return s;
}

Людина - це багато лайна, щоб "зробити кращий глобальний".

Основні недоліки цієї реалізації (якщо я не дозволив пропустити деякі помилки), це:

  • якщо new Singleton()кидки, блокування не буде відпущено. Це можна виправити, використовуючи справжній об'єкт блокування RAII замість простого, який у мене є тут. Це також може допомогти зробити портативні речі, якщо ви використовуєте щось на кшталт Boost, щоб забезпечити незахищену платформу для обгортки.
  • це гарантує безпеку потоку, коли викликається запит на екземпляр Singleton main()- якщо ви викликаєте його до цього часу (як, наприклад, при ініціалізації статичного об'єкта), речі можуть не працювати, оскільки CRITICAL_SECTIONвони не можуть бути ініціалізовані.
  • замок потрібно робити щоразу, коли запит екземпляра. Як я вже говорив, це проста безпечна реалізація потоку. Якщо вам потрібна краща (або ви хочете знати, чому такі речі, як техніка блокування подвійної перевірки, є хибними), дивіться документи, пов'язані з відповіддю Гроо .

1
Ой-ой. Що станеться, якщо new Singleton()кидає?
sbi

@Bob - якщо бути справедливим, з належним набором бібліотек, все суттєве відношення до не копіюваності та правильного блокування RAII відійшло б або було б мінімальним. Але я хотів, щоб приклад був розумно автономним. Незважаючи на те, що для одиноких є велика робота, можливо, мінімальний прибуток, я вважаю їх корисними в управлінні використанням глобальних компаній. Вони, як правило, полегшують з'ясування, де і коли вони використовуються трохи краще, ніж просто угода про іменування.
Майкл Берр

@sbi: у цьому прикладі, якщо new Singleton()кидки, напевно проблема з блокуванням. Слід використовувати належний клас блокування RAII, щось на зразок lock_guardBoost. Я хотів, щоб приклад був більш-менш автономним, і це вже було трохи чудовисько, тому я відмовився від безпеки виключень (але зате назвав це). Можливо, я мушу це виправити, щоб цей код не ставав вирізаним-не-вставленим десь недоречним.
Майкл Берр

Чому динамічно виділяти сингл? Чому б просто не зробити 'pInstance' статичним членом 'Singleton :: instance ()'?
Мартін Йорк

@Martin - готово. Ви маєте рацію, це робить трохи простіше - було б ще краще, якби я використовував клас замка RAII.
Майкл Берр

10

Переглядаючи наступний стандарт (розділ 6.7.4), він пояснює, наскільки статична локальна ініціалізація є безпечною для потоків. Тож як тільки цей розділ стандарту буде широко впроваджений, Сінглтон Меєра стане кращою реалізацією.

Я вже не згоден з багатьма відповідями. Більшість компіляторів вже реалізують статичну ініціалізацію таким чином. Єдиним помітним винятком є ​​Microsoft Visual Studio.


6

Правильна відповідь залежить від вашого компілятора. Він може вирішити зробити його безпечним для ниток; це не "природно" нитка безпечно.


5

Чи безпечна низка наступних потоків [...]?

На більшості платформ це не є безпечним для потоків. (Додайте звичайну відмову від відповідальності, пояснюючи, що стандарт C ++ не знає про потоки, тому, юридично, він не говорить про те, є він чи ні.)

Якщо ні, то чому [...]?

Причина цього не в тому, що ніщо не заважає одночасно виконати sконструктор більш ніж одному потоку .

як зробити цю нитку безпечною?

"C ++ і небезпека подвійного перевірки блокування" Скотта Мейєрса та Андрея Олександреску є досить хорошим трактатом на тему безпечних ниток однотонних.


2

Як сказав MSalters: Це залежить від реалізації C ++, яку ви використовуєте. Перевірте документацію. Щодо іншого питання: "Якщо ні, то чому?" - Стандарт C ++ ще не згадує нічого про потоки. Але майбутня версія C ++ обізнана з потоками, і в ній прямо вказано, що ініціалізація статичних локальних пристроїв є безпечною для потоків. Якщо дві нитки викликають таку функцію, один потік виконає ініціалізацію, а інший заблокує і чекатиме її завершення.

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