Mutex ví dụ / hướng dẫn? [đóng cửa]


176

Tôi chưa quen với đa luồng và đang cố gắng hiểu cách thức hoạt động của mutexes. Đã làm rất nhiều Google nhưng nó vẫn còn một số nghi ngờ về cách thức hoạt động của nó bởi vì tôi đã tạo ra chương trình của riêng mình trong đó khóa không hoạt động.

Một cú pháp hoàn toàn không trực quan của mutex là pthread_mutex_lock( &mutex1 );, trong đó có vẻ như mutex đang bị khóa, khi điều tôi thực sự muốn khóa là một số biến khác. Liệu cú pháp này có nghĩa là khóa một mutex khóa một vùng mã cho đến khi mutex được mở khóa? Sau đó, làm thế nào để chủ đề biết rằng khu vực bị khóa? [ CẬP NHẬT: Chủ đề biết rằng khu vực bị khóa, bởi Hàng rào bộ nhớ ]. Và không phải là một hiện tượng như vậy được gọi là phần quan trọng? [ CẬP NHẬT: Các đối tượng phần quan trọng chỉ có sẵn trong Windows, trong đó các đối tượng nhanh hơn mutexes và chỉ hiển thị đối với luồng thực hiện nó. Mặt khác, phần quan trọng chỉ đề cập đến vùng mã được bảo vệ bởi một mutex ]

Nói tóm lại, bạn có thể vui lòng trợ giúp với chương trình ví dụ mutex đơn giản nhất có thể và giải thích đơn giản nhất có thể về logic của cách thức hoạt động không? Tôi chắc chắn rằng điều này sẽ giúp nhiều người mới khác.


2
Tiếp tục nhấn mạnh vào sự cần thiết của một hướng dẫn đơn giản (có thể là tăng chủ đề, tbb hoặc pthreads): Ví dụ về sự nhầm lẫn: 1. stackoverflow.com/questions / 3528877 / thép 2. stackoverflow.com/questions/2979525/ tựa 3. stackoverflow.com/questions/2095977/to-mutex-or-not-to-mutex 4. stackoverflow.com/questions/39 31026/iêu 5. stackoverflow.com/questions/1525189/ thép
Nav

1
Tôi không có ý xúc phạm điều này, nhưng điều mà bình luận cuối cùng của bạn gợi ý cho tôi là chúng ta cần ít sự tương tự hơn và một lời giải thích kỹ thuật tốt hơn về cách thức hoạt động của một mutex và tại sao chúng ta cần chúng.
San Jacinto

@San: Không có hành vi phạm tội nào :) Nhận xét của tôi chỉ nhằm gợi ý rằng một người mới có thể nhận được lời giải thích ngắn gọn nhất, rõ ràng nhất về các trường hợp đột biến. Nhiều chất tương tự có thể gây nhầm lẫn cho người mới, vì vậy các chất tương tự khác nhau nên được giữ riêng. Toàn bộ lý do để tôi đăng các ques và ans là bởi vì là một người mới, tôi thấy thật khó khăn khi đọc qua các giải thích dài và các mẫu mã. Tôi sẽ không muốn bất cứ ai khác trải qua nỗi đau.
Nav

2
@Cory: Nếu câu trả lời này có thể được cải thiện, tôi rất vui lòng nhận đề xuất của bạn. Tôi rất vui vì nhiều người khác đã tìm thấy nó hữu ích. Nếu nó không giúp bạn, thì cũng có câu trả lời từ những người khác, những người đã chỉ ra các hướng dẫn về mutex khác. Tại sao phải tiêu cực như vậy?
Nav

Câu trả lời:


278

Đây là nỗ lực khiêm tốn của tôi để giải thích khái niệm này cho người mới trên khắp thế giới: (một phiên bản được mã hóa màu trên blog của tôi)

