คอนสตรัคเตอร์ C # คงที่ปลอดภัยหรือไม่?


247

กล่าวอีกนัยหนึ่งเธรดการใช้งานซิงเกิลนี้ปลอดภัยหรือไม่:

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

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

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

1
มันเป็นเธรดที่ปลอดภัย สมมติว่ามีหลายเธรดที่ต้องการรับคุณสมบัติInstanceพร้อมกัน หนึ่งในเธรดจะถูกบอกให้เรียกใช้ประเภทการเริ่มต้นครั้งแรก (หรือที่เรียกว่าตัวสร้างแบบคงที่) ในขณะเดียวกันเธรดอื่น ๆ ที่ต้องการอ่านInstanceคุณสมบัติจะถูกล็อกจนกว่าตัวเตรียมข้อมูลเบื้องต้นชนิดจะเสร็จสิ้น เฉพาะหลังจากที่ initializer ฟิลด์ได้ข้อสรุปแล้วเธรดจะได้รับอนุญาตให้รับInstanceค่า ดังนั้นไม่มีใครสามารถเห็นInstanceความเป็นnullอยู่
Jeppe Stig Nielsen

@JeppeStigNielsen เธรดอื่นไม่ได้ถูกล็อค จากประสบการณ์ของฉันเองฉันได้รับข้อผิดพลาดที่น่ารังเกียจเพราะสิ่งนั้น การรับประกันคือเฉพาะเธรดเริ่มต้นเท่านั้นที่จะเริ่มต้นสแตติกหรือตัวสร้าง แต่เธรดอื่นจะพยายามใช้วิธีการคงที่แม้ว่ากระบวนการก่อสร้างจะไม่เสร็จสิ้น
Narvalex

2
@Narvalex โปรแกรมตัวอย่างนี้ (ที่เข้ารหัสใน URL) ไม่สามารถสร้างปัญหาที่คุณอธิบายได้ อาจจะขึ้นอยู่กับ CLR รุ่นที่คุณใช้อยู่?
Jeppe Stig Nielsen

@JeppeStigNielsen ขอบคุณที่สละเวลา คุณช่วยอธิบายให้ฉันฟังหน่อยได้ไหมว่าทำไมที่นี่ถึงทุ่งโล่ง?
Narvalex

5
@Narvalex กับที่รหัสบนกรณีXสิ้นสุดลงด้วยการได้โดยไม่ต้องเกลียว-1 ไม่ใช่ปัญหาความปลอดภัยของเธรด แต่เครื่องมือเริ่มต้นx = -1จะรันก่อน (อยู่ในบรรทัดก่อนหน้าในรหัสซึ่งเป็นหมายเลขบรรทัดล่าง) แล้วการเริ่มต้นX = GetX()การทำงานซึ่งจะทำให้บนกรณีเท่ากับX -1จากนั้นตัวสร้างแบบคงที่ "ชัดเจน" จะเป็นตัวเริ่มต้นของประเภทstatic C() { ... }ซึ่งจะเปลี่ยนเป็นตัวพิมพ์เล็กxเท่านั้น ดังนั้นหลังจากนั้นMainวิธีการ (หรือOtherวิธีการ) สามารถไปและอ่านตัวพิมพ์Xใหญ่ ค่าของมันจะเป็นเท่า-1กันแม้จะมีเพียงหนึ่งเธรด
Jeppe Stig Nielsen

คำตอบ:


189

ตัวสร้างแบบสแตติกรับประกันว่าจะรันเพียงหนึ่งครั้งต่อโดเมนแอปพลิเคชันก่อนที่อินสแตนซ์ใด ๆ ของคลาสจะถูกสร้างขึ้นหรือมีการเข้าถึงสมาชิกแบบคงที่ใด ๆ https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/static-constructors

