Thread construcție în condiții de siguranță leneș de un singleton în C ++

Există o modalitate de a implementa un obiect singleton în C ++ care este:

  1. Lazile sunt construite într-o manieră sigură (două fire pot fi simultan primul utilizator al singleton-ului, acesta ar trebui să fie construit doar o singură dată).
  2. Nu se bazează în prealabil pe construirea de variabile statice (astfel încât obiectul singleton să poată fi folosit în timpul construirii variabilelor statice)

(Nu-mi cunosc destul de bine C ++, dar este cazul ca variabilele statice integrale și constante să fie inițializate înainte ca orice cod să fie executat (adică chiar înainte de executarea constructorilor statici - valorile lor ar putea fi deja "inițializate" în program imaginea)? Dacă da - poate că acest lucru poate fi exploatat pentru a implementa un mutex singleton - care, la rândul său, poate fi folosit pentru a păstra crearea adevăratului singleton ..)


Excelent, se pare că acum am câteva răspunsuri bune (rușinea că nu pot marca 2 sau 3 ca fiind răspunsul ). Se pare că există două soluții ample:

  1. Utilizarea initialisation statice (spre deosebire de initialisation dinamică) a unei variabile statice POD, și punerea în aplicare propria mea mutex cu faptul că, folosind instrucțiunile atomice predefinite. Acesta a fost tipul de soluție am fost în aluzie la întrebarea mea, și cred că am știut deja.
  2. Utilizați o altă funcție de bibliotecă cum ar fi pthread_once sau stimula :: call_once . Acestea Eu cu siguranță nu am știut despre - și sunt foarte recunoscător pentru răspunsurile postate
  3. .
0
fr hi bn

9 răspunsuri

Nu puteți să o faceți fără vreo variabilă statică, totuși dacă sunteți dispus să tolerați unul, puteți utiliza Boost.Thread pentru acest scop. Citiți secțiunea "inițializare unică" pentru mai multe informații.

Apoi, în funcția de accesoriu singleton, folosiți boost :: call_once pentru a construi obiectul și pentru al returna.

0
adăugat
Doar opinia mea, dar cred că trebuie să ai grijă cu Boost. Nu sunt convins că thread-ul său este în siguranță, chiar dacă are multe subproiecte înrudite. (Aceasta este după efectuarea a două audituri la câțiva ani și urmărirea rapoartelor de erori închise ca "nu se va rezolva").
adăugat autor jww, sursa

În principiu, cereți crearea unei sincronizări a unui singleton, fără a utiliza nici o sincronizare (variabile construite anterior). În general, nu, acest lucru nu este posibil. Ai nevoie de ceva disponibil pentru sincronizare.

În ceea ce privește cealaltă întrebare, da, pot fi inițializate variabile statice care pot fi inițializate static (adică nu este necesar un cod de execuție) înainte de a fi executat alt cod. Acest lucru face posibilă utilizarea unui mutex inițializat static pentru sincronizarea creării unui singleton.

Din revizuirea din 2003 a standardului C ++:

Obiectele cu o durată de stocare statică (3.7.1) trebuie inițializate zero (8.5) înainte de orice altă inițializare. Inițializarea zero și inițierea cu o expresie constantă sunt denumite colectiv inițierea statică; toate celelalte inițializări sunt inițiale dinamice. Obiectele tipurilor POD (3.9) cu durată de stocare statică inițializate cu expresii constante (5.19) trebuie inițializate înainte de a avea loc orice inițializare dinamică. Obiectele cu o durată de stocare statică definită în domeniul spațiului de nume în aceeași unitate de traducere și inițializate dinamic trebuie inițializate în ordinea în care apare definiția lor în unitatea de traducere.

Dacă știți că veți folosi acest singleton în timpul inițializării altor obiecte statice, cred că veți găsi că sincronizarea nu este o problemă. Din câte știu, toate compilatoarele majore inițializează obiecte statice într-un fir unic, deci siguranța firului în timpul inițializării statice. Puteți să vă declarați câmpul singleton ca fiind NULL și apoi verificați dacă a fost inițializat înainte de al utiliza.