Rất nhiều người chạy đến một buồng điện thoại đơn độc (họ không có điện thoại di động) để nói chuyện với những người thân yêu của họ. Người đầu tiên bắt được tay nắm cửa của gian hàng, là người được phép sử dụng điện thoại. Anh ta phải giữ chặt tay nắm cửa miễn là anh ta sử dụng điện thoại, nếu không người khác sẽ nắm lấy tay cầm, ném anh ta ra và nói chuyện với vợ anh ta :) Không có hệ thống xếp hàng như vậy. Khi người đó kết thúc cuộc gọi, ra khỏi buồng và rời khỏi tay nắm cửa, người tiếp theo nắm tay nắm cửa sẽ được phép sử dụng điện thoại.

Một chủ đề là: Mỗi người
Các mutex là: Các tay nắm cửa
Các khóa là: tay của người
Các tài nguyên là: Chiếc điện thoại

Bất kỳ luồng nào phải thực thi một số dòng mã không được sửa đổi bởi các luồng khác cùng một lúc (sử dụng điện thoại để nói chuyện với vợ), trước tiên phải có được một khóa trên mutex (nắm chặt tay nắm cửa của gian hàng ). Chỉ sau đó, một chủ đề mới có thể chạy các dòng mã đó (thực hiện cuộc gọi điện thoại).

Khi luồng đã thực thi mã đó, nó sẽ giải phóng khóa trên mutex để một luồng khác có thể có được khóa trên mutex (những người khác có thể truy cập vào bốt điện thoại).

[ Khái niệm có một mutex là một chút vô lý khi xem xét truy cập độc quyền trong thế giới thực, nhưng trong thế giới lập trình tôi đoán không có cách nào khác để cho các luồng khác 'thấy' rằng một luồng đã thực thi một số dòng mã. Có các khái niệm về các đột biến đệ quy, v.v., nhưng ví dụ này chỉ nhằm mục đích cho bạn thấy khái niệm cơ bản. Hy vọng ví dụ cho bạn một bức tranh rõ ràng về khái niệm này. ]

Với luồng C ++ 11:

#include <iostream>
#include <thread>
#include <mutex>

std::mutex m;//you can use std::lock_guard if you want to be exception safe
int i = 0;

void makeACallFromPhoneBooth() 
{
    m.lock();//man gets a hold of the phone booth door and locks it. The other men wait outside
      //man happily talks to his wife from now....
      std::cout << i << " Hello Wife" << std::endl;
      i++;//no other thread can access variable i until m.unlock() is called
      //...until now, with no interruption from other men
    m.unlock();//man lets go of the door handle and unlocks the door
}

int main() 
{
    //This is the main crowd of people uninterested in making a phone call

    //man1 leaves the crowd to go to the phone booth
    std::thread man1(makeACallFromPhoneBooth);
    //Although man2 appears to start second, there's a good chance he might
    //reach the phone booth before man1
    std::thread man2(makeACallFromPhoneBooth);
    //And hey, man3 also joined the race to the booth
    std::thread man3(makeACallFromPhoneBooth);

    man1.join();//man1 finished his phone call and joins the crowd
    man2.join();//man2 finished his phone call and joins the crowd
    man3.join();//man3 finished his phone call and joins the crowd
    return 0;
}

Biên dịch và chạy bằng g++ -std=c++0x -pthread -o thread thread.cpp;./thread

Thay vì sử dụng rõ ràng lockunlock, bạn có thể sử dụng dấu ngoặc như hiển thị ở đây , nếu bạn đang sử dụng khóa có phạm vi cho lợi thế mà nó cung cấp . Khóa phạm vi có một hiệu suất trên đầu mặc dù.


2
@San: Tôi sẽ thành thật; Có, tôi rất thích việc bạn đã cố gắng hết sức để giải thích các chi tiết (với dòng chảy) cho một người mới hoàn thành. NHƯNG, (xin đừng hiểu lầm tôi) ý định của bài đăng này là đưa khái niệm này vào một lời giải thích ngắn (vì các câu trả lời khác chỉ vào hướng dẫn dài). Tôi hy vọng bạn sẽ không phiền nếu tôi yêu cầu bạn sao chép toàn bộ câu trả lời của bạn và đăng nó dưới dạng một câu trả lời riêng biệt? Để tôi có thể quay lại và chỉnh sửa câu trả lời của mình để chỉ ra câu trả lời của bạn.
Nav

