Apakah utas konstruktor statis C # aman?


247

Dengan kata lain, apakah thread implementasi Singleton ini aman:

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

    static Singleton()
    {
        instance = new Singleton();
    }

    public static Singleton Instance
    {
        get { return instance; }
    }
}

1
Itu aman dari utas. Misalkan beberapa utas ingin mendapatkan properti Instancesekaligus. Salah satu utas akan diperintahkan untuk pertama kali menjalankan penginisialisasi tipe (juga dikenal sebagai konstruktor statis). Sementara itu semua utas lainnya yang ingin membaca Instanceproperti, akan dikunci hingga jenis inisialisasi selesai. Hanya setelah inisialisasi bidang selesai, utas akan diizinkan untuk mendapatkan Instancenilai. Jadi tidak ada yang bisa melihat Instancekeberadaan null.
Jeppe Stig Nielsen

@JeppeStigNielsen Utas lainnya tidak dikunci. Dari pengalaman saya sendiri, saya mendapat kesalahan buruk karena itu. Jaminannya adalah hanya thread pertama yang akan memulai initializer statis, atau konstruktor, tetapi kemudian thread lain akan mencoba menggunakan metode statis, bahkan jika proses konstruksi tidak selesai.
Narvalex

2
@Narvalex Program sampel ini (sumber yang disandikan dalam URL) tidak dapat mereproduksi masalah yang Anda jelaskan. Mungkin itu tergantung pada versi CLR yang Anda miliki?
Jeppe Stig Nielsen

@JeppeStigNielsen Terima kasih telah meluangkan waktu Anda. Bisakah Anda jelaskan kepada saya mengapa di sini bidang ini ditimpa?
Narvalex

5
@ Varvalex Dengan kode itu, huruf besar Xakhirnya menjadi -1 bahkan tanpa threading . Ini bukan masalah keamanan thread. Alih-alih, penginisialisasi x = -1berjalan pertama (pada baris sebelumnya dalam kode, nomor baris lebih rendah). Kemudian inisialisasi X = GetX()dijalankan, yang membuat huruf besar Xsama dengan -1. Dan kemudian konstruktor statis "eksplisit", tipe initializer static C() { ... }berjalan, yang hanya mengubah huruf kecil x. Jadi setelah semua itu, Mainmetode (atau Othermetode) dapat melanjutkan dan membaca huruf besar X. Nilainya akan -1, bahkan hanya dengan satu utas.
Jeppe Stig Nielsen

Jawaban:


189

Konstruktor statis dijamin akan dijalankan hanya sekali per domain aplikasi, sebelum instance kelas dibuat atau anggota statis diakses. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-constructors

Implementasi yang ditunjukkan adalah thread aman untuk konstruksi awal, yaitu, tidak ada penguncian atau pengujian nol diperlukan untuk membangun objek Singleton. Namun, ini tidak berarti bahwa penggunaan instance akan disinkronkan. Ada berbagai cara yang bisa dilakukan; Saya telah menunjukkan satu di bawah ini.

public class Singleton
{
    private static Singleton instance;
    // Added a static mutex for synchronising use of instance.
    private static System.Threading.Mutex mutex;
    private Singleton() { }
    static Singleton()
    {
        instance = new Singleton();
        mutex = new System.Threading.Mutex();
    }

    public static Singleton Acquire()
    {
        mutex.WaitOne();
        return instance;
    }

    // Each call to Acquire() requires a call to Release()
    public static void Release()
    {
        mutex.ReleaseMutex();
    }
}

53
Perhatikan bahwa jika objek tunggal Anda tidak dapat diubah, menggunakan mutex atau mekanisme sinkronisasi apa pun adalah berlebihan dan tidak boleh digunakan. Juga, saya menemukan contoh implementasi di atas sangat rapuh :-). Semua kode menggunakan Singleton.Acquire () diharapkan memanggil Singleton.Release () ketika selesai menggunakan instance singleton. Gagal melakukan ini (mis. Kembali sebelum waktunya, meninggalkan ruang lingkup melalui pengecualian, lupa menelepon Release), lain kali Singleton ini diakses dari utas lain yang akan menemui jalan buntu di Singleton. Acquire ().
Milan Gardian

2
Setuju, meskipun aku akan melangkah lebih jauh. Jika singleton Anda tidak berubah, menggunakan singleton adalah berlebihan. Cukup tetapkan konstanta. Pada akhirnya, menggunakan singleton dengan benar mengharuskan pengembang tahu apa yang mereka lakukan. Betapapun rapuhnya implementasi ini, itu masih lebih baik daripada yang ada di pertanyaan di mana kesalahan-kesalahan itu bermanifestasi secara acak alih-alih sebagai suatu mutex yang jelas belum dirilis.
Zooba

