Chủ đề xây dựng tĩnh C # có an toàn không?


247

Nói cách khác, chủ đề triển khai Singleton này có an toàn không:

public class Singleton
{
    private static Singleton instance;

    private Singleton() { }

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

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

1
Nó là chủ đề an toàn. Giả sử một số chủ đề muốn có được tài sản Instancecùng một lúc. Một trong các luồng sẽ được yêu cầu trước tiên chạy trình khởi tạo kiểu (còn được gọi là hàm tạo tĩnh). Trong khi đó, tất cả các luồng khác muốn đọc thuộc Instancetính, sẽ bị khóa cho đến khi trình khởi tạo kiểu kết thúc. Chỉ sau khi trình khởi tạo trường kết thúc, các luồng sẽ được phép nhận Instancegiá trị. Vì vậy, không ai có thể nhìn thấy Instanceđược null.
Jeppe Stig Nielsen

@JeppeStigNielsen Các chủ đề khác không bị khóa. Từ kinh nghiệm của bản thân, tôi đã nhận được những lỗi khó chịu vì điều đó. Đảm bảo là chỉ có luồng xử lý mới khởi động trình khởi tạo tĩnh hoặc hàm tạo, nhưng sau đó các luồng khác sẽ cố gắng sử dụng phương thức tĩnh, ngay cả khi quá trình xây dựng không kết thúc.
Narvalex

2
@Narvalex Chương trình mẫu này (nguồn được mã hóa bằng URL) không thể tái tạo vấn đề bạn mô tả. Có lẽ nó phụ thuộc vào phiên bản CLR bạn có?
Jeppe Stig Nielsen

@JeppeStigNielsen Cảm ơn bạn đã dành thời gian. Bạn có thể vui lòng giải thích cho tôi tại sao ở đây lĩnh vực này được overriden?
Narvalex

5
@Narvalex Với mã đó, chữ hoa Xcuối cùng -1 thậm chí không có luồng . Nó không phải là một vấn đề an toàn chủ đề. Thay vào đó, trình khởi tạo x = -1chạy trước (nó nằm trên một dòng trước đó trong mã, số dòng thấp hơn). Sau đó, trình khởi X = GetX()chạy chạy, làm cho chữ hoa Xbằng -1. Và sau đó, hàm tạo tĩnh "tường minh", trình khởi tạo kiểu static C() { ... }chạy, chỉ thay đổi chữ thường x. Vì vậy, sau tất cả điều đó, Mainphương thức (hoặc Otherphương thức) có thể tiếp tục và đọc chữ hoa X. Giá trị của nó sẽ là -1, thậm chí chỉ với một luồng.
Jeppe Stig Nielsen

Câu trả lời:


189

Các hàm tạo tĩnh được đảm bảo chỉ được chạy một lần cho mỗi miền ứng dụng, trước khi bất kỳ phiên bản nào của lớp được tạo hoặc bất kỳ thành viên tĩnh nào được truy cập. https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/groupes-and-structs/static-constructor

Việc triển khai được hiển thị là luồng an toàn cho việc xây dựng ban đầu, nghĩa là không yêu cầu kiểm tra khóa hoặc null để xây dựng đối tượng Singleton. Tuy nhiên, điều này không có nghĩa là bất kỳ việc sử dụng thể hiện nào sẽ được đồng bộ hóa. Có nhiều cách khác nhau để làm điều này; Tôi đã chỉ ra một cái bên dưới.

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
Lưu ý rằng nếu đối tượng singleton của bạn là bất biến, sử dụng mutex hoặc bất kỳ cơ chế đồng bộ hóa nào là quá mức cần thiết và không nên được sử dụng. Ngoài ra, tôi thấy việc thực hiện mẫu ở trên cực kỳ dễ vỡ :-). Tất cả mã sử dụng Singleton.Acquire () dự kiến ​​sẽ gọi Singleton.Release () khi hoàn thành sử dụng cá thể singleton. Không làm điều này (ví dụ: trả về sớm, để lại phạm vi ngoại lệ, quên gọi Phát hành), lần tiếp theo Singleton này được truy cập từ một luồng khác, nó sẽ bế tắc trong Singleton.Acquire ().
Milan Gardian