2
@Tom Trong trường hợp đó, bạn không nên truy cập vào mutex đó. Các thao tác trên nó nên được gói gọn để bất cứ thứ gì nó bảo vệ đều được bảo vệ khỏi các tính năng tomfoolery đó. Nếu khi bạn sử dụng API được hiển thị của thư viện, thư viện được đảm bảo an toàn cho chuỗi, thì bạn có thể bao gồm một mutex khác biệt để bảo vệ các mục được chia sẻ của riêng bạn. Mặt khác, bạn thực sự đang thêm một tay nắm cửa mới, như bạn đã đề xuất.
San Jacinto

2
Để mở rộng quan điểm của tôi, những gì bạn muốn làm là thêm một phòng khác, lớn hơn xung quanh gian hàng. Phòng cũng có thể có một nhà vệ sinh và vòi hoa sen. Hãy nói rằng chỉ có 1 người được phép ở trong phòng cùng một lúc. Bạn phải thiết kế phòng sao cho căn phòng này phải có cửa có tay cầm bảo vệ lối vào phòng giống như buồng điện thoại. Vì vậy, bây giờ, mặc dù bạn có thêm các trường hợp khác, bạn có thể sử dụng lại buồng điện thoại trong bất kỳ dự án nào. Một lựa chọn khác là để lộ các cơ chế khóa cho từng thiết bị trong phòng và quản lý các khóa trong lớp phòng. Dù bằng cách nào, bạn sẽ không thêm khóa mới vào cùng một đối tượng.
San Jacinto

8
Ví dụ luồng C ++ 11 của bạn là sai . TBB cũng vậy, đầu mối nằm trong tên khóa có phạm vi .
Jonathan Wakely 4/10/2015

