32

Phương pháp đồng bộ hóa nào để sử dụng để đảm bảo một singleton vẫn là một singleton?Chủ đề an toàn instantiation của một singleton

+(Foo*)sharedInstance 
{ 
    @synchronized(self) 
    { 
     if (nil == _sharedInstance) 
     { 
     _sharedInstance = [[Foo alloc] init]; 
     ... 
     } 
    } 
    return _sharedInstance; 
} 

hoặc sử dụng mutex?

#import <pthread.h> 

static pthread_mutex_t _mutex = PTHREAD_MUTEX_INITIALIZER; 

+(Foo*)sharedInstance 
{ 
    pthread_mutex_lock(&_mutex); 
    if (nil == _sharedInstance) 
    { 
     _sharedInstance = [[Foo alloc] init]; 
     ... 
    } 
    pthread_mutex_unlock(&_mutex); 
    return _sharedInstance; 
} 

Hmmm .. any comments on this?

+2

Bạn có thể quan tâm đọc bài viết này (http: //steve.yegge. googlepages.com/singleton-considered-stupid). – sand

+1

Mặc dù ghét của yegge cho người độc thân, họ chắc chắn máy chủ một mục đích trên iPhone. Nhưng nếu bạn chỉ đơn giản là tạo một 'không gian tên', hãy sử dụng các phương thức lớp thay thế. – bentford

+0

@bentford - Tôi đã lập trình iPhones được 4 năm, mã cũ và mới. Tôi đã từng nhìn thấy các single được sử dụng một lần (trong một số mã khá tệ hại). –

Trả lời

55

Hãy chắc chắn rằng bạn đọc tham luận về câu hỏi này/câu trả lời, quá. Why should we separate alloc and init calls to avoid deadlocks in Objective-C?


Để mở rộng vấn đề điều kiện chủng tộc; sửa đổi thực là không có sự khởi tạo không xác định trong ứng dụng của bạn. Không xác định hoặc lười biếng kết quả khởi trong hành vi đó có thể dễ dàng thay đổi do sự thay đổi có vẻ vô hại - cấu hình, "không liên quan" thay đổi mã, vv ...

Tốt hơn để khởi tạo một cách rõ ràng hệ thống con vào một điểm nổi tiếng-tốt trong tuổi thọ của chương trình. I E. thả [MyClass sharedInstance]; vào phương thức applicationDidFinishLaunching: của đại biểu ứng dụng nếu bạn thực sự cần hệ thống con được khởi tạo sớm trong chương trình (hoặc di chuyển nó sớm hơn, nếu bạn muốn phòng thủ thêm).

Vẫn còn tốt hơn để di chuyển khởi tạo hoàn toàn khỏi phương thức đó. I E. [MyClass initializeSharedInstance]; trong đó +sharedInstance xác nhận() nếu phương thức đó không được gọi trước.

Nhiều như tôi là một người hâm mộ tiện lợi, 25 năm lập trình ObjC đã dạy tôi rằng khởi tạo lười biếng là nguồn cung cấp bảo trì và tái cấu trúc hơn những vấn đề đau đầu.


Trong khi tình trạng chủng tộc mô tả dưới đây tồn tại, mã này không sửa chữa những gì được mô tả dưới đây. Nó đã làm cho một vài thập kỷ khi chúng tôi không lo lắng về concurrency trong initializers dụ chia sẻ. Để lại mã sai cho sự thịnh vượng.

Hãy nhớ rằng đối với cả hai câu trả lời chính xác của Colin và Harald, có một điều kiện đua rất tinh tế có thể dẫn bạn đến một thế giới của sự khốn khổ.

Cụ thể, nếu -init của lớp đang được phân bổ xảy ra, hãy gọi phương thức sharedInstance, nó sẽ làm như vậy trước khi biến được đặt. Trong cả hai trường hợp, nó sẽ dẫn đến bế tắc.

Đây là lần bạn muốn tách phân bổ và init. Cribbing đang Colin vì nó là giải pháp tốt nhất (giả sử Mac OS X):

+(MyClass *)sharedInstance 
{ 
    static MyClass *sharedInstance = nil; 
    static dispatch_once_t pred; 

    // partial fix for the "new" concurrency issue 
    if (sharedInstance) return sharedInstance; 
    // partial because it means that +sharedInstance *may* return an un-initialized instance 
    // this is from https://stackoverflow.com/questions/20895214/why-should-we-separate-alloc-and-init-calls-to-avoid-deadlocks-in-objective-c/20895427#20895427 

    dispatch_once(&pred, ^{ 
     sharedInstance = [MyClass alloc]; 
     sharedInstance = [sharedInstance init]; 
    }); 

    return sharedInstance; 
} 

lưu ý này chỉ hoạt động trên Mac OS X; Đặc biệt là X 10.6+ và iOS 4.0+.Trên các hệ điều hành cũ hơn, nơi các khối không có sẵn, hãy sử dụng khóa hoặc một trong các phương tiện khác nhau để làm điều gì đó một khi không phải là khối dựa.


Mẫu trên không thực sự ngăn chặn sự cố được mô tả trong văn bản và sẽ gây ra bế tắc khi gặp phải. Vấn đề là dispatch_once() không được tái nhập và do đó, nếu init gọi sharedInstance, thành phố wedge.

+0

Cảm ơn cho điều này, mặc dù nó đã cho tôi nửa giờ để tìm tài liệu tham khảo chính xác cho các khối (^) cú pháp http: // phát triển. apple.com/Mac/library/documentation/Cocoa/Conceptual/Blocks/Blocks.pdf, vì lý do nào đó không được bao gồm trong tài liệu Mục tiêu C của họ –

+0

Cảm ơn bạn. Đó cũng là điều đầu tiên tôi nghĩ đến khi nhìn thấy câu trả lời của bạn. Tôi không biết có khả năng viết các khối như vậy trong mục tiêu c (như thực hiện lớp ẩn danh trong Java hoặc các đại biểu trong C#). Đây là câu trả lời cho vấn đề khác của tôi ở đây: http://stackoverflow.com/questions/2118728/method-signature-for-a-selector Thay vì sử dụng gọi lại bây giờ tôi sử dụng khối. :) Có lẽ, tôi khởi tạo singleton của mình trong AppDelegate và thực sự không quan tâm đến đồng thời luồng. Ví dụ là đơn giản ở đó, khi cần thiết. Nó là một loại bộ nhớ cache cho vài thứ như hình ảnh, vv – MacTouch

+4

@bbum, (1) tại sao '-init' gọi' + sharedInstance', và (2) tại sao bạn lại bao gồm dòng 'if (sharedInstance) trả về sharedInstance; '? – ma11hew28

2

Điều này CocoaDev page có thể hữu ích cho nhu cầu của bạn.

+0

Cảm ơn. Hmm .. Tôi không nghĩ về khả năng giữ lại hoặc thừa nhận một singleton. Địa ngục, những người làm những điều như vậy? :) – MacTouch

38

Các chủ đề nhanh nhất cách an toàn để làm điều này là với Grand Central Dispatch (libdispatch) và dispatch_once()

+(MyClass *)sharedInstance 
{ 
    static MyClass *sharedInstance = nil; 
    static dispatch_once_t pred; 

    dispatch_once(&pred, ^{ 
     sharedInstance = [[MyClass alloc] init]; 
    }); 

    return sharedInstance; 
} 
+0

tại sao bạn phải sử dụng dispatch_once? Tại sao bạn không thể thực hiện một câu lệnh if như sau: "if (sharedInstance == nil) { sharedInstance = [MyClass alloc] init];" – Honey

+1

điều này không đảm bảo rằng nó sẽ được phân bổ chỉ một lần. gửi một lần đảm bảo rằng khối sẽ chỉ được thực hiện một lần ngay cả khi nhiều luồng tất cả gọi cùng một mã cùng một lúc. Trong đoạn mã của bạn, một thread có thể thực thi câu lệnh if, sau đó đi đến bắt đầu cấp phát cá thể, sau đó một luồng khác cùng lúc đánh giá câu lệnh if và sau đó phân bổ một thể hiện thứ hai và bây giờ bạn đã bị bộ nhớ bị rò rỉ, bạn không thể lấy lại . Mã của bạn là chức năng, nhưng nó sẽ không xử lý các trường hợp như những gì tôi vừa mô tả. –

+0

Cảm ơn tôi đã nhận được quan điểm của bạn về việc an toàn luồng. Những gì tôi không nhận được là tại sao JUST không sử dụng dispatch_once đủ? Tại sao tôi vẫn thấy mọi người làm gì nếu câu lệnh + dispatch_once + static + class method? Nếu bạn đang sử dụng dispatch_once thì tại sao bạn chỉ định tĩnh? – Honey

12

Nếu bất cứ ai quan tâm, đây là một Macro cho cùng một điều:

/*! 
    * @function Singleton GCD Macro 
    */ 
    #ifndef SINGLETON_GCD 
    #define SINGLETON_GCD(classname)       \ 
                   \ 
    + (classname *)shared##classname {       \ 
                   \ 
     static dispatch_once_t pred;       \ 
     static classname * shared##classname = nil;    \ 
     dispatch_once(&pred, ^{        \ 
      shared##classname = [[self alloc] init];   \ 
     });              \ 
     return shared##classname;        \ 
    }               
    #endif 