การนำไปใช้งานที่แสดงนั้นเป็นเธรดที่ปลอดภัยสำหรับการก่อสร้างเริ่มต้นกล่าวคือไม่จำเป็นต้องมีการทดสอบการล็อกหรือโมฆะสำหรับการสร้างวัตถุ Singleton อย่างไรก็ตามนี่ไม่ได้หมายความว่าการใช้งานอินสแตนซ์ใด ๆ จะถูกซิงโครไนซ์ มีหลายวิธีที่สามารถทำได้ ฉันได้แสดงไว้ด้านล่าง

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
โปรดทราบว่าหากวัตถุซิงเกิลตันของคุณไม่เปลี่ยนรูปการใช้ mutex หรือกลไกการซิงโครไนซ์ใด ๆ เป็น overkill และไม่ควรใช้ นอกจากนี้ฉันพบว่าการใช้งานตัวอย่างข้างต้นเปราะมาก :-) รหัสทั้งหมดที่ใช้ Singleton.Acquire () คาดว่าจะเรียกใช้ Singleton.Release () เมื่อเสร็จสิ้นการใช้อินสแตนซ์ซิงเกิล ความล้มเหลวในการทำเช่นนี้ (เช่นกลับมาก่อนกำหนดออกจากขอบเขตผ่านข้อยกเว้นลืมเรียก Release) ในครั้งต่อไปที่ Singleton นี้เข้าถึงได้จากเธรดอื่นมันจะหยุดชะงักใน Singleton.Acquire ()
Milan Gardian

2
ตกลงแม้ว่าฉันจะไปต่อ หากซิงเกิลตันของคุณไม่เปลี่ยนรูปการใช้ซิงเกิลก็มากเกินไป เพียงแค่กำหนดค่าคงที่ ในที่สุดการใช้ซิงเกิลตันอย่างถูกต้องนั้นนักพัฒนาต้องรู้ว่าพวกเขากำลังทำอะไรอยู่ ในฐานะที่เปราะบางเช่นการดำเนินการนี้ก็ยังดีกว่าหนึ่งในคำถามที่ข้อผิดพลาดเหล่านั้นปรากฏแบบสุ่มมากกว่าเป็น mutex ที่ยังไม่ได้เผยแพร่อย่างเห็นได้ชัด
Zooba

26
วิธีหนึ่งในการลดความเปราะบางของวิธี Release () คือการใช้คลาสอื่นที่มี IDisposable เป็นตัวจัดการการซิงค์ เมื่อคุณได้รับซิงเกิลคุณจะได้รับ handler และสามารถใส่โค้ดที่ต้องการให้ซิงเกิลนั้นเป็นบล็อกที่ใช้เพื่อจัดการกับการวางจำหน่าย
CodexArcanum

5
สำหรับคนอื่น ๆ ที่อาจจะสะดุดโดยสิ่งนี้: สมาชิกสนามคงที่ใด ๆ ที่มี initializers จะเริ่มต้นได้ก่อนที่จะเรียกตัวสร้างคง
Adam W. McKinley

12
คำตอบในวันนี้คือการใช้Lazy<T>- ทุกคนที่ใช้รหัสที่ฉันโพสต์ในตอนแรกทำผิด (และโดยสุจริตมันไม่ดีเลยที่จะเริ่มต้นด้วย - 5 ปีที่แล้ว - ฉันไม่ได้ดีกับสิ่งนี้เหมือนปัจจุบัน -me คือ :))
Zooba

86

ในขณะที่คำตอบทั้งหมดเหล่านี้ให้คำตอบทั่วไปเหมือนกัน แต่ก็มีข้อแม้หนึ่งข้อ

โปรดจำไว้ว่าการสืบทอดที่เป็นไปได้ทั้งหมดของคลาสทั่วไปนั้นได้รับการรวบรวมเป็นประเภทบุคคล ดังนั้นควรใช้ความระมัดระวังเมื่อใช้ตัวสร้างสแตติกสำหรับประเภททั่วไป

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

แก้ไข:

นี่คือการสาธิต:

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

ในคอนโซล:

Hit System.Object
Hit System.String

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

6
ฉันคิดว่าเป็นจุดที่ฉันพยายามทำ Generic types ถูกคอมไพล์เป็นแต่ละประเภทตามที่ใช้พารามิเตอร์ทั่วไปดังนั้นตัวสร้างสแตติกสามารถและจะถูกเรียกหลายครั้ง
Brian Rudolph