3
Tôi biết rõ cả hai, @Jonathan. Bạn dường như đã bỏ lỡ câu tôi đã viết (could've shown scoped locking by not using acquire and release - which also is exception safe -, but this is clearer. Đối với việc sử dụng khóa có phạm vi, tùy thuộc vào nhà phát triển, tùy thuộc vào loại ứng dụng họ đang xây dựng. Câu trả lời này nhằm giải quyết sự hiểu biết cơ bản về khái niệm mutex và không đi sâu vào tất cả sự phức tạp của nó, vì vậy các bình luận và liên kết của bạn đều được chào đón nhưng hơi ngoài phạm vi của hướng dẫn này.
Nav

41

Mặc dù một mutex có thể được sử dụng để giải quyết các vấn đề khác, lý do chính mà chúng tồn tại là để cung cấp loại trừ lẫn nhau và do đó giải quyết cái được gọi là điều kiện chủng tộc. Khi hai (hoặc nhiều) chủ đề hoặc quy trình đang cố gắng truy cập cùng một biến đồng thời, chúng ta có tiềm năng cho một điều kiện cuộc đua. Hãy xem xét các mã sau đây

//somewhere long ago, we have i declared as int
void my_concurrently_called_function()
{
  i++;
}

Các phần bên trong của chức năng này trông rất đơn giản. Đó chỉ là một tuyên bố. Tuy nhiên, một ngôn ngữ lắp ráp giả điển hình tương đương có thể là:

load i from memory into a register
add 1 to i
store i back into memory

Bởi vì tất cả các hướng dẫn ngôn ngữ lắp ráp tương đương đều được yêu cầu để thực hiện thao tác tăng trên i, chúng tôi nói rằng tăng i là một hoạt động không phải là đạn. Một hoạt động nguyên tử là một hoạt động có thể được hoàn thành trên phần cứng với một người bảo đảm không bị gián đoạn một khi việc thực hiện lệnh đã bắt đầu. Tăng i bao gồm một chuỗi gồm 3 hướng dẫn nguyên tử. Trong một hệ thống đồng thời trong đó một số luồng đang gọi hàm, các vấn đề phát sinh khi một luồng đọc hoặc ghi không đúng lúc. Hãy tưởng tượng chúng ta có hai luồng chạy simultaneoulsy và một luồng gọi hàm này ngay lập tức. Chúng ta cũng nói rằng chúng ta đã khởi tạo thành 0. Ngoài ra, giả sử rằng chúng ta có nhiều thanh ghi và hai luồng đang sử dụng các thanh ghi hoàn toàn khác nhau, do đó sẽ không có xung đột. Thời gian thực tế của những sự kiện này có thể là:

thread 1 load 0 into register from memory corresponding to i //register is currently 0
thread 1 add 1 to a register //register is now 1, but not memory is 0
thread 2 load 0 into register from memory corresponding to i
thread 2 add 1 to a register //register is now 1, but not memory is 0
thread 1 write register to memory //memory is now 1
thread 2 write register to memory //memory is now 1

Điều xảy ra là chúng tôi có hai luồng tăng dần đồng thời, hàm của chúng tôi được gọi hai lần, nhưng kết quả không phù hợp với thực tế đó. Có vẻ như chức năng chỉ được gọi một lần. Điều này là do tính nguyên tử bị "phá vỡ" ở cấp độ máy, có nghĩa là các luồng có thể làm gián đoạn lẫn nhau hoặc làm việc sai thời điểm.

Chúng ta cần một cơ chế để giải quyết điều này. Chúng ta cần áp đặt một số thứ tự cho các hướng dẫn ở trên. Một cơ chế phổ biến là chặn tất cả các luồng trừ một. Mutex Pthread sử dụng cơ chế này.

Bất kỳ luồng nào phải thực thi một số dòng mã có thể sửa đổi một cách không an toàn các giá trị được chia sẻ bởi các luồng khác cùng một lúc (sử dụng điện thoại để nói chuyện với vợ), trước tiên nên thực hiện khóa trên mutex. Theo cách này, bất kỳ luồng nào yêu cầu quyền truy cập vào dữ liệu được chia sẻ đều phải thông qua khóa mutex. Chỉ sau đó, một chủ đề sẽ có thể thực thi mã. Phần mã này được gọi là phần quan trọng.

Khi luồng đã thực hiện phần quan trọng, nó sẽ giải phóng khóa trên mutex để một luồng khác có thể có được khóa trên mutex.

Khái niệm có một mutex có vẻ hơi kỳ quặc khi xem xét con người tìm kiếm quyền truy cập độc quyền vào các vật thể thực, nhưng khi lập trình, chúng ta phải có chủ ý. Các chủ đề và quy trình đồng thời không có sự giáo dục về văn hóa và xã hội mà chúng ta làm, vì vậy chúng ta phải buộc họ chia sẻ dữ liệu độc đáo.

Về mặt kỹ thuật, làm thế nào để một mutex hoạt động? Nó không phải chịu các điều kiện chủng tộc giống như chúng ta đã đề cập trước đó? Không phải pthread_mutex_lock () phức tạp hơn một chút mà là một bước tăng đơn giản của một biến?

Về mặt kỹ thuật, chúng tôi cần một số hỗ trợ phần cứng để giúp chúng tôi. Các nhà thiết kế phần cứng cung cấp cho chúng tôi các hướng dẫn máy làm nhiều hơn một việc nhưng được đảm bảo là nguyên tử. Một ví dụ kinh điển của một hướng dẫn như vậy là test-and-set (TAS). Khi thử lấy khóa trên tài nguyên, chúng tôi có thể sử dụng TAS có thể kiểm tra xem giá trị trong bộ nhớ có bằng 0. Nếu đó là tín hiệu của chúng tôi rằng tài nguyên đó đang được sử dụng và chúng tôi không làm gì cả (hoặc chính xác hơn , chúng tôi chờ đợi bằng một số cơ chế. Một mutth pthreads sẽ đưa chúng tôi vào một hàng đợi đặc biệt trong hệ điều hành và sẽ thông báo cho chúng tôi khi tài nguyên có sẵn. Các hệ thống Dumber có thể yêu cầu chúng tôi thực hiện một vòng lặp chặt chẽ, kiểm tra tình trạng lặp đi lặp lại) . Nếu giá trị trong bộ nhớ không phải là 0, TAS sẽ đặt vị trí thành một giá trị khác 0 mà không sử dụng bất kỳ hướng dẫn nào khác. Nó ' Giống như kết hợp hai hướng dẫn lắp ráp thành 1 để cung cấp cho chúng ta tính nguyên tử. Do đó, kiểm tra và thay đổi giá trị (nếu thay đổi là phù hợp) không thể bị gián đoạn một khi nó đã bắt đầu. Chúng ta có thể xây dựng các mutexes trên đầu một hướng dẫn như vậy.

Lưu ý: một số phần có thể xuất hiện tương tự như câu trả lời trước đó. Tôi đã chấp nhận lời mời của anh ấy để chỉnh sửa, anh ấy thích cách ban đầu của nó, vì vậy tôi đang giữ những gì tôi có được truyền vào một chút lời nói của anh ấy.


1
Cảm ơn bạn rất nhiều, San. Tôi đã liên kết với câu trả lời của bạn :) Trên thực tế, tôi đã dự định rằng bạn lấy câu trả lời của tôi + câu trả lời của bạn và đăng nó dưới dạng một câu trả lời riêng biệt, để giữ cho dòng chảy. Tôi không thực sự bận tâm nếu bạn sử dụng lại bất kỳ phần nào trong câu trả lời của tôi. Chúng tôi không làm điều này cho chính mình.
Nav