1

Nếu bất cứ ai quan tâm, đây là một vĩ mô cho điều tương tự :)

IMHO, nó cung cấp sự linh hoạt hơn so với the other variations.

#define SHARED_INSTANCE(...) ({\ 
    static dispatch_once_t pred;\ 
    static id sharedObject;\ 
    dispatch_once(&pred, ^{\ 
     sharedObject = (__VA_ARGS__);\ 
    });\ 
    sharedObject;\ 
}) 

Cách sử dụng, một dòng khởi tạo:

+ (instancetype) sharedInstanceOneLine { 
    return SHARED_INSTANCE([[self alloc] init]); 
} 

Cách sử dụng, đa dòng khởi tạo (thông báo dấu ngoặc nhọn xung quanh khối mã):

+ (instancetype) sharedInstanceMultiLine { 
    return SHARED_INSTANCE({ 
     NSLog(@"creating shared instance"); 
     CGFloat someValue = 84/2.0f; 
     [[self alloc] initWithSomeValue:someValue]; // no return statement 
    }); 
} 

Cách sử dụng ở phần bên phải của một bài tập:

- (void) someMethod { 
    MethodPrivateHelper *helper = SHARED_INSTANCE([[MethodPrivateHelper alloc] init]); 
    // do smth with the helper 
} 
// someMethod should not call itself to avoid deadlock, see bbum's answer 

