Việc triển khai sau đây, sử dụng khởi tạo lười biếng, của Singleton
luồ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?
Việc triển khai sau đây, sử dụng khởi tạo lười biếng, của Singleton
luồ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?
Câu trả lời:
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.
Để 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_SECTION
trướ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à:
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.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_SECTION
có thể không được khởi tạo.new Singleton()
ném?
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_guard
từ 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.
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.
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ủ đề.
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.