1
สิ่งนี้ถูกต้องเมื่อ T เป็นประเภทค่าสำหรับประเภทการอ้างอิง T จะสร้างประเภทสามัญเพียงประเภทเดียวเท่านั้น
sll

2
@sll: ไม่จริง ... ดูการแก้ไขของฉัน
Brian Rudolph

2
cosntructor ที่น่าสนใจ แต่คงที่จริงๆเรียกร้องให้ทุกประเภทลองใช้กับการอ้างอิงหลายประเภท
sll

28

การใช้ตัวสร้างสแตติกจริง ๆ แล้วเป็น threadsafe ตัวสร้างแบบคงที่รับประกันว่าจะดำเนินการเพียงครั้งเดียว

จากข้อกำหนดภาษา C # :

ตัวสร้างแบบคงที่สำหรับคลาสดำเนินการอย่างมากที่สุดในโดเมนแอปพลิเคชันที่กำหนด การดำเนินการของตัวสร้างแบบคงที่จะถูกเรียกโดยเหตุการณ์แรกของเหตุการณ์ต่อไปนี้ที่จะเกิดขึ้นภายในแอปพลิเคชันโดเมน:

  • ตัวอย่างของการเรียนที่ถูกสร้างขึ้น
  • สมาชิกแบบสแตติกของคลาสใด ๆ ที่ถูกอ้างถึง

ใช่คุณสามารถมั่นใจได้ว่าซิงเกิลตันของคุณจะถูกสร้างขึ้นอย่างถูกต้อง

Zooba ทำคะแนนได้อย่างยอดเยี่ยม (และ 15 วินาทีต่อหน้าฉันด้วย!) ที่ตัวสร้างสแตติกจะไม่รับประกันการเข้าถึงที่แชร์แบบปลอดภัยต่อเธรดไปยังซิงเกิล ที่จะต้องมีการจัดการในลักษณะอื่น


8

นี่คือเวอร์ชั่น Cliffnotes จากหน้า MSDN ด้านบนใน c # singleton:

ใช้รูปแบบต่อไปนี้เสมอคุณจะไม่ผิดพลาด:

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

   private Singleton(){}

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

นอกเหนือจากคุณสมบัติซิงเกิลที่เห็นได้ชัดมันให้สองสิ่งนี้ฟรี (สำหรับซิงเกิลตันใน c ++):

  1. สันหลังยาวก่อสร้าง (หรือไม่มีการก่อสร้างถ้ามันไม่เคยเรียก)
  2. การประสาน

3
ขี้เกียจหากชั้นเรียนไม่มีสถิตยศาสตร์อื่นใด (เช่น const) มิฉะนั้นการเข้าถึงวิธีการคงที่หรือคุณสมบัติใด ๆ จะส่งผลให้การสร้างอินสแตน ดังนั้นฉันจะไม่เรียกมันว่าขี้เกียจ
Schultz9999

6

ตัวสร้างแบบคงที่รับประกันว่าจะใช้งานได้เพียงหนึ่งครั้งต่อ App Domain ดังนั้นแนวทางของคุณควรจะดี อย่างไรก็ตามมันไม่แตกต่างจากเวอร์ชั่นอินไลน์ที่กระชับกว่านี้:

private static readonly Singleton instance = new Singleton();

ความปลอดภัยของเธรดเป็นปัญหามากกว่าเมื่อคุณเริ่มต้นสิ่งต่างๆอย่างขี้เกียจ


4
แอนดรูที่ไม่เทียบเท่าอย่างเต็มที่ โดยไม่ใช้ตัวสร้างสแตติกการรับประกันบางอย่างเกี่ยวกับเมื่อ initializer จะถูกดำเนินการจะหายไป โปรดดูลิงค์เหล่านี้สำหรับคำอธิบายในเชิงลึก: * < csharpindepth.com/Articles/General/Beforefieldinit.aspx > * < ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html >
Derek Park

ดีเร็กผมคุ้นเคยกับbeforefieldinit "การเพิ่มประสิทธิภาพ" แต่ส่วนตัวผมไม่เคยกังวลเกี่ยวกับมัน
Andrew Peters