Cu toate acestea, aceasta presupune că știți că veți folosi acest singleton în timpul inițializării statice. Acest lucru nu este, de asemenea, garantat de standard, deci dacă doriți să fiți în siguranță, utilizați un mutex inițializat static.

Editați: sugestia lui Chris de a utiliza o comparație-și-swap atomică ar funcționa cu siguranță. În cazul în care portabilitatea nu este o problemă (și crearea de singletonuri temporare suplimentare nu este o problemă), atunci este vorba de o soluție puțin mai scăzută a aeriene.

0
adăugat

Ați putea folosi soluția lui Matt, dar va trebui să utilizați o secțiune critică pentru blocare și verificând "pObj == NULL" atât înainte cât și după blocare. Desigur, pObj ar trebui să fie și static;). Un mutex ar fi inutil de greu în acest caz, ar fi mai bine să mergeți cu o secțiune critică.

OJ, that doesn't work. As Chris pointed out, that's double-check locking, which is not guaranteed to work in the current C++ standard. See: C++ and the Perils of Double-Checked Locking

Editați: Nicio problemă, OJ. Este foarte frumos în limbile în care funcționează. Mă aștept să funcționeze în C ++ 0x (deși nu sunt sigur), pentru că este un astfel de idiom convenabil.

0
adăugat

Iată un getter foarte simplu construit singur:

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

Acest lucru este leneș, iar următorul standard C ++ (C ++ 0x) necesită ca acesta să fie în siguranță. De fapt, cred că cel puțin g ++ implementează acest lucru într-un mod sigur. Deci, dacă acesta este compilatorul dvs. țintă sau dacă folosiți un compilator care implementează acest lucru într-un mod sigur (pot fi mai noi compilatoare Visual Studio nu știu), atunci acest lucru ar putea fi tot ce aveți nevoie .

De asemenea, consultați http: //www.open-std .org / jtc1 / sc22 / wg21 / docs / papers / 2008 / n2513.html pe acest subiect.

0
adăugat
Frumos! Acesta va fi mult mai curat decât soluția noastră actuală. Când va fi C ++ 0x (sau ar trebui să fie C ++ 1x) în cele din urmă să fie terminat ..?
adăugat autor pauldoo, sursa
VS2015 introduce suportul sigur pentru fir pentru acest model de inițializare.
adăugat autor Chris Betti, sursa

În timp ce această întrebare a fost deja răspunsă, cred că există și alte câteva puncte pe care trebuie să le menționăm:

  • Dacă doriți o instanță lentă a singleton-ului în timp ce utilizați un pointer într-o instanță alocată dinamic, va trebui să vă asigurați că ați curățat-o la punctul corect.
  • S-ar putea să utilizați soluția lui Matt, dar va trebui să utilizați o secțiune de mutex / critică adecvată pentru blocare și verificând "pObj == NULL" atât înainte cât și după blocare. Desigur, pObj ar trebui, de asemenea, să fie static ;) . Un mutex ar fi inutil greu în acest caz, ar fi mai bine să mergi cu o secțiune critică.

Dar, așa cum am spus deja, nu puteți garanta inițializarea leneșilor fără a utiliza cel puțin un primitiv de sincronizare.

Editați: Yup Derek, aveți dreptate. Greșeala mea. :)

0
adăugat

Din păcate, răspunsul lui Matt conține ceea ce se numește blocare dublă verificată care nu este acceptată de modelul de memorie C / C ++. (Aceasta este susținută de Java 1.5 și mai târziu și cred că .NET model de memorie.) Aceasta înseamnă că între momentul în care verificarea pObj == NULL are loc și când blocarea (mutex) este dobândită, pObj poate să fi fost deja atribuită unui alt fir. Comutarea între ele se face ori de câte ori OS-ul dorește, nu între "liniile" unui program (care nu au semnificație post-compilație în majoritatea limbilor).

