Мързелива безопасна за нишка конструкция на сингълтон в C++

Има ли начин за внедряване на единичен обект в C++, който е:

  1. Мързеливо конструиран по безопасен за нишки начин (две нишки могат едновременно да бъдат първият потребител на сингълтона - той все още трябва да бъде конструиран само веднъж).
  2. Не разчита на статични променливи, които са конструирани предварително (така че единичният обект сам по себе си е безопасен за използване по време на конструирането на статични променливи).

(Не познавам достатъчно добре моя C++, но дали интегралните и константните статични променливи се инициализират преди да се изпълни какъвто и да е код (т.е. дори преди да се изпълнят статичните конструктори - техните стойности може вече да са "инициализирани" в програмата) Ако е така - може би това може да се използва за внедряване на сингълтон мютекс - който от своя страна може да се използва за защита на създаването на истински сингълтон..)


Отлично, изглежда, че сега имам няколко добри отговора (жалко, че не мога да отбележа 2 или 3 като отговор). Изглежда, че има две широки решения:

  1. Използване на статична инициализация (за разлика от динамична инициализация) на статична променлива на POD и внедряване на моя собствен mutex с помощта на вградените атомарни инструкции. Това беше типът решение, за което намекнах във въпроса си и вярвам, че вече знаех.
  2. Използвайте някоя друга библиотечна функция като pthread_once или boost::call_once. За тях със сигурност не знаех - и съм много благодарен за публикуваните отговори.

person pauldoo    schedule 09.08.2008    source източник


Отговори (9)


По принцип вие искате синхронизирано създаване на сингълтон, без да използвате никаква синхронизация (предварително конструирани променливи). По принцип не, това не е възможно. Имате нужда от нещо налично за синхронизация.

Що се отнася до другия ви въпрос, да, статичните променливи, които могат да бъдат статично инициализирани (т.е. не е необходим код за изпълнение), са гарантирани, че ще бъдат инициализирани преди да бъде изпълнен друг код. Това прави възможно използването на статично инициализиран мютекс за синхронизиране на създаването на сингълтън.

От ревизията от 2003 г. на стандарта C++:

Обекти със статична продължителност на съхранение (3.7.1) трябва да бъдат инициализирани с нула (8.5), преди да се извърши всяка друга инициализация. Нулевата инициализация и инициализацията с постоянен израз се наричат ​​колективно статична инициализация; всяка друга инициализация е динамична инициализация. Обекти от типове POD (3.9) със статична продължителност на съхранение, инициализирана с константни изрази (5.19), трябва да бъдат инициализирани, преди да се извърши каквато и да е динамична инициализация. Обекти със статична продължителност на съхранение, дефинирани в обхвата на пространството от имена в една и съща транслационна единица и динамично инициализирани, се инициализират в реда, в който тяхната дефиниция се появява в транслационната единица.

Ако знаете, че ще използвате този сингълтън по време на инициализацията на други статични обекти, мисля, че ще откриете, че синхронизирането не е проблем. Доколкото ми е известно, всички основни компилатори инициализират статични обекти в една нишка, така че нишката е безопасна по време на статична инициализация. Можете да декларирате вашия единичен указател като NULL и след това да проверите дали е инициализиран, преди да го използвате.

Това обаче предполага, че знаете, че ще използвате този сингълтън по време на статична инициализация. Това също не е гарантирано от стандарта, така че ако искате да сте напълно безопасни, използвайте статично инициализиран мютекс.

Редактиране: Предложението на Крис за използване на атомно сравнение и размяна със сигурност ще работи. Ако преносимостта не е проблем (и създаването на допълнителни временни сингълтони не е проблем), тогава това е решение с малко по-ниски разходи.

person Derek Park    schedule 09.08.2008

За съжаление, отговорът на Мат включва това, което се нарича заключване с двойна проверка, което не се поддържа от модела на паметта C/C++. (Поддържа се от Java 1.5 и по-нови — и мисля, че .NET — модел на паметта.) Това означава, че между времето, когато се извършва проверката pObj == NULL и когато се придобие заключването (mutex), pObj може вече да е било присвоено на друга нишка. Превключването на нишки се случва винаги, когато операционната система поиска, а не между "редовете" на програма (които нямат значение след компилирането на повечето езици).