การเชื่อมโยงการทำงานสำหรับการแสดงความคิดเห็น @ DerekPark ของ: csharpindepth.com/Articles/General/Beforefieldinit.aspx ลิงค์นี้ดูเหมือนค้าง: ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html
phoog

4

ตัวสร้างแบบสแตติกจะเสร็จสิ้นการรันก่อนที่เธรดใด ๆ จะได้รับอนุญาตให้เข้าถึงคลาส

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

รหัสด้านบนสร้างผลลัพธ์ด้านล่าง

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).

แม้ว่าตัวสร้างแบบสแตติกใช้เวลานานในการรันเธรดอื่นจะหยุดและรอ กระทู้ทั้งหมดอ่านค่าของชุด _x ที่ด้านล่างของตัวสร้างสแตติก


3

ภาษาทั่วไปข้อกำหนดโครงสร้างพื้นฐานรับประกันว่า "ประเภท initializer จะวิ่งครั้งว่าสำหรับประเภทใดก็ตามเว้นแต่เรียกว่าอย่างชัดเจนโดยผู้ใช้รหัส." (มาตรา 9.5.3.1.) ดังนั้นหากคุณไม่มี IL บางตัวในการเรียก Singleton ::. cctor โดยตรง (ไม่น่าเป็นไปได้) Constructor แบบคงที่ของคุณจะทำงานอย่างแน่นอนหนึ่งครั้งก่อนที่จะใช้ Singleton type เพียงครั้งเดียวของ Singleton จะถูกสร้างขึ้น และคุณสมบัติอินสแตนซ์ของคุณปลอดภัยสำหรับเธรด

โปรดทราบว่าหากตัวสร้างของ Singleton เข้าถึงคุณสมบัติอินสแตนซ์ (แม้โดยทางอ้อม) คุณสมบัติอินสแตนซ์จะเป็นโมฆะ สิ่งที่ดีที่สุดที่คุณสามารถทำได้คือการตรวจจับเมื่อสิ่งนี้เกิดขึ้นและส่งข้อยกเว้นโดยตรวจสอบว่าอินสแตนซ์นั้นไม่เป็นโมฆะในตัวเข้าถึงคุณสมบัติ หลังจากตัวสร้างสแตติกของคุณเสร็จสมบูรณ์คุณสมบัติอินสแตนซ์จะไม่เป็นโมฆะ

เมื่อคำตอบของ Zoombaชี้ให้เห็นคุณจะต้องทำให้ Singleton ปลอดภัยในการเข้าถึงจากหลายเธรดหรือใช้กลไกการล็อกโดยใช้อินสแตนซ์ซิงเกิล


2

เพื่อจะอวดความรู้ แต่ไม่มีสิ่งนี้ในฐานะตัวสร้างแบบคงที่ แต่เป็นตัวเริ่มต้นแบบคงที่ แต่นี่คือตัวอย่างเล็ก ๆของการพึ่งพาตัวสร้างแบบคงที่ของวงจรซึ่งแสดงให้เห็นถึงจุดนี้


1
Microsoft ดูเหมือนจะไม่เห็นด้วย msdn.microsoft.com/en-us/library/k9x6w0hc.aspx
Westy92


0

แม้ว่าคำตอบอื่น ๆ ส่วนใหญ่จะถูกต้อง แต่ก็มีข้อแม้อื่นที่มีตัวสร้างแบบคงที่

ตามหัวข้อII.10.5.3.3 การแข่งขันและการหยุดชะงักของโครงสร้างพื้นฐานภาษาสามัญ ECMA-335

การเริ่มต้นประเภทอย่างเดียวจะต้องไม่สร้างการหยุดชะงักเว้นแต่ว่าบางรหัสที่เรียกจากตัวเริ่มต้นประเภท (โดยตรงหรือโดยอ้อม) จะเรียกใช้การบล็อกอย่างชัดเจน

โค้ดต่อไปนี้ส่งผลให้เกิดการหยุดชะงัก

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

ผู้เขียนต้นฉบับคืออิกอร์รอฟสกี, เห็นโพสต์ของเขาที่นี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.