std :: unique_lock <std :: mutex> hoặc std :: lock_guard <std :: mutex>?


348

Tôi có hai trường hợp sử dụng.

A. Tôi muốn đồng bộ hóa truy cập bằng hai luồng vào hàng đợi.

B. Tôi muốn đồng bộ hóa quyền truy cập của hai luồng vào hàng đợi và sử dụng biến điều kiện vì một trong các luồng sẽ chờ nội dung được lưu vào hàng đợi bởi luồng khác.

Đối với trường hợp sử dụng AI xem ví dụ mã bằng cách sử dụng std::lock_guard<>. Đối với trường hợp sử dụng BI xem ví dụ mã sử dụng std::unique_lock<>.

Sự khác biệt giữa hai và tôi nên sử dụng cái nào trong trường hợp sử dụng?

Câu trả lời:


344

Sự khác biệt là bạn có thể khóa và mở khóa a std::unique_lock. std::lock_guardsẽ chỉ bị khóa một lần khi xây dựng và mở khóa khi phá hủy.

Vì vậy, đối với trường hợp sử dụng B, bạn chắc chắn cần một std::unique_lockbiến điều kiện. Trong trường hợp A, nó phụ thuộc vào việc bạn có cần phải khóa lại người bảo vệ hay không.

std::unique_lockcó các tính năng khác cho phép nó, ví dụ: được xây dựng mà không khóa mutex ngay lập tức nhưng để xây dựng trình bao bọc RAII (xem tại đây ).

std::lock_guardcũng cung cấp trình bao bọc RAII thuận tiện, nhưng không thể khóa nhiều mutexes một cách an toàn. Nó có thể được sử dụng khi bạn cần một trình bao bọc cho phạm vi giới hạn, ví dụ: hàm thành viên:

class MyClass{
    std::mutex my_mutex;
    void member_foo() {
        std::lock_guard<mutex_type> lock(this->my_mutex);            
        /*
         block of code which needs mutual exclusion (e.g. open the same 
         file in multiple threads).
        */

        //mutex is automatically released when lock goes out of scope           
};

Để làm rõ một câu hỏi bằng chmike, theo mặc định std::lock_guardstd::unique_lockgiống nhau. Vì vậy, trong trường hợp trên, bạn có thể thay thế std::lock_guardbằng std::unique_lock. Tuy nhiên, std::unique_lockcó thể có một chút chi phí.

Lưu ý rằng những ngày này nên sử dụng std::scoped_lockthay vì std::lock_guard.


2
Với hướng dẫn std :: unique_lock <std :: mutex> lock (myMutex); mutex sẽ bị khóa bởi nhà xây dựng?
chmike

3
@chmike Vâng, nó sẽ. Đã thêm một số làm rõ.
Stephan Dollberg

10
@chmike Vâng, tôi nghĩ đó không phải là một câu hỏi về hiệu quả hơn chức năng. Nếu std::lock_guardđủ cho trường hợp của bạn A, thì bạn nên sử dụng nó. Nó không chỉ tránh được chi phí không cần thiết mà còn thể hiện ý định với người đọc rằng bạn sẽ không bao giờ mở khóa bảo vệ này.
Stephan Dollberg

5
@chmike: Về mặt lý thuyết là có. Tuy nhiên, Mutice không phải là cấu trúc nhẹ chính xác, do đó, chi phí bổ sung unique_lockcó thể bị giảm do chi phí thực sự khóa và mở khóa mutex (nếu trình biên dịch không tối ưu hóa chi phí đó, điều đó có thể xảy ra).
Grizzly

6
So for usecase B you definitely need a std::unique_lock for the condition variable- có nhưng chỉ trong luồng đó cv.wait(), bởi vì phương thức đó nguyên tử giải phóng mutex. Trong luồng khác nơi bạn cập nhật (các) biến được chia sẻ và sau đó gọi cv.notify_one(), một điều đơn giản là lock_guardđủ để khóa mutex trong phạm vi ... trừ khi bạn làm bất cứ điều gì phức tạp hơn mà tôi không thể tưởng tượng được! ví dụ: en.cppreference.com/w/cpp/thread/condition_variable - hoạt động với tôi :)
underscore_d