13

Các chủ đề tốt nhất hướng dẫn tôi biết là ở đây:

https://computing.llnl.gov/tutorials/pthreads/

Tôi thích rằng nó được viết về API, hơn là về một triển khai cụ thể và nó đưa ra một số ví dụ đơn giản để giúp bạn hiểu về đồng bộ hóa.


Tôi đồng ý rằng đây chắc chắn là một hướng dẫn tốt, nhưng nó có rất nhiều thông tin trên một trang và các chương trình dài. Câu hỏi tôi đã đăng là phiên bản mutex của bài phát biểu "Tôi có một giấc mơ", nơi những người mới sẽ tìm thấy một cách đơn giản để tìm hiểu về mutexes và hiểu cách hoạt động của cú pháp không trực quan (đây là một lời giải thích thiếu trong tất cả các hướng dẫn) .
Nav

7

Tôi tình cờ thấy bài đăng này gần đây và nghĩ rằng nó cần một giải pháp cập nhật cho mutex c ++ 11 của thư viện chuẩn (cụ thể là std :: mutex).

Tôi đã dán một số mã bên dưới (các bước đầu tiên của tôi với một mutex - Tôi đã học được sự tương tranh trên win32 với HANDLE, SetEvent, WaitForMult MônObjects, v.v.).

Vì đó là nỗ lực đầu tiên của tôi với std :: mutex và bạn bè, tôi rất thích xem các bình luận, đề xuất và cải tiến!

#include <condition_variable>
#include <mutex>
#include <algorithm>
#include <thread>
#include <queue>
#include <chrono>
#include <iostream>


int _tmain(int argc, _TCHAR* argv[])
{   
    // these vars are shared among the following threads
    std::queue<unsigned int>    nNumbers;

    std::mutex                  mtxQueue;
    std::condition_variable     cvQueue;
    bool                        m_bQueueLocked = false;

    std::mutex                  mtxQuit;
    std::condition_variable     cvQuit;
    bool                        m_bQuit = false;


    std::thread thrQuit(
        [&]()
        {
            using namespace std;            

            this_thread::sleep_for(chrono::seconds(5));

            // set event by setting the bool variable to true
            // then notifying via the condition variable
            m_bQuit = true;
            cvQuit.notify_all();
        }
    );


    std::thread thrProducer(
        [&]()
        {
            using namespace std;

            int nNum = 13;
            unique_lock<mutex> lock( mtxQuit );

            while ( ! m_bQuit )
            {
                while( cvQuit.wait_for( lock, chrono::milliseconds(75) ) == cv_status::timeout )
                {
                    nNum = nNum + 13 / 2;

                    unique_lock<mutex> qLock(mtxQueue);
                    cout << "Produced: " << nNum << "\n";
                    nNumbers.push( nNum );
                }
            }
        }   
    );

    std::thread thrConsumer(
        [&]()
        {
            using namespace std;
            unique_lock<mutex> lock(mtxQuit);

            while( cvQuit.wait_for(lock, chrono::milliseconds(150)) == cv_status::timeout )
            {
                unique_lock<mutex> qLock(mtxQueue);
                if( nNumbers.size() > 0 )
                {
                    cout << "Consumed: " << nNumbers.front() << "\n";
                    nNumbers.pop();
                }               
            }
        }
    );

    thrQuit.join();
    thrProducer.join();
    thrConsumer.join();

    return 0;
}