Освен това, както Мат признава, той използва int като заключване, а не като примитивна операционна система. недей така Правилните заключвания изискват използването на инструкции за бариера на паметта, потенциално промиване на кеш линия и т.н.; използвайте примитивите на вашата операционна система за заключване. Това е особено важно, защото използваните примитиви могат да се променят между отделните линии на процесора, на които работи вашата операционна система; това, което работи на CPU Foo, може да не работи на CPU Foo2. Повечето операционни системи или първоначално поддържат POSIX нишки (pthreads) или ги предлагат като обвивка за пакета за нишки на OS, така че често е най-добре да илюстрирате примери, като ги използвате.

Ако вашата операционна система предлага подходящи примитиви и ако абсолютно се нуждаете от нея за производителност, вместо да извършвате този тип заключване/инициализация, можете да използвате операция атомно сравнение и размяна за инициализиране на споделена глобална променлива. По същество това, което пишете, ще изглежда така:

MySingleton *MySingleton::GetSingleton() {
    if (pObj == NULL) {
        // create a temporary instance of the singleton
        MySingleton *temp = new MySingleton();
        if (OSAtomicCompareAndSwapPtrBarrier(NULL, temp, &pObj) == false) {
            // if the swap didn't take place, delete the temporary instance
            delete temp;
        }
    }

    return pObj;
}

Това работи само ако е безопасно да създадете няколко екземпляра на вашия сингълтон (по един на нишка, която се случва да извика GetSingleton() едновременно) и след това да изхвърлите екстрите. Функцията OSAtomicCompareAndSwapPtrBarrier, осигурена в Mac OS X — повечето операционни системи предоставят подобен примитив — проверява дали pObj е NULL и всъщност го настройва на temp само ако е така. Това използва хардуерна поддръжка, за да извърши наистина, буквално само размяната веднъж и да каже дали се е случило.

Друго средство, което можете да използвате, ако вашата операционна система го предлага, което е между тези две крайности, е pthread_once. Това ви позволява да настроите функция, която се изпълнява само веднъж - основно чрез извършване на всички операции по заключване/бариера/и т.н. трикове за вас - без значение колко пъти е извикан или на колко нишки е извикан.

person Chris Hanson    schedule 09.08.2008

Ето един много прост лениво конструиран единичен гетер:

Singleton *Singleton::self() {
    static Singleton instance;
    return &instance;
}

Това е мързеливо и следващият C++ стандарт (C++0x) изисква да бъде безопасен за нишки. Всъщност вярвам, че поне g++ прилага това по безопасен начин. Така че, ако това е целевият ви компилатор или ако използвате компилатор, който също прилага това по безопасен за нишки начин (може би по-новите компилатори на Visual Studio го правят? Не знам), тогава това може да е всичко, от което се нуждаете .

Вижте също http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2008/n2513.html по тази тема.

person Frerich Raabe    schedule 19.05.2010
comment
хубаво! Това ще бъде много по-спретнато от сегашното ни решение. Кога C++0x (или трябва да бъде C++1x) най-накрая ще бъде завършен..? - person pauldoo; 20.05.2010
comment
VS2015 въвежда поддръжка за безопасна нишка за този модел за инициализация. - person Chris Betti; 22.11.2015

Не можете да го направите без никакви статични променливи, но ако желаете да толерирате такава, можете да използвате Boost.Thread за тази цел. Прочетете раздела „еднократна инициализация“ за повече информация.

След това във вашата сингълтън функция за достъп използвайте boost::call_once, за да конструирате обекта и да го върнете.

person Chris Jester-Young    schedule 10.08.2008
comment
Само мое мнение, но мисля, че трябва да внимавате с Boost. Не съм убеден, че неговата нишка е безопасна, въпреки че има много подпроекти, свързани с нишки. (Това е след извършване на два одита с интервал от няколко години и гледане на докладите за грешки, затворени, тъй като няма да се коригират). - person jww; 09.01.2014