2
Đồng ý, mặc dù tôi sẽ đi xa hơn. Nếu singleton của bạn là bất biến, sử dụng một singleton là quá mức cần thiết. Chỉ cần xác định hằng số. Cuối cùng, sử dụng một singleton đúng cách đòi hỏi các nhà phát triển biết họ đang làm gì. Dễ vỡ như cách triển khai này, nó vẫn tốt hơn so với câu hỏi trong đó các lỗi đó biểu hiện ngẫu nhiên chứ không phải là một mutex rõ ràng chưa được phát hành.
Zooba

26
Một cách để giảm độ giòn của phương thức Release () là sử dụng một lớp khác với IDis Dùng làm trình xử lý đồng bộ. Khi bạn có được singleton, bạn có được trình xử lý và có thể đặt mã yêu cầu singleton vào một khối sử dụng để xử lý việc phát hành.
CodexArcanum

5
Đối với những người khác có thể bị vấp ngã bởi điều này: Bất kỳ thành viên trường tĩnh nào có bộ khởi tạo đều được khởi tạo trước khi hàm tạo tĩnh được gọi.
Adam W. McKinley

12
Câu trả lời ngày nay là sử dụng Lazy<T>- bất kỳ ai sử dụng mã mà tôi đã đăng ban đầu đều làm sai (và thành thật mà nói, nó không tốt khi bắt đầu - 5 năm trước - tôi không giỏi về công cụ này như hiện tại -tôi là :)).
Zooba

86

Trong khi tất cả các câu trả lời này đều đưa ra cùng một câu trả lời chung, có một lời cảnh báo.

Hãy nhớ rằng tất cả các dẫn xuất tiềm năng của một lớp chung được biên dịch thành các loại riêng lẻ. Vì vậy, hãy thận trọng khi thực hiện các hàm tạo tĩnh cho các loại chung.

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

BIÊN TẬP:

Đây là cuộc biểu tình:

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

Trong bảng điều khiển:

Hit System.Object
Hit System.String

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

6
Tôi nghĩ đó là điểm tôi đang cố gắng thực hiện. Các kiểu chung được biên dịch thành các kiểu riêng lẻ dựa trên các tham số chung được sử dụng, do đó, hàm tạo tĩnh có thể và sẽ được gọi nhiều lần.
Brian Rudolph

1
Điều này đúng khi T thuộc loại giá trị, đối với loại tham chiếu T chỉ có một loại chung sẽ được tạo
sll

2
@sll: Không đúng ... Xem Chỉnh sửa của tôi
Brian Rudolph

2
Các cosntructor thú vị nhưng thực sự tĩnh được gọi cho tất cả các loại, chỉ cần thử cho nhiều loại tham chiếu
sll

28

Sử dụng một constructor tĩnh thực sự là chủ đề an toàn. Hàm tạo tĩnh được đảm bảo chỉ được thực hiện một lần.

Từ đặc tả ngôn ngữ C # :

Hàm tạo tĩnh cho một lớp thực thi nhiều nhất một lần trong một miền ứng dụng nhất định. Việc thực thi một hàm tạo tĩnh được kích hoạt bởi sự kiện đầu tiên sau đây xảy ra trong một miền ứng dụng:

  • Một thể hiện của lớp được tạo ra.
  • Bất kỳ thành viên tĩnh nào của lớp đều được tham chiếu.

Vì vậy, có, bạn có thể tin tưởng rằng singleton của bạn sẽ được khởi tạo chính xác.

Zooba đã đưa ra một điểm tuyệt vời (và 15 giây trước tôi nữa!) Rằng nhà xây dựng tĩnh sẽ không đảm bảo quyền truy cập được chia sẻ an toàn theo luồng cho người độc thân. Điều đó sẽ cần phải được xử lý theo cách khác.


8

Đây là phiên bản Cliffnotes từ trang MSDN ở trên trên c # singleton:

Sử dụng mẫu sau, luôn luôn, bạn không thể sai:

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

   private Singleton(){}

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

