ефективний потокобезпечний синглтон в C ++


76

Звичайний шаблон для синглтон-класу - це щось на зразок

static Foo &getInst()
{
  static Foo *inst = NULL;
  if(inst == NULL)
    inst = new Foo(...);
  return *inst;    
}

Однак я розумію, що це рішення не є безпечним для потоку, оскільки 1) конструктор Foo може бути викликаний більше одного разу (що може мати значення, а може і не мати) і 2) inst може бути не повністю побудований до того, як його повернуть в інший потік .

Одне з рішень - обернути мутекс навколо всього методу, але тоді я плачу за накладні витрати на синхронізацію ще довго після того, як мені це насправді потрібно. Альтернатива - це щось на зразок

static Foo &getInst()
{
  static Foo *inst = NULL;
  if(inst == NULL)
  {
    pthread_mutex_lock(&mutex);
    if(inst == NULL)
      inst = new Foo(...);
    pthread_mutex_unlock(&mutex);
  }
  return *inst;    
}

Це правильний спосіб зробити це, чи є якісь підводні камені, про які я повинен знати? Наприклад, чи можуть виникати проблеми із статичним порядком ініціалізації, тобто чи завжди гарантовано inst має значення NULL при першому виклику getInst?


6
Але у вас немає часу, щоб знайти приклад і провести тільне голосування? Я зараз свіжий.
bmargulies

1
можливо дублікат stackoverflow.com/questions/6915 / ...
kennytm

3
@bmargulies Ні, допитувача, очевидно, не можна було турбувати, то чому я повинен? Я вирішив відмовитись від голосування та закриття як дурнів, оскільки, здається, я один з небагатьох, хто турбується про те, щоб не пускати дерьмо з SO. А чи знаєте ви, лінь почувається добре!

Мені знадобився час, щоб ретельно описати свою проблему за допомогою фрагментів та обговорення того, що я знав / пробував. Мені шкода, що я витратив ваш час на "лайно". :(
user168715 04

1
@sbi: так само, як і я. Розсипання відповідей на тисячі запитань - найкращий спосіб ускладнити їх подальший пошук.
Matthieu M.

Відповіді:


44

Ваше рішення називається "подвійно перевірене блокування", і те, як ви його написали, не є безпечним.

Ця стаття Мейєрса / Александреску пояснює, чому - але ця папір також широко сприймається неправильно. Він започаткував мем "подвійне перевірене блокування небезпечно в C ++", але його фактичний висновок полягає в тому, що подвійне перевірене блокування в C ++ може бути реалізовано безпечно, для цього просто потрібно використовувати бар'єри пам'яті в неочевидному місці.

Стаття містить псевдокод, який демонструє, як використовувати бар’єри пам’яті для безпечної реалізації DLCP, тому вам не повинно бути важко виправити реалізацію.


if (inst == NULL) {temp = new Foo (...); inst = temp;} Чи не гарантує це, що конструктор закінчив роботу перед призначенням inst? Я розумію, що це можна (і, мабуть, буде) оптимізовано, але логічно це вирішує проблему, ні?
stu

1
Це не допомагає, оскільки сумісний компілятор може вільно змінювати порядок етапів призначення та побудови, як вважає за потрібне.
JoeG

Я уважно прочитав статтю, і здається, що рекомендація просто уникати DLCP з Singleton. Вам доведеться нестабільно виганяти клас і додавати бар’єри пам’яті (чи це також не вплине на ефективність?). Для практичних потреб використовуйте простий, єдиний замок та кешуйте об’єкт, який ви отримуєте з "GetInstance".
гаярад

також просто прочитав статтю, і я зрозумів головний висновок: DLCP може бути реалізований безпечно для потоку з використанням бар'єрів пам'яті, але не портативно (до c ++ 11)
greatest_prime_is_463035818

101

Якщо ви використовуєте C ++ 11, ось правильний спосіб зробити це:

Foo& getInst()
{
    static Foo inst(...);
    return inst;
}

Згідно з новим стандартом, більше не потрібно турбуватися про цю проблему. Ініціалізація об’єкта здійснюватиметься лише одним потоком, інші потоки будуть чекати, поки він завершиться. Або ви можете використовувати std :: call_once. (більше інформації тут )


2
Це рішення C ++ 11, яке я очікував би від людей.
Олександр Ох

8
На жаль, це не є безпечним для потоків у VS2013, див. "Magic Statics
Кріс Дрю,


8
Щоб уникнути плутанини, можливо, ви можете або додати статику до оголошення функції, або прямо вказати, що це функція, що не є членом.
MikeMB

Чи є виклики для цього екземпляра з різних потоків безпечними для потоків, або функції екземпляра класу повинні дбати про атомність самі по собі?
Шадасвіар,

12

Herb Sutter розповідає про подвійну перевірку блокування в CppCon 2014.

Нижче наведено код, який я реалізував у C ++ 11 на основі цього:

class Foo {
public:
    static Foo* Instance();
private:
    Foo() {}
    static atomic<Foo*> pinstance;
    static mutex m_;
};

atomic<Foo*> Foo::pinstance { nullptr };
std::mutex Foo::m_;

Foo* Foo::Instance() {
  if(pinstance == nullptr) {
    lock_guard<mutex> lock(m_);
    if(pinstance == nullptr) {
        pinstance = new Foo();
    }
  }
  return pinstance;
}

Ви також можете перевірити повну програму тут: http://ideone.com/olvK13


1
@Etherealone, що ти пропонуєш?
qqibrow

4
Простий static Foo foo;і return &foo;всередині функції примірника буде досить; staticініціалізація безпечна для потоку в C ++ 11. Віддайте перевагу посиланням на покажчики.
Etherealone

Я отримую повідомлення про помилку в MSVC 2015: Код серйозності Опис Проектний рядок файлу Рядок джерела придушення Помилка (активний) більше ніж один оператор "==" відповідає цим операндам:
user2286810

@qqibrow може бути, ви також можете зробити конструктор копіювання, конструктор переміщення, оператор присвоєння, перемістити оператор присвоєння як приватний.
Mayur

9

Використання pthread_once, яке гарантує, що функція ініціалізації запускається один раз атомно.

(У Mac OS X він використовує блокування віджимання. Не знаю реалізації інших платформ.)


3

TTBOMK, єдиним гарантованим потокобезпечним способом зробити це без блокування буде ініціалізація всіх ваших одиночних ліній перед тим, як ви коли-небудь почнете потік.



0

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

Ви можете знайти вихідний код тут .


0

Тут працює TLS? https://en.wikipedia.org/wiki/Thread-local_storage#C_and_C++

Наприклад,

static _thread Foo *inst = NULL;
static Foo &getInst()
{
  if(inst == NULL)
    inst = new Foo(...);
  return *inst;    
 }

Але нам також потрібен спосіб його явного видалення, наприклад

static void deleteInst() {
   if (!inst) {
     return;
   }
   delete inst;
   inst = NULL;
}

-2

Рішення не є безпечним для потоку, оскільки оператор

inst = new Foo();

може бути розбитий на два твердження компілятором:

Заява1: inst = malloc (sizeof (Foo));
Заява2: inst-> Foo ();

Припустимо, що після виконання оператора 1 одним потоком відбувається перемикання контексту. І 2-й потік також виконує getInstance()метод. Тоді другий потік виявить, що покажчик 'inst' не є нульовим. Отже, другий потік поверне вказівник на неініціалізований об’єкт, оскільки конструктор ще не викликаний 1-м потоком.


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