În plus, după cum recunoaște Matt, el folosește un int ca un element de blocare mai degrabă decât un primitiv sistem de operare. Nu face asta. Încuietori adecvate necesită utilizarea instrucțiunilor de barieră de memorie, potențiale șocuri de linie cache și așa mai departe; utilizați primitivele sistemului de operare pentru blocare. Acest lucru este deosebit de important deoarece primitivele utilizate pot schimba între liniile CPU individuale pe care rulează sistemul de operare; ce funcționează pe un procesor Foo ar putea să nu funcționeze pe CPU Foo2. Majoritatea sistemelor de operare suportă fire native POSIX (pthreads) sau le oferă ca ambalaj pentru pachetul de filetare OS, deci este adesea mai bine să ilustrați exemplele care le utilizează.

Dacă sistemul dvs. de operare oferă primitive adecvate și dacă aveți absolut nevoie de performanță, în loc să efectuați acest tip de blocare / inițializare, puteți utiliza o operație comparare atomică și swap pentru a inițializa o variabilă globală comună. În esență, ceea ce scrieți va arăta astfel:

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;
}

Acest lucru funcționează numai dacă este sigur să creați mai multe instanțe ale singleton-ului dvs. (una pentru fiecare fir care se întâmplă să invocați simultan GetSingleton ()) și apoi aruncați extras. Funcția OSAtomicCompareAndSwapPtrBarrier furnizată pe Mac OS X? majoritatea sistemelor de operare oferă un primitiv similar? verifică dacă pObj este NULL și o va seta numai la temp dacă este. Acest lucru utilizează suportul hardware pentru a într-adevăr, literalmente efectua doar swap o dată și spune dacă sa întâmplat.

O altă posibilitate de a obține un efect de levier în cazul în care sistemul dvs. de operare oferă acest lucru între aceste două extreme este pthread_once . Aceasta vă permite să configurați o funcție care se execută o singură dată - practic făcând toate blocările / bariera / etc. trickery pentru tine - indiferent de câte ori este invocat sau de câte fire sunt invocate.

0
adăugat

Pentru gcc, acest lucru este destul de ușor:

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

CCG va asigura că inițializarea este atomică. Pentru VC ++, acest lucru nu este cazul . :-(

O problemă majoră cu acest mecanism este lipsa de testabilitate: dacă trebuie să resetați LazyType la una nouă între teste sau doriți să schimbați LazyType * la un MockLazyType *, nu veți putea. Având în vedere acest lucru, este de obicei cel mai bine să utilizați un indicator static mutex + static.

De asemenea, eventual, la o parte: Cel mai bine este să evitați întotdeauna tipurile non-POD statice. (Indicatorii pentru POD sunt în regulă.) Motivele pentru acest lucru sunt multe: după cum spui, ordinea inițializării nu este definită - nici ordinea în care sunt numiți distrugătorii nu este definită. Din acest motiv, programele se vor sfarși atunci când încearcă să iasă; de multe ori nu este o afacere mare, dar uneori un showstopper atunci când profiler pe care încercați să utilizați necesită o ieșire curată.

0
adăugat
adăugat autor jww, sursa
Ai dreptate pe asta. Dar mai bine dacă ai îndrăznea expresia "Pentru VC ++ asta nu este fiica". blogs.msdn.com/oldnewthing/archive/2004/03/ 08 / 85901.aspx
adăugat autor Varuna, sursa

Presupun că nu spun că nu faceți acest lucru pentru că nu este sigur și probabil că va rupe mai des decât inițierea acestor lucruri în main() nu va fi atât de popular.

(Și da, știu că asta înseamnă că nu ar trebui să încerci să faci lucruri interesante în constructorii de obiecte globale.

0
adăugat
  1. citiți pe modelul de memorie slab. Poate rupe încuietorile și spinlock-urile verificate. Intel este un model puternic de memorie (încă), deci Intel este mai ușor

  2. Folosiți-vă cu atenție "volatile" pentru a evita cachearea părților obiectului din registre, altfel veți fi inițializat cursorul obiectului, dar nu și obiectul însuși, iar celălalt fir va cade

  3. ordinea inițializării variabilelor statice față de încărcarea codului partajat nu este uneori trivială. Am văzut cazuri când codul pentru a distruge un obiect a fost deja descărcat, astfel că programul sa prăbușit la ieșire

  4. astfel de obiecte sunt greu de distrus în mod corespunzător

În general, single-urile sunt greu de făcut bine și greu de depanat. Este mai bine să le eviți cu totul.

0
adăugat