Ngoài các tính năng đơn lẻ rõ ràng, nó cung cấp cho bạn hai điều này miễn phí (đối với singleton trong c ++):

  1. lười xây dựng (hoặc không xây dựng nếu nó không bao giờ được gọi)
  2. đồng bộ hóa

3
Lười biếng nếu lớp học không có bất kỳ thống kê không liên quan nào khác (như consts). Nếu không, truy cập bất kỳ phương thức tĩnh hoặc thuộc tính sẽ dẫn đến việc tạo cá thể. Vì vậy, tôi sẽ không gọi nó là lười biếng.
Schultz9999

6

Các nhà xây dựng tĩnh được đảm bảo chỉ bắn một lần cho mỗi Miền ứng dụng để cách tiếp cận của bạn sẽ ổn. Tuy nhiên, về mặt chức năng nó không khác với phiên bản nội tuyến ngắn gọn hơn:

private static readonly Singleton instance = new Singleton();

An toàn chủ đề là một vấn đề khi bạn đang lười biếng khởi tạo mọi thứ.


4
Andrew, điều đó không hoàn toàn tương đương. Bằng cách không sử dụng hàm tạo tĩnh, một số đảm bảo về thời điểm bộ khởi tạo sẽ được thực thi sẽ bị mất. Vui lòng xem các liên kết này để được giải thích sâu sắc: * < csharpindepth.com/Articles/General/B Beforefieldinit.aspx > * < ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html >
Derek Park

Derek, tôi quen với beforefieldinit "tối ưu hóa", nhưng cá nhân tôi, tôi không bao giờ lo lắng về nó.
Andrew Peters

liên kết làm việc cho nhận xét của @ DerekPark: csharpindepth.com/Articles/General/B Beforefieldinit.aspx . Liên kết này dường như đã cũ: ondotnet.com/pub/a/dotnet/2003/07/07/staticxtor.html
phoog

4

Hàm tạo tĩnh sẽ kết thúc chạy trước khi bất kỳ luồng nào được phép truy cập vào lớp.

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

Các mã ở trên tạo ra kết quả dưới đây.

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

Mặc dù hàm tạo tĩnh mất nhiều thời gian để chạy, các luồng khác dừng lại và chờ đợi. Tất cả các luồng đọc giá trị của _x được đặt ở dưới cùng của hàm tạo tĩnh.


3

Đặc tả cơ sở hạ tầng ngôn ngữ chung đảm bảo rằng "trình khởi tạo kiểu sẽ chạy chính xác một lần cho bất kỳ loại đã cho nào, trừ khi được gọi rõ ràng bằng mã người dùng." . và thuộc tính Instance của bạn là an toàn chủ đề.

Lưu ý rằng nếu hàm tạo của Singleton truy cập thuộc tính Instance (thậm chí gián tiếp) thì thuộc tính Instance sẽ là null. Điều tốt nhất bạn có thể làm là phát hiện khi điều này xảy ra và đưa ra một ngoại lệ, bằng cách kiểm tra trường hợp đó là không null trong trình truy cập thuộc tính. Sau khi hàm tạo tĩnh của bạn hoàn thành, thuộc tính Instance sẽ không null.

Như câu trả lời của Zoomba chỉ ra, bạn sẽ cần làm cho Singleton an toàn để truy cập từ nhiều luồng hoặc thực hiện cơ chế khóa xung quanh bằng cách sử dụng cá thể singleton.




0

Mặc dù các câu trả lời khác hầu hết là đúng, nhưng vẫn có một cảnh báo khác với các hàm tạo tĩnh.

Theo phần II.10.5.3.3 Các chủng tộc và sự bế tắc của Cơ sở hạ tầng ngôn ngữ chung ECMA-335

Chỉ riêng việc khởi tạo kiểu sẽ không tạo ra bế tắc trừ khi một số mã được gọi từ trình khởi tạo kiểu (trực tiếp hoặc gián tiếp) gọi rõ ràng các hoạt động chặn.

Đoạn mã sau dẫn đến bế tắc

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

Tác giả gốc là Igor Ostrovsky, xem bài viết của mình ở đây .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.