Việc triển khai chủ đề mẫu Singleton của Meyers có an toàn không?


145

Việc triển khai sau đây, sử dụng khởi tạo lười biếng, của Singletonluồng (Meyers 'Singleton) có an toàn không?

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

Nếu không, tại sao và làm thế nào để làm cho nó an toàn?


Ai đó có thể vui lòng giải thích tại sao điều này không phải là chủ đề an toàn. Các bài viết được đề cập trong các liên kết thảo luận về an toàn của luồng bằng cách sử dụng một triển khai thay thế (sử dụng biến con trỏ tức là Singleton * pInstance).
Ankur



Câu trả lời:


168

Trong C ++ 11 , nó là chủ đề an toàn. Theo tiêu chuẩn , §6.7 [stmt.dcl] p4:

Nếu điều khiển đi vào khai báo đồng thời trong khi biến đang được khởi tạo, thì việc thực thi đồng thời sẽ chờ hoàn thành việc khởi tạo.

Hỗ trợ GCC và VS cho tính năng ( Khởi tạo và phá hủy động với đồng thời , còn được gọi là Magic Statics trên MSDN ) như sau:

Cảm ơn @Mankude và @olen_gam vì những bình luận của họ.


Trong C ++ 03 , mã này không phải là chủ đề an toàn. Có một bài viết của Meyers có tên là "C ++ và các hiểm họa của khóa kiểm tra hai lần" bàn về việc triển khai mô hình an toàn của chủ đề, và kết luận là, ít nhiều, (trong C ++ 03) khóa hoàn toàn xung quanh phương pháp khởi tạo về cơ bản là cách đơn giản nhất để đảm bảo đồng thời phù hợp trên tất cả các nền tảng, trong khi hầu hết các dạng biến thể mô hình khóa được kiểm tra hai lần có thể phải chịu các điều kiện chạy đua trên các kiến ​​trúc nhất định , trừ khi các hướng dẫn được đặt xen kẽ với các rào cản bộ nhớ chiến lược.


3
Ngoài ra còn có một cuộc thảo luận rộng rãi về Mô hình Singleton (trọn đời và an toàn luồng) của Alexandrescu trong Thiết kế C ++ hiện đại. Xem trang web của Loki: loki-lib.sourceforge.net/index.php?n=Potype.Singleton
Matthieu M.

1
Bạn có thể tạo một singleton an toàn luồng với boost :: call_once.
CashCow

1
Thật không may, phần này của tiêu chuẩn không được triển khai trong Trình biên dịch Visual Studio 2012 C ++. Được gọi là "Magic Statics" trong bảng "C ++ 11 Các tính năng ngôn ngữ cốt lõi: Đồng thời" tại đây: msdn.microsoft.com/en-us/l
Library / vstudio / hh567368.aspx

Đoạn trích từ xây dựng địa chỉ tiêu chuẩn nhưng không phá hủy. Có phải tiêu chuẩn ngăn không cho đối tượng bị phá hủy trên một luồng trong khi (hoặc trước đó) một luồng khác cố gắng truy cập nó khi kết thúc chương trình?
stewbasic

IANA (ngôn ngữ C ++) L nhưng phần 3.6.3 [basic.start.term] p2 cho thấy có thể đánh vào hành vi không xác định bằng cách cố gắng truy cập vào đối tượng sau khi nó bị phá hủy?
stewbasic

21

Để trả lời câu hỏi của bạn về lý do tại sao nó không an toàn, không phải vì cuộc gọi đầu tiên instance()phải gọi hàm tạo Singleton s. Để đảm bảo an toàn, điều này sẽ phải xảy ra trong một phần quan trọng, nhưng không có yêu cầu nào trong tiêu chuẩn là phải thực hiện một phần quan trọng (tiêu chuẩn cho đến nay là hoàn toàn im lặng đối với các luồng). Trình biên dịch thường thực hiện điều này bằng cách sử dụng một kiểm tra đơn giản và gia tăng của một boolean tĩnh - nhưng không phải trong một phần quan trọng. Một cái gì đó giống như mã giả sau đây:

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

Vì vậy, đây là một Singleton an toàn chủ đề đơn giản (cho Windows). Nó sử dụng một trình bao bọc lớp đơn giản cho đối tượng Windows CRITICAL_SECTION để chúng ta có thể có trình biên dịch tự động khởi tạo CRITICAL_SECTIONtrước khi main()được gọi. Lý tưởng nhất là một lớp phần quan trọng RAII thực sự sẽ được sử dụng có thể xử lý các trường hợp ngoại lệ có thể xảy ra khi phần quan trọng được tổ chức, nhưng điều đó nằm ngoài phạm vi của câu trả lời này.

Hoạt động cơ bản là khi một phiên bản Singletonđược yêu cầu, khóa được thực hiện, Singleton được tạo nếu cần, sau đó khóa được giải phóng và tham chiếu Singleton được trả về.

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

Con người - đó là rất nhiều điều tào lao để "làm cho một toàn cầu tốt hơn".

Hạn chế chính của việc triển khai này (nếu tôi không để một số lỗi xảy ra) là:

  • nếu new Singleton()ném, khóa sẽ không được phát hành. Điều này có thể được khắc phục bằng cách sử dụng một đối tượng khóa RAII thực sự thay vì đối tượng đơn giản tôi có ở đây. Điều này cũng có thể giúp làm cho mọi thứ trở nên di động nếu bạn sử dụng một cái gì đó như Boost để cung cấp trình bao bọc độc lập nền tảng cho khóa.
  • điều này đảm bảo an toàn cho luồng khi phiên bản Singleton được yêu cầu sau khi main()được gọi - nếu bạn gọi nó trước đó (như trong khởi tạo đối tượng tĩnh), mọi thứ có thể không hoạt động vì CRITICAL_SECTIONcó thể không được khởi tạo.
  • một khóa phải được thực hiện mỗi khi một thể hiện được yêu cầu. Như tôi đã nói, đây là một chủ đề đơn giản thực hiện an toàn. Nếu bạn cần một cái tốt hơn (hoặc muốn biết lý do tại sao những thứ như kỹ thuật khóa kiểm tra hai lần bị lỗi), hãy xem các giấy tờ được liên kết trong câu trả lời của Groo .

1
À ồ. Điều gì xảy ra nếu new Singleton()ném?
sbi

@Bob - công bằng mà nói, với một bộ thư viện thích hợp, tất cả các hành trình phải làm với khả năng không sao chép và khóa RAII thích hợp sẽ biến mất hoặc tối thiểu. Nhưng tôi muốn ví dụ này được khép kín một cách hợp lý. Mặc dù singleton là rất nhiều công việc để có thể đạt được tối thiểu, tôi đã thấy chúng hữu ích trong việc quản lý việc sử dụng toàn cầu. Họ có xu hướng làm cho nó dễ dàng hơn để tìm ra nơi và khi họ được sử dụng tốt hơn một chút so với chỉ một quy ước đặt tên.
Michael Burr

@sbi: trong ví dụ này, nếu new Singleton()ném chắc chắn có vấn đề với khóa. Nên sử dụng lớp khóa RAII thích hợp, giống như lock_guardtừ Boost. Tôi muốn ví dụ này ít nhiều khép kín, và nó đã là một con quái vật nên tôi đã bỏ đi sự an toàn ngoại lệ (nhưng gọi nó ra). Có lẽ tôi nên sửa nó để mã này không bị cắt ở đâu đó không phù hợp.
Michael Burr

Tại sao tự động phân bổ singleton? Tại sao không biến 'pInstance' thành thành viên tĩnh của 'Singleton :: instance ()'?
Martin York

@Martin - xong rồi. Bạn nói đúng, điều đó làm cho nó đơn giản hơn một chút - sẽ còn tốt hơn nữa nếu tôi sử dụng lớp khóa RAII.
Michael Burr

10

Nhìn vào tiêu chuẩn tiếp theo (mục 6.7.4), nó khám phá cách khởi tạo cục bộ tĩnh là luồng an toàn. Vì vậy, một khi phần tiêu chuẩn đó được triển khai rộng rãi, Meyer's Singleton sẽ là phần triển khai được ưa thích.

Tôi không đồng ý với nhiều câu trả lời rồi. Hầu hết các trình biên dịch đã thực hiện khởi tạo tĩnh theo cách này. Một ngoại lệ đáng chú ý là Microsoft Visual Studio.


6

Câu trả lời đúng phụ thuộc vào trình biên dịch của bạn. Nó có thể quyết định làm cho nó an toàn; đó không phải là chủ đề "tự nhiên".


5

Việc thực hiện sau đây [...] chủ đề có an toàn không?

Trên hầu hết các nền tảng, điều này không an toàn cho chuỗi. (Nối phần từ chối trách nhiệm thông thường giải thích rằng tiêu chuẩn C ++ không biết về các luồng, vì vậy, về mặt pháp lý, nó không cho biết liệu nó có hay không.)

Nếu không, tại sao [...]?

Lý do không phải là không có gì ngăn cản nhiều hơn một luồng thực thi đồng thời s'hàm tạo.

Làm thế nào để làm cho nó chủ đề an toàn?

"C ++ và những hiểm họa của khóa kiểm tra hai lần" của Scott Meyers và Andrei Alexandrescu là một chuyên luận khá hay về chủ đề của những người độc thân an toàn theo chủ đề.


2

Như MSalters đã nói: Nó phụ thuộc vào việc triển khai C ++ mà bạn sử dụng. Kiểm tra tài liệu. Đối với câu hỏi khác: "Nếu không, tại sao?" - Tiêu chuẩn C ++ chưa đề cập bất cứ điều gì về chủ đề. Nhưng phiên bản C ++ sắp tới nhận thức được các luồng và nó tuyên bố rõ ràng rằng việc khởi tạo các địa phương tĩnh là an toàn luồng. Nếu hai luồng gọi một hàm như vậy, một luồng sẽ thực hiện khởi tạo trong khi luồng kia sẽ chặn và chờ cho nó kết thúc.

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.