26
Salah satu cara untuk mengurangi kerapuhan metode Release () adalah dengan menggunakan kelas lain dengan IDisposable sebagai penangan sinkronisasi. Saat Anda memperoleh singleton, Anda mendapatkan handler dan dapat memasukkan kode yang membutuhkan singleton ke dalam blok penggunaan untuk menangani rilis.
CodexArcanum

5
Untuk orang lain yang mungkin tersandung oleh ini: Setiap anggota bidang statis dengan inisialisasi diinisialisasi sebelum konstruktor statis dipanggil.
Adam W. McKinley

12
Jawabannya akhir-akhir ini adalah menggunakan Lazy<T>- siapa pun yang menggunakan kode yang saya posting pada awalnya melakukan kesalahan (dan jujur ​​itu tidak baik untuk memulai dengan - 5-tahun-lalu-saya tidak sebagus barang ini seperti saat ini -aku adalah :) ).
Zooba

86

Sementara semua jawaban ini memberikan jawaban umum yang sama, ada satu peringatan.

Ingat bahwa semua derivasi potensial dari kelas generik dikompilasi sebagai tipe individu. Jadi gunakan hati-hati ketika menerapkan konstruktor statis untuk tipe generik.

class MyObject<T>
{
    static MyObject() 
    {
       //this code will get executed for each T.
    }
}

EDIT:

Inilah demonstrasi:

static void Main(string[] args)
{
    var obj = new Foo<object>();
    var obj2 = new Foo<string>();
}

public class Foo<T>
{
    static Foo()
    {
         System.Diagnostics.Debug.WriteLine(String.Format("Hit {0}", typeof(T).ToString()));        
    }
}

Di konsol:

Hit System.Object
Hit System.String

typeof (MyObject <T>)! = typeof (MyObject <Y>);
Karim Agha

6
Saya pikir itulah yang ingin saya sampaikan. Tipe generik dikompilasi sebagai tipe individual berdasarkan parameter generik yang digunakan, sehingga konstruktor statis dapat dan akan dipanggil beberapa kali.
Brian Rudolph

1
Ini benar ketika T adalah tipe nilai, untuk tipe referensi T hanya satu tipe generik yang akan dihasilkan
sll

2
@sll: Tidak Benar ... Lihat Edit saya
Brian Rudolph

2
Cosntructor menarik tetapi benar-benar statis dipanggil untuk semua jenis, hanya mencoba untuk beberapa jenis referensi
sll

28

Menggunakan konstruktor statis sebenarnya adalah threadsafe. Konstruktor statis dijamin akan dieksekusi hanya sekali.

Dari spesifikasi bahasa C # :

Konstruktor statis untuk suatu kelas dieksekusi paling banyak sekali dalam domain aplikasi yang diberikan. Eksekusi konstruktor statis dipicu oleh yang pertama dari peristiwa berikut terjadi dalam domain aplikasi:

  • Sebuah instance dari kelas dibuat.
  • Setiap anggota statis kelas direferensikan.

Jadi ya, Anda bisa percaya bahwa singleton Anda akan dipakai dengan benar.

Zooba membuat titik yang sangat baik (dan 15 detik sebelum saya juga!) Bahwa konstruktor statis tidak akan menjamin akses bersama aman-thread ke singleton. Itu perlu ditangani dengan cara lain.


8

Inilah versi Cliffnotes dari halaman MSDN di atas pada c # singleton:

Gunakan pola berikut, selalu, Anda tidak bisa salah:

public sealed class Singleton
{
   private static readonly Singleton instance = new Singleton();

   private Singleton(){}

   public static Singleton Instance
   {
      get 
      {
         return instance; 
      }
   }
}

Di luar fitur singleton yang jelas, ia memberi Anda dua hal ini secara gratis (sehubungan dengan singleton di c ++):

  1. konstruksi malas (atau tidak ada konstruksi jika tidak pernah disebut)
  2. sinkronisasi

3
Malas jika kelas tidak memiliki statika yang tidak terkait lainnya (seperti const). Jika tidak mengakses metode atau properti statis apa pun akan menghasilkan pembuatan instance. Jadi saya tidak akan menyebutnya malas.
Schultz9999

6

Konstruktor statis dijamin akan memecat hanya sekali per App Domain sehingga pendekatan Anda harus OK. Namun, secara fungsional tidak ada bedanya dengan versi inline yang lebih ringkas:

private static readonly Singleton instance = new Singleton();

Keamanan utas lebih merupakan masalah ketika Anda malas menginisialisasi sesuatu.