114

lock_guardunique_lockkhá nhiều điều tương tự; lock_guardlà phiên bản giới hạn với giao diện giới hạn.

A lock_guardluôn luôn giữ một khóa từ xây dựng của nó để phá hủy nó. A unique_lockcó thể được tạo mà không cần khóa ngay lập tức, có thể mở khóa tại bất kỳ điểm nào trong sự tồn tại của nó và có thể chuyển quyền sở hữu khóa từ phiên bản này sang phiên bản khác.

Vì vậy, bạn luôn luôn sử dụng lock_guard, trừ khi bạn cần các khả năng của unique_lock. A condition_variablecần a unique_lock.


11
A condition_variable needs a unique_lock.- có nhưng chỉ ở phía wait()ing, như được nêu trong nhận xét của tôi cho inf.
gạch dưới

48

Sử dụng lock_guardtrừ khi bạn cần có khả năng thủ công unlockmutex ở giữa mà không phá hủy lock.

Đặc biệt, condition_variablemở khóa mutex của nó khi đi ngủ khi có cuộc gọi đến wait. Đó là lý do tại sao a lock_guardkhông đủ ở đây.


Truyền khóa lock_guard cho một trong các phương thức chờ của biến có điều kiện sẽ ổn vì mutex luôn được phản hồi khi chờ đợi kết thúc, vì bất kỳ lý do gì. Tuy nhiên, tiêu chuẩn chỉ cung cấp một giao diện cho unique_lock. Điều này có thể được coi là một thiếu sót trong tiêu chuẩn.
Chris Vine

3
@Chris Bạn vẫn sẽ phá vỡ đóng gói trong trường hợp này. Phương thức chờ sẽ cần để có thể trích xuất mutex từ lock_guardvà mở khóa nó, do đó tạm thời phá vỡ bất biến lớp của người bảo vệ. Mặc dù điều này xảy ra vô hình với người dùng, tôi sẽ coi đó là một lý do chính đáng để không cho phép sử dụng lock_guardtrong trường hợp này.
ComicSansMS

Nếu vậy, nó sẽ vô hình và không thể phát hiện. gcc-4.8 làm điều đó. chờ đợi /gthr-poseix.h). Điều tương tự cũng có thể được thực hiện đối với lock_guard (nhưng không phải vì nó không nằm trong tiêu chuẩn cho condition_variable).
Chris Vine

4
@Chris Điểm lock_guardnày không cho phép truy xuất mutex cơ bản nào cả. Đây là một giới hạn có chủ ý để cho phép suy luận đơn giản hơn về mã sử dụng lock_guardtrái ngược với mã sử dụng a unique_lock. Cách duy nhất để đạt được những gì bạn yêu cầu là cố tình phá vỡ sự đóng gói của lock_guardlớp và phơi bày việc thực hiện nó với một lớp khác (trong trường hợp này là condition_variable). Đây là một mức giá khó trả cho lợi thế đáng ngờ của người dùng về một biến điều kiện không phải nhớ sự khác biệt giữa hai loại khóa.
ComicSansMS

4
@Chris Bạn lấy ý tưởng ở đâu condition_variable_any.waitđể làm việc với lock_guard? Tiêu chuẩn yêu cầu loại Khóa được cung cấp để đáp ứng BasicLockableyêu cầu (§30.5.2), lock_guardkhông có. Chỉ có mutex cơ bản của nó làm, nhưng vì lý do tôi đã chỉ ra trước đó giao diện của lock_guardkhông cung cấp quyền truy cập vào mutex.
ComicSansMS

11

Có những điều phổ biến nhất định giữa lock_guardunique_locksự khác biệt nhất định.

Nhưng trong ngữ cảnh của câu hỏi, trình biên dịch không cho phép sử dụng lock_guardkết hợp với biến điều kiện, bởi vì khi một luồng xử lý chờ trên một biến điều kiện, mutex sẽ được mở khóa tự động và khi các luồng / luồng khác thông báo và luồng hiện tại được gọi (ra khỏi chờ), khóa được mua lại.