1
Siêu! Cảm ơn vì đăng. Mặc dù như tôi đã đề cập trước đây, mục đích của tôi chỉ đơn giản là giải thích khái niệm về một mutex. Tất cả các hướng dẫn khác làm cho nó rất khó khăn với các khái niệm bổ sung về người tiêu dùng sản xuất và các biến điều kiện, v.v., điều này khiến tôi rất khó hiểu những gì trên trái đất đang diễn ra.
Nav

4

Hàm này pthread_mutex_lock()được mutex cho luồng gọi hoặc chặn luồng cho đến khi mutex có thể được lấy. Các liên quan pthread_mutex_unlock()phát hành mutex.

Hãy nghĩ về mutex như một hàng đợi; mọi luồng cố gắng để có được mutex sẽ được đặt ở cuối hàng đợi. Khi một luồng giải phóng mutex, luồng tiếp theo trong hàng đợi sẽ tắt và hiện đang chạy.

Một phần quan trọng đề cập đến một vùng mã nơi không thể xác định được. Thường điều này là do nhiều luồng đang cố gắng truy cập vào một biến được chia sẻ. Phần quan trọng không an toàn cho đến khi một số loại đồng bộ hóa được đưa ra. Khóa mutex là một hình thức đồng bộ hóa.


1
Có đảm bảo rằng chính xác các chủ đề cố gắng tiếp theo sẽ nhập?
Asen Mkrtchyan

1
@Arsen Không bảo đảm. Nó chỉ là một sự tương tự hữu ích.
chrisaycock

3

Bạn phải kiểm tra biến mutex trước khi sử dụng vùng được bảo vệ bởi mutex. Vì vậy, pthread_mutex_lock () của bạn có thể (tùy thuộc vào việc triển khai) đợi cho đến khi mutex1 được phát hành hoặc trả về một giá trị cho biết rằng không thể lấy được khóa nếu người khác đã khóa nó.

Mutex thực sự chỉ là một semaphore đơn giản hóa. Nếu bạn đọc về họ và hiểu họ, bạn sẽ hiểu về mutexes. Có một số câu hỏi liên quan đến mutexes và semaphores trong SO. Sự khác biệt giữa semaphore nhị phân và mutex , Khi nào chúng ta nên sử dụng mutex và khi nào chúng ta nên sử dụng semaphore , v.v. Ví dụ về nhà vệ sinh trong liên kết đầu tiên là một ví dụ tốt như người ta có thể nghĩ đến. Tất cả các mã làm là để kiểm tra nếu khóa có sẵn và nếu có, hãy bảo lưu nó. Lưu ý rằng bạn không thực sự dự trữ nhà vệ sinh, nhưng chìa khóa.


1
pthread_mutex_lockkhông thể quay lại nếu người khác giữ khóa. Nó chặn trong trường hợp này và đó là toàn bộ vấn đề. pthread_mutex_trylocklà chức năng sẽ trở lại nếu khóa được giữ.
R .. GitHub DỪNG GIÚP ICE

1
Vâng, ban đầu tôi không nhận ra đây là gì.
Makis

3

Đối với những người tìm kiếm ví dụ mutex shortex:

#include <mutex>

int main() {
    std::mutex m;

    m.lock();
    // do thread-safe stuff
    m.unlock();
}

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.