Sửa đổi này sử dụng hai tính năng ngôn ngữ: phần mở rộng GCC compound expressions, cũng được hỗ trợ bởi Clang và C99 variadic macros support.

Sau khi sơ chế, sản lượng sẽ trông giống như (bạn có thể kiểm tra nó cho mình bằng cách gọi Product > Perform Action > Preprocess "YourClassName.m" trong Xcode 5):

+ (instancetype) sharedInstanceOneLine { 
    return ({ 
     static dispatch_once_t pred; 
     static id sharedObject; 
     dispatch_once(&pred, ^{ 
      sharedObject = ([[self alloc] init]); 
     }); 
     sharedObject; // this object will be returned from the block 
    }); 
} 

+ (instancetype) sharedInstanceMultiLine { 
    return ({ 
     static dispatch_once_t pred; 
     static id sharedObject; 
     dispatch_once(&pred, ^{ 
      sharedObject = ({ 
       NSLog(@"creating shared instance"); 
       CGFloat someValue = 84/2.0f; 
       [[self alloc] initWithSomeValue:someValue]; 
      }); 
     }); 
     sharedObject; 
    }); 
} 

- (void) someMethod { 
    MethodPrivateHelper *helper = ({ 
     static dispatch_once_t pred; 
     static id sharedObject; 
     dispatch_once(&pred, ^{ 
      sharedObject = ([[MethodPrivateHelper alloc] init]); 
     }); 
     sharedObject; 
    }); 
}