За gcc това е доста лесно:

LazyType* GetMyLazyGlobal() {
    static const LazyType* instance = new LazyType();
    return instance;
}

GCC ще се увери, че инициализацията е атомарна. За VC++ това не е така. :-(

Един основен проблем с този механизъм е липсата на възможност за тестване: ако трябва да нулирате LazyType на нов между тестовете или искате да промените LazyType* на MockLazyType*, няма да можете. Като се има предвид това, обикновено е най-добре да използвате статичен мютекс + статичен указател.

Също така, вероятно настрана: Най-добре е винаги да избягвате статични типове, които не са POD. (Указателите към POD са ОК.) Причините за това са много: както споменахте, редът на инициализация не е дефиниран -- нито пък редът, в който се извикват деструкторите. Поради това програмите ще се сринат, когато се опитат да излязат; често не е голяма работа, но понякога showstopper, когато профилиращият, който се опитвате да използвате, изисква чист изход.

person 0124816    schedule 16.09.2008
comment
Вие сте напълно прав за това. Но по-добре, ако удебелите За VC++ това не е правилната фраза. blogs.msdn.com/oldnewthing/archive/2004/03/ 08/85901.aspx - person Varuna; 18.12.2009
comment

Въпреки че на този въпрос вече е отговорено, мисля, че има някои други точки, които трябва да се споменат:

  • Ако искате lazy-instanciation на singleton, докато използвате указател към динамично разпределен екземпляр, ще трябва да се уверите, че сте го почистили в правилната точка.
  • Бихте могли да използвате решението на Matt, но ще трябва да използвате правилен mutex/критичен раздел за заключване и като поставите отметка на „pObj == NULL“ преди и след заключването. Разбира се, pObj също трябва да бъде статичен ;) . Мутексът би бил ненужно тежък в този случай, по-добре е да изберете критична секция.

Но както вече беше посочено, не можете да гарантирате безопасна за нишки мързелива инициализация, без да използвате поне един примитив за синхронизиране.

Редактиране: Да, Дерек, прав си. Моя грешка. :)

person OJ.    schedule 10.08.2008

Бихте могли да използвате решението на Matt, но ще трябва да използвате правилен mutex/критичен раздел за заключване и като поставите отметка на „pObj == NULL“ преди и след заключването. Разбира се, pObj също трябва да е статичен ;) . Мутексът би бил ненужно тежък в този случай, по-добре е да изберете критична секция.

OJ, това не работи. Както посочи Крис, това е заключване с двойна проверка, което не е гарантирано да работи в текущия C++ стандарт. Вижте: C++ и опасностите от заключването с двойна проверка

Редактиране: Няма проблем, OJ. Наистина е хубаво на езици, където работи. Очаквам, че ще работи в C++0x (въпреки че не съм сигурен), защото е толкова удобен идиом.

person Derek Park    schedule 10.08.2008

  1. прочетете на модел със слаба памет. Може да разбие двойно проверени брави и спинлокове. Intel е силен модел памет (все още), така че на Intel е по-лесно

  2. внимателно използвайте "volatile", за да избегнете кеширането на части от обекта в регистрите, в противен случай ще сте инициализирали указателя на обекта, но не и самия обект, и другата нишка ще се срине

  3. редът на инициализацията на статичните променливи спрямо зареждането на споделен код понякога не е тривиален. Виждал съм случаи, когато кодът за унищожаване на обект вече е разтоварен, така че програмата се срива при излизане

  4. такива обекти са трудни за правилно унищожаване

Като цяло сингълтоните са трудни за правилно изпълнение и трудни за отстраняване на грешки. По-добре е да ги избягвате напълно.

person n-alexander    schedule 09.11.2009

Предполагам, че казването не правете това, защото не е безопасно и вероятно ще се повреди по-често, отколкото просто инициализирането на тези неща в main(), няма да бъде толкова популярно.

(И да, знам, че предлагането на това означава, че не трябва да се опитвате да правите интересни неща в конструкторите на глобални обекти. Това е смисълът.)

person Mat Noguchi    schedule 20.08.2008