5

Vì vậy, tôi đã nhìn thấy rất nhiều bài viết bây giờ tuyên bố rằng trên C++ đôi kiểm tra khóa, thường được sử dụng để ngăn chặn nhiều chủ đề từ cố gắng để khởi tạo một singleton lười biếng tạo ra, bị hỏng. Bình thường đôi đang khóa kiểm tra lần đọc như thế này:Có vấn đề gì với việc sửa chữa khóa kép này?

class singleton { 
private: 
    singleton(); // private constructor so users must call instance() 
    static boost::mutex _init_mutex; 

public: 
    static singleton & instance() 
    { 
     static singleton* instance; 

     if(!instance) 
     { 
      boost::mutex::scoped_lock lock(_init_mutex); 

      if(!instance)   
       instance = new singleton; 
     } 

     return *instance; 
    } 
}; 

Vấn đề rõ ràng là dòng gán dụ - trình biên dịch là miễn phí để phân bổ các đối tượng và sau đó gán con trỏ đến nó, OR để thiết lập con trỏ đến nơi nó sẽ được phân bổ, sau đó phân bổ nó. Trường hợp thứ hai phá vỡ thành ngữ - một luồng có thể cấp phát bộ nhớ và gán con trỏ nhưng không chạy hàm tạo của singleton trước khi nó được đưa vào trạng thái ngủ - sau đó luồng thứ hai sẽ thấy rằng cá thể không phải là rỗng và cố gắng trả về nó , mặc dù nó vẫn chưa được xây dựng.

I saw a suggestion để sử dụng chỉ số boolean cục bộ và kiểm tra thay vì instance. Một cái gì đó như thế này:

class singleton { 
private: 
    singleton(); // private constructor so users must call instance() 
    static boost::mutex _init_mutex; 
    static boost::thread_specific_ptr<int> _sync_check; 

public: 
    static singleton & instance() 
    { 
     static singleton* instance; 

     if(!_sync_check.get()) 
     { 
      boost::mutex::scoped_lock lock(_init_mutex); 

      if(!instance)   
       instance = new singleton; 

      // Any non-null value would work, we're really just using it as a 
      // thread specific bool. 
      _sync_check = reinterpret_cast<int*>(1); 
     } 

     return *instance; 
    } 
}; 

Bằng cách này mỗi thread kết thúc kiểm tra nếu các trường hợp đã được tạo ra một lần, nhưng dừng lại sau đó, mà đòi hỏi phải thực hiện một số hit nhưng vẫn gần như không xấu như khóa mỗi cuộc gọi. Nhưng điều gì xảy ra nếu chúng ta vừa sử dụng một bool tĩnh cục bộ ?:

class singleton { 
private: 
    singleton(); // private constructor so users must call instance() 
    static boost::mutex _init_mutex; 

public: 
    static singleton & instance() 
    { 
     static bool sync_check = false; 
     static singleton* instance; 

     if(!sync_check) 
     { 
      boost::mutex::scoped_lock lock(_init_mutex); 

      if(!instance)   
       instance = new singleton; 

      sync_check = true; 
     } 

     return *instance; 
    } 
}; 

Tại sao nó không hoạt động? Ngay cả khi sync_check đã được đọc bởi một sợi khi nó được gán trong một giá trị rác khác vẫn sẽ là nonzero và do đó đúng. This Dr. Dobb's article tuyên bố rằng bạn phải khóa vì bạn sẽ không bao giờ thắng một trận chiến với trình biên dịch theo hướng dẫn sắp xếp lại. Điều này khiến tôi nghĩ rằng điều này không phải vì lý do nào đó, nhưng tôi không thể hiểu tại sao. Nếu các yêu cầu về các điểm chuỗi bị mất khi bài viết của Tiến sĩ Dobb khiến tôi tin, tôi không hiểu tại sao bất kỳ mã nào sau khi khóa không thể sắp xếp lại trước khi khóa. Mà sẽ làm cho C + + đa luồng thời gian bị hỏng.

