Apakah implementasi Meyers dari utas pola Singleton aman?

145

Apakah implementasi berikut, menggunakan inisialisasi lazy, dari Singletonutas (Meyers 'Singleton) aman?

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

Jika tidak, mengapa dan bagaimana membuatnya aman?

Ankur
sumber
Dapatkah seseorang tolong jelaskan mengapa ini bukan thread yang aman. Artikel yang disebutkan dalam tautan membahas keamanan utas menggunakan implementasi alternatif (menggunakan variabel penunjuk yaitu statis Singleton * pInstance).
Ankur

Jawaban:

168

Di C ++ 11 , ini aman untuk thread. Menurut standar , §6.7 [stmt.dcl] p4:

Jika kontrol memasuki deklarasi bersamaan saat variabel diinisialisasi, eksekusi bersamaan harus menunggu selesainya inisialisasi.

Dukungan GCC dan VS untuk fitur tersebut ( Inisialisasi dan Penghancuran Dinamis dengan Konkurensi , juga dikenal sebagai Magic Statics on MSDN ) adalah sebagai berikut:

Terima kasih kepada @Mankarse dan @olen_gam atas komentar mereka.


Di C ++ 03 , kode ini tidak aman untuk thread. Ada sebuah artikel oleh Meyers yang disebut "C ++ dan Perils of Double-Checked Locking" yang membahas implementasi aman dari pola, dan kesimpulannya adalah, lebih atau kurang, bahwa (dalam C ++ 03) mengunci penuh di sekitar metode instantiating pada dasarnya adalah cara paling sederhana untuk memastikan konkurensi yang tepat pada semua platform, sementara sebagian besar bentuk varian pola penguncian ganda mungkin menderita dari kondisi balapan pada arsitektur tertentu , kecuali instruksi yang disatukan dengan hambatan memori tempat yang strategis.

Groo
sumber
3
Ada juga diskusi luas tentang Pola Singleton (seumur hidup dan keamanan benang) oleh Alexandrescu dalam Desain C ++ Modern. Lihat situs Loki: loki-lib.sourceforge.net/index.php?n=Pattern.Singleton
Matthieu M.
1
Anda dapat membuat singleton aman-thread dengan boost :: call_once.
CashCow
1
Sayangnya, ini bagian dari standar tidak diimplementasikan dalam Visual Studio 2012 C ++ Compiler. Disebut sebagai "Magic Statics" dalam tabel "C ++ 11 Fitur Bahasa Inti: Concurrency" di sini: msdn.microsoft.com/en-us/library/vstudio/hh567368.aspx
olen_garn
Cuplikan dari standar membahas konstruksi tetapi bukan penghancuran. Apakah standar mencegah objek dari kerusakan pada satu utas sementara (atau sebelum) utas lain mencoba mengaksesnya pada penghentian program?
stewbasic
IANA (bahasa C ++) L tetapi bagian 3.6.3 [basic.start.term] p2 menunjukkan bahwa mungkin untuk mengenai perilaku tidak terdefinisi dengan mencoba mengakses objek setelah dihancurkan?
stewbasic
21

Untuk menjawab pertanyaan Anda tentang mengapa ini bukan threadsafe, itu bukan karena panggilan pertama instance()harus memanggil konstruktor Singleton s. Agar aman threads ini harus terjadi di bagian kritis, dan tapi tidak ada persyaratan dalam standar bahwa bagian kritis diambil (standar sampai saat ini benar-benar diam di utas). Compiler sering menerapkan ini menggunakan pemeriksaan sederhana dan penambahan boolean statis - tetapi tidak di bagian kritis. Sesuatu seperti kodesemu berikut:

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

Jadi, inilah Singleton aman-utas sederhana (untuk Windows). Ia menggunakan pembungkus kelas sederhana untuk objek Windows CRITICAL_SECTION sehingga kita dapat membuat kompiler secara otomatis menginisialisasi CRITICAL_SECTIONsebelum main()dipanggil. Idealnya kelas bagian kritis RAII yang benar akan digunakan yang dapat menangani pengecualian yang mungkin terjadi ketika bagian kritis diadakan, tapi itu di luar cakupan jawaban ini.

Operasi mendasar adalah bahwa ketika sebuah instance Singletondiminta, kunci diambil, Singleton dibuat jika perlu, maka kunci dilepaskan dan referensi Singleton dikembalikan.

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

Man - itu banyak omong kosong untuk "membuat global yang lebih baik".

Kelemahan utama implementasi ini (jika saya tidak membiarkan beberapa bug masuk) adalah:

  • jika new Singleton()melempar, kunci tidak akan dilepaskan. Ini dapat diperbaiki dengan menggunakan objek kunci RAII yang benar dan bukan yang sederhana yang saya miliki di sini. Ini juga dapat membantu membuat hal-hal portabel jika Anda menggunakan sesuatu seperti Boost untuk menyediakan pembungkus independen platform untuk kunci.
  • ini menjamin keamanan utas ketika instance Singleton diminta setelah main()dipanggil - jika Anda menyebutnya sebelum itu (seperti dalam inisialisasi objek statis) hal-hal mungkin tidak berfungsi karena CRITICAL_SECTIONmungkin tidak diinisialisasi.
  • kunci harus diambil setiap kali mesin virtual diminta. Seperti yang saya katakan, ini adalah implementasi aman thread sederhana. Jika Anda membutuhkan yang lebih baik (atau ingin tahu mengapa hal-hal seperti teknik kunci periksa ganda salah), lihat makalah yang terkait dengan jawaban Groo .
Michael Burr
sumber
1
Uh oh. Apa yang terjadi jika new Singleton()melempar?
sbi
@ Bob - agar adil, dengan set perpustakaan yang tepat, semua kesalahan yang harus dilakukan dengan non-copyability dan kunci RAII yang tepat akan hilang atau minimal. Tapi saya ingin contohnya cukup mandiri. Meskipun singleton banyak pekerjaan untuk keuntungan minimal, saya telah menemukan mereka berguna dalam mengelola penggunaan global. Mereka cenderung membuatnya lebih mudah untuk menemukan di mana dan kapan mereka digunakan sedikit lebih baik daripada hanya konvensi penamaan.
Michael Burr
@ SB: dalam contoh ini, jika new Singleton()melempar pasti ada masalah dengan kunci. Kelas kunci RAII yang tepat harus digunakan, seperti lock_guarddari Boost. Saya ingin contohnya kurang lebih mandiri, dan itu sudah sedikit monster jadi saya tinggalkan pengecualian keamanan (tapi memanggilnya keluar). Mungkin saya harus memperbaikinya sehingga kode ini tidak mendapatkan cut-n-paste di suatu tempat yang tidak pantas.
Michael Burr
Mengapa mengalokasikan secara singleton secara dinamis? Mengapa tidak menjadikan 'pInstance' sebagai anggota statis 'Singleton :: instance ()'?
Martin York
@ Martin - selesai. Anda benar, itu membuatnya sedikit lebih sederhana - akan lebih baik jika saya menggunakan kelas kunci RAII.
Michael Burr
10

Melihat standar berikutnya (bagian 6.7.4), ini menjelaskan bagaimana inisialisasi lokal statis aman dari benang. Jadi begitu bagian standar itu diterapkan secara luas, Singleton Meyer akan menjadi implementasi yang disukai.

Saya sudah tidak setuju dengan banyak jawaban. Sebagian besar kompiler sudah menerapkan inisialisasi statis dengan cara ini. Satu-satunya pengecualian adalah Microsoft Visual Studio.

deft_code
sumber
6

Jawaban yang benar tergantung pada kompiler Anda. Itu dapat memutuskan untuk membuatnya threadsafe; itu bukan threads "secara alami" aman.

MSalters
sumber
5

Apakah thread implementasi [...] berikut ini aman?

Pada kebanyakan platform, ini bukan thread-safe. (Tambahkan penafian biasa yang menjelaskan bahwa standar C ++ tidak tahu tentang utas, jadi, secara hukum, ia tidak mengatakan apakah itu benar atau tidak.)

Jika tidak, mengapa [...]?

Alasannya bukan karena tidak ada yang mencegah lebih dari satu utas menjalankan skonstruktor secara bersamaan .

bagaimana cara membuatnya aman?

"C ++ dan Perils of Double-Checked Locking" oleh Scott Meyers dan Andrei Alexandrescu adalah risalah yang cukup bagus tentang masalah lajang yang aman.

sbi
sumber
2

Seperti yang dikatakan MSalters: Itu tergantung pada implementasi C ++ yang Anda gunakan. Periksa dokumentasinya. Adapun pertanyaan lainnya: "Jika tidak, mengapa?" - Standar C ++ belum menyebutkan apapun tentang utas. Tetapi versi C ++ yang akan datang sadar akan utas dan secara eksplisit menyatakan bahwa inisialisasi lokal statis adalah utas yang aman. Jika dua utas memanggil fungsi seperti itu, satu utas akan melakukan inisialisasi sementara yang lain akan memblokir & menunggu sampai utas selesai.

sellibitze
sumber