Hiện tượng này là trái với nguyên tắc lock_guard. lock_guardchỉ có thể được xây dựng một lần và phá hủy chỉ một lần.

Do đó lock_guardkhông thể được sử dụng kết hợp với một biến điều kiện, nhưng unique_lockcó thể được (vì unique_lockcó thể bị khóa và mở khóa nhiều lần).


5
he compiler does not allow using a lock_guard in combination with a condition variableĐiều này là sai. Nó chắc chắn không cho phép và làm việc một cách hoàn hảo với một lock_guardtrên notify()mặt ing. Chỉ có phía wait()int yêu cầu a unique_lock, vì wait()phải giải phóng khóa trong khi kiểm tra điều kiện.
gạch dưới

0

Chúng không thực sự giống nhau, lock_guard<muType>gần giống nhau std::mutex, với một điểm khác biệt là thời gian tồn tại của nó kết thúc ở cuối phạm vi (được gọi là D-tor) nên một định nghĩa rõ ràng về hai trường hợp này:

lock_guard<muType> có một cơ chế để sở hữu một mutex trong suốt thời gian của một khối phạm vi.

unique_lock<muType> là một trình bao bọc cho phép khóa hoãn lại, các nỗ lực hạn chế thời gian trong việc khóa, khóa đệ quy, chuyển quyền sở hữu khóa và sử dụng với các biến điều kiện.

Dưới đây là một ví dụ ẩn dụ:

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <functional>
#include <chrono>

using namespace std::chrono;

class Product{

   public:

       Product(int data):mdata(data){
       }

       virtual~Product(){
       }

       bool isReady(){
       return flag;
       }

       void showData(){

        std::cout<<mdata<<std::endl;
       }

       void read(){

         std::this_thread::sleep_for(milliseconds(2000));

         std::lock_guard<std::mutex> guard(mmutex);

         flag = true;

         std::cout<<"Data is ready"<<std::endl;

         cvar.notify_one();

       }

       void task(){

       std::unique_lock<std::mutex> lock(mmutex);

       cvar.wait(lock, [&, this]() mutable throw() -> bool{ return this->isReady(); });

       mdata+=1;

       }

   protected:

    std::condition_variable cvar;
    std::mutex mmutex;
    int mdata;
    bool flag = false;

};

int main(){

     int a = 0;
     Product product(a);

     std::thread reading(product.read, &product);
     std::thread setting(product.task, &product);

     reading.join();
     setting.join();


     product.showData();
    return 0;
}

Trong ví dụ này, tôi đã sử dụng unique_lock<muType>vớicondition variable


-5

Như đã được đề cập bởi những người khác, std :: unique_lock theo dõi trạng thái bị khóa của mutex, vì vậy bạn có thể trì hoãn khóa cho đến sau khi xây dựng khóa và mở khóa trước khi phá hủy khóa. std :: lock_guard không cho phép điều này.

Dường như không có lý do tại sao các hàm chờ std :: condition_variable không nên lấy lock_guard cũng như unique_lock, bởi vì bất cứ khi nào chờ đợi kết thúc (vì bất kỳ lý do gì), mutex sẽ tự động được phản hồi để không gây ra bất kỳ vi phạm ngữ nghĩa nào. Tuy nhiên, theo tiêu chuẩn, để sử dụng std :: lock_guard với biến điều kiện, bạn phải sử dụng std :: condition_variable_any thay vì std :: condition_variable.

Chỉnh sửa : đã xóa "Sử dụng giao diện pthreads std :: condition_variable và std :: condition_variable_any phải giống hệt nhau". Nhìn vào việc thực hiện của gcc:

  • std :: condition_variable :: Wait (std :: unique_lock &) chỉ gọi pthread_cond_wait () trên biến điều kiện pthread bên dưới đối với mutex được giữ bởi unique_lock (và do đó cũng có thể làm như vậy đối với lock_guard, nhưng không phải vì tiêu chuẩn không cung cấp cho điều đó)
  • std :: condition_variable_any có thể hoạt động với bất kỳ đối tượng có thể khóa nào, bao gồm cả đối tượng không phải là khóa mutex (do đó, nó thậm chí có thể hoạt động với một semaphore liên tiến trình)
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.