Tôi đoán tôi có thể thấy trình biên dịch được cho phép đặc biệt sắp xếp lại sync_check trước khóa bởi vì đó là biến cục bộ (và mặc dù tĩnh chúng tôi không trả về tham chiếu hoặc con trỏ tới nó) - nhưng sau đó vẫn có thể được giải quyết bằng cách biến nó thành một thành viên tĩnh (có hiệu quả toàn cầu) thay thế.

Điều này có hiệu quả hay không? Tại sao?

+2

Vấn đề là biến có thể được gán trước khi hàm tạo đang chạy (hoặc hoàn thành), không phải trước khi đối tượng được cấp phát. – kdgregory

+0

Cảm ơn, đã sửa. Tôi đã hoàn toàn sai lầm về điều kiện chủng tộc. –

+1

Vâng, bạn đã đúng, hiện tại C++ thực sự là "đa luồng thời gian bị hỏng". khi chỉ xem xét tiêu chuẩn. Các nhà cung cấp trình biên dịch thường cung cấp các cách xung quanh điều này mặc dù, do đó các kết quả thực tế không phải là khủng khiếp. – Suma

Trả lời

5

Khắc phục của bạn không khắc phục được bất cứ điều gì vì ghi vào sync_check và thể hiện có thể được thực hiện theo thứ tự trên CPU. Ví dụ, hãy tưởng tượng hai cuộc gọi đầu tiên đến ví dụ xảy ra cùng khoảng thời gian trên hai CPU khác nhau. Luồng đầu tiên sẽ lấy khóa, khởi tạo con trỏ và đặt sync_check thành true, theo thứ tự đó, nhưng bộ vi xử lý có thể thay đổi thứ tự của ghi vào bộ nhớ. Trên CPU kia thì có thể cho thread thứ hai kiểm tra sync_check, thấy rằng nó là true, nhưng instance có thể chưa được ghi vào bộ nhớ. Xem Lockless Programming Considerations for Xbox 360 and Microsoft Windows để biết chi tiết.

Giải pháp sync_check cụ thể của chủ đề mà bạn đề cập sẽ hoạt động sau đó (giả sử bạn khởi tạo con trỏ đến 0).

+0

Về câu cuối cùng của bạn: Có nhưng, tôi không chắc chắn nhưng tôi nghĩ rằng thread_specific_ptr sử dụng một mutex nội bộ. Vì vậy, những gì sẽ là điểm của việc sử dụng giải pháp đó so với chỉ luôn luôn khóa mutex (không có khóa kép)? – n1ckp

1