4
Andrew, itu tidak sepenuhnya setara. Dengan tidak menggunakan konstruktor statis, beberapa jaminan tentang kapan initializer akan dieksekusi hilang. Silakan lihat tautan ini untuk penjelasan mendalam: * < csharpindepth.com/Articles/General/Beforefieldinit.aspx > * < ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html >
Derek Park

Derek, saya kenal dengan "optimasi" medan sebelumnya tetapi, secara pribadi, saya tidak pernah khawatir tentang hal itu.
Andrew Peters

tautan kerja untuk komentar @ DerekPark: csharpindepth.com/Articles/General/Beforefieldinit.aspx . Tautan ini tampaknya sudah usang
phoog

4

Konstruktor statis akan selesai berjalan sebelum utas apa pun diizinkan untuk mengakses kelas.

    private class InitializerTest
    {
        static private int _x;
        static public string Status()
        {
            return "_x = " + _x;
        }
        static InitializerTest()
        {
            System.Diagnostics.Debug.WriteLine("InitializerTest() starting.");
            _x = 1;
            Thread.Sleep(3000);
            _x = 2;
            System.Diagnostics.Debug.WriteLine("InitializerTest() finished.");
        }
    }

    private void ClassInitializerInThread()
    {
        System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.GetHashCode() + ": ClassInitializerInThread() starting.");
        string status = InitializerTest.Status();
        System.Diagnostics.Debug.WriteLine(Thread.CurrentThread.GetHashCode() + ": ClassInitializerInThread() status = " + status);
    }

    private void classInitializerButton_Click(object sender, EventArgs e)
    {
        new Thread(ClassInitializerInThread).Start();
        new Thread(ClassInitializerInThread).Start();
        new Thread(ClassInitializerInThread).Start();
    }

Kode di atas menghasilkan hasil di bawah ini.

10: ClassInitializerInThread() starting.
11: ClassInitializerInThread() starting.
12: ClassInitializerInThread() starting.
InitializerTest() starting.
InitializerTest() finished.
11: ClassInitializerInThread() status = _x = 2
The thread 0x2650 has exited with code 0 (0x0).
10: ClassInitializerInThread() status = _x = 2
The thread 0x1f50 has exited with code 0 (0x0).
12: ClassInitializerInThread() status = _x = 2
The thread 0x73c has exited with code 0 (0x0).

Meskipun konstruktor statis membutuhkan waktu lama untuk berjalan, utas lainnya berhenti dan menunggu. Semua utas membaca nilai _x yang ditetapkan di bagian bawah konstruktor statis.


3

The Common Language Spesifikasi Infrastruktur jaminan bahwa "jenis initializer akan dijalankan tepat sekali untuk setiap jenis tertentu, kecuali secara eksplisit disebut oleh kode pengguna." (Bagian 9.5.3.1.) Jadi, kecuali jika Anda memiliki beberapa IL yang aneh pada panggilan longgar Singleton :: .cctor secara langsung (tidak mungkin) konstruktor statis Anda akan berjalan tepat sekali sebelum tipe Singleton digunakan, hanya satu instance Singleton yang akan dibuat, dan properti Instance Anda aman-utas.

Perhatikan bahwa jika konstruktor Singleton mengakses properti Instance (bahkan secara tidak langsung) maka properti Instance akan menjadi nol. Yang terbaik yang dapat Anda lakukan adalah mendeteksi kapan ini terjadi dan melempar pengecualian, dengan memeriksa bahwa instance tersebut tidak nol di accessor properti. Setelah konstruktor statis Anda selesai, properti Instance akan menjadi non-null.

Seperti yang ditunjukkan oleh jawaban Zoomba, Anda perlu membuat Singleton aman untuk diakses dari banyak utas, atau menerapkan mekanisme penguncian dengan menggunakan instance singleton.




0

Meskipun jawaban lain sebagian besar benar, ada peringatan lain dengan konstruktor statis.

Sesuai bagian II.10.5.3.3 Ras dan kebuntuan dari ECMA-335 Common Language Infrastructure

Jenis inisialisasi saja tidak boleh membuat kebuntuan kecuali beberapa kode dipanggil dari penginisialisasi tipe (langsung atau tidak langsung) secara eksplisit memanggil operasi pemblokiran.

Kode berikut ini menghasilkan jalan buntu

using System.Threading;
class MyClass
{
    static void Main() { /* Won’t run... the static constructor deadlocks */  }

    static MyClass()
    {
        Thread thread = new Thread(arg => { });
        thread.Start();
        thread.Join();
    }
}

Penulis asli adalah Igor Ostrovsky, lihat jabatannya di sini .

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.