Có một số đọc lớn về vấn đề này (mặc dù nó .net/C# hướng) ở đây: http://msdn.microsoft.com/en-us/magazine/cc163715.aspx

gì nó boils xuống là bạn cần để có thể nói với CPU mà nó không thể sắp xếp lại của bạn đọc/viết cho truy cập biến này (kể từ Pentium ban đầu, CPU có thể sắp xếp lại các lệnh nhất định nếu nó cho rằng logic sẽ không bị ảnh hưởng), và rằng nó cần đảm bảo rằng cache nhất quán (đừng quên điều đó - chúng ta devs giả sử rằng tất cả bộ nhớ chỉ là một tài nguyên phẳng, nhưng trên thực tế, mỗi lõi CPU có cache, một số không được chia sẻ (L1), một số có thể được chia sẻ đôi khi (L2)) - initizlization của bạn có thể ghi vào RAM chính, nhưng lõi khác có thể có giá trị chưa được khởi tạo trong bộ nhớ cache. Nếu bạn không có bất kỳ ngữ nghĩa đồng thời nào, CPU có thể không biết rằng cache của nó bị bẩn.

Tôi không biết mặt C++, nhưng trong .net, bạn sẽ chỉ định biến là dễ bay hơi để bảo vệ quyền truy cập vào biến đó (hoặc bạn sẽ sử dụng phương thức đọc/ghi bộ nhớ trong System.Threading).

Ngoài ra, tôi đã đọc trong .net 2.0, kiểm tra khóa kép được đảm bảo hoạt động mà không có biến "dễ bay hơi" (đối với bất kỳ trình đọc .net nào) - điều đó không giúp bạn với C++ mã.

Nếu bạn muốn an toàn, bạn sẽ cần phải làm tương đương C++ để đánh dấu biến là biến động trong C#.

+1

C++ biến có thể được khai báo là dễ bay hơi, nhưng tôi nghi ngờ điều này có cùng ngữ nghĩa giống như C#. Tôi cũng nhớ đọc ở đâu đó rằng đây là một sự lạm dụng dễ bay hơi nhưng tôi không nhớ tại sao tôi không thể đánh giá được bài báo lý luận như thế nào. –

+0

Trong các ngôn ngữ khác nhau, nó có thể là một sự lạm dụng (thậm chí có thể là một sự lạm dụng trong C#). Một trong những khía cạnh thực sự khó khăn của việc viết mã khóa thấp hoặc khóa miễn phí là sự khác biệt trong hướng dẫn. Tôi đã dành thời gian đọc về nó, và có vẻ như ngay cả trong Microsoft, một số blogger dường như mâu thuẫn với nhau khi bạn cần một hàng rào bộ nhớ, và khi bạn nên sử dụng dễ bay hơi. Đó là một vấn đề khó khăn, để chắc chắn. – JMarsch

+0

Không có tương đương với .NET bay hơi trong C++ hiện tại (như được định nghĩa theo chuẩn). Đây là một trong những lĩnh vực chuẩn C++ 0x sắp ra mắt sẽ mang lại. Trong khi đó bạn cần phải sử dụng những gì complier của bạn cung cấp (trong Visual Studio có nghĩa là hàng rào dễ bay hơi và bộ nhớ). – Suma

0

"Trường hợp thứ hai phá vỡ thành ngữ - hai chuỗi có thể kết thúc việc tạo singleton." Nhưng nếu tôi hiểu mã một cách chính xác, ví dụ đầu tiên, bạn kiểm tra xem trường hợp đã tồn tại (có thể được thực hiện bởi nhiều chủ đề cùng một lúc), nếu nó không có một chủ đề để khóa nó và nó tạo ra Ví dụ - chỉ một luồng có thể thực thi việc tạo tại thời điểm đó. Tất cả các chủ đề khác bị khóa và sẽ đợi.

Khi thể hiện được tạo và mutex được mở khóa, chuỗi chờ tiếp theo sẽ khóa mutex nhưng nó sẽ không cố gắng tạo phiên bản mới vì kiểm tra sẽ thất bại.

Lần tới, biến mẫu được kiểm tra, nó sẽ được đặt để không có chủ đề nào sẽ cố gắng tạo phiên bản mới.

Tôi không chắc chắn về trường hợp một sợi được gán con trỏ dụ mới cho ví dụ trong khi một chuỗi khác kiểm tra cùng một biến - nhưng tôi tin rằng nó sẽ được xử lý chính xác trong trường hợp này.

Tôi có thiếu gì đó ở đây không? Bạn không chắc chắn về việc sắp xếp lại các hoạt động nhưng trong trường hợp này nó sẽ thay đổi logic vì vậy tôi sẽ không mong đợi nó xảy ra - nhưng tôi không có chuyên gia về chủ đề này.

+0

Bạn nói đúng - tôi đã sai về điều kiện chủng tộc thực tế. Vấn đề là một thread thứ hai có thể thấy instance là non-null và cố gắng trả về nó trước khi thread đầu tiên đã xây dựng nó. Tôi đã chỉnh sửa bài đăng của mình. –

Các vấn đề liên quan