Khóa đệ quy (Mutex) so với Khóa không đệ quy (Mutex)


182

POSIX cho phép mutexes được đệ quy. Điều đó có nghĩa là cùng một chủ đề có thể khóa cùng một mutex hai lần và sẽ không bế tắc. Tất nhiên nó cũng cần phải mở khóa nó hai lần, nếu không thì không có chủ đề nào khác có thể có được mutex. Không phải tất cả các hệ thống hỗ trợ pthread cũng hỗ trợ các biến đổi đệ quy, nhưng nếu chúng muốn tuân thủ POSIX, chúng phải như vậy .

Các API khác (API cấp cao hơn) cũng thường cung cấp các mutexes, thường được gọi là Khóa. Một số hệ thống / ngôn ngữ (ví dụ: Mục tiêu ca cao-C) cung cấp cả hai, các đột biến đệ quy và không đệ quy. Một số ngôn ngữ cũng chỉ cung cấp một hoặc một ngôn ngữ khác. Ví dụ, trong các mutex Java luôn được đệ quy (cùng một luồng có thể hai lần "đồng bộ hóa" trên cùng một đối tượng). Tùy thuộc vào chức năng chủ đề nào khác mà họ cung cấp, việc không có các biến đổi đệ quy có thể không có vấn đề gì, vì chúng có thể dễ dàng được viết cho chính bạn (Tôi đã tự thực hiện các biến đổi đệ quy trên cơ sở các hoạt động đột biến / điều kiện đơn giản hơn).

Điều tôi không thực sự hiểu: Đột biến không đệ quy tốt cho cái gì? Tại sao tôi muốn có một bế tắc luồng nếu nó khóa cùng một mutex hai lần? Ngay cả các ngôn ngữ cấp cao có thể tránh điều đó (ví dụ: kiểm tra nếu điều này sẽ bế tắc và ném ngoại lệ nếu có) thường không làm điều đó. Họ sẽ để cho bế tắc thay thế.

Có phải điều này chỉ dành cho các trường hợp, trong đó tôi vô tình khóa nó hai lần và chỉ mở khóa một lần và trong trường hợp đột biến đệ quy, sẽ khó tìm ra vấn đề hơn, vì vậy thay vào đó tôi có bế tắc ngay lập tức để xem khóa không chính xác xuất hiện ở đâu? Nhưng tôi không thể làm điều tương tự với việc trả lại bộ đếm khóa khi mở khóa và trong tình huống, tôi chắc chắn rằng tôi đã phát hành khóa cuối cùng và bộ đếm không bằng 0, tôi có thể ném ngoại lệ hoặc ghi lại sự cố không? Hoặc có trường hợp sử dụng nào khác hữu ích hơn cho các trường hợp đột biến không đệ quy mà tôi không thấy? Hoặc có thể chỉ là hiệu suất, vì một mutex không đệ quy có thể nhanh hơn một chút so với một đệ quy? Tuy nhiên, tôi đã thử nghiệm điều này và sự khác biệt thực sự không lớn.

Câu trả lời:


151

Sự khác biệt giữa một mutex đệ quy và không đệ quy có liên quan đến quyền sở hữu. Trong trường hợp của mutex đệ quy, kernel phải theo dõi luồng đã thực sự thu được mutex lần đầu tiên để nó có thể phát hiện sự khác biệt giữa đệ quy so với một luồng khác nên chặn. Như một câu trả lời khác đã chỉ ra, có một câu hỏi về chi phí bổ sung của cả hai về bộ nhớ để lưu trữ bối cảnh này và cả các chu trình cần thiết để duy trì nó.

Tuy nhiên , có những cân nhắc khác khi chơi ở đây quá.

Bởi vì mutex đệ quy có ý thức về quyền sở hữu, nên chủ đề lấy mutex phải là cùng một chủ đề giải phóng mutex. Trong trường hợp các mutexes không đệ quy, không có ý thức về quyền sở hữu và bất kỳ luồng nào thường có thể giải phóng mutex bất kể luồng nào ban đầu lấy mutex. Trong nhiều trường hợp, loại "mutex" này thực sự là một hành động semaphore, trong đó bạn không nhất thiết phải sử dụng mutex như một thiết bị loại trừ mà sử dụng nó như một thiết bị đồng bộ hóa hoặc báo hiệu giữa hai hoặc nhiều luồng.

Một tài sản khác đi kèm với ý thức sở hữu trong một mutex là khả năng hỗ trợ thừa kế ưu tiên. Vì hạt nhân có thể theo dõi luồng sở hữu mutex và cả danh tính của tất cả các trình chặn, nên trong một hệ thống luồng ưu tiên, có thể nâng mức độ ưu tiên của luồng hiện đang sở hữu mutex lên mức ưu tiên của luồng ưu tiên cao nhất hiện đang chặn trên mutex. Sự kế thừa này ngăn ngừa vấn đề đảo ngược ưu tiên có thể xảy ra trong những trường hợp như vậy. (Lưu ý rằng không phải tất cả các hệ thống đều hỗ trợ kế thừa ưu tiên trên các trường hợp như vậy, nhưng đó là một tính năng khác có thể thực hiện được thông qua khái niệm quyền sở hữu).

Nếu bạn tham khảo hạt nhân VxWorks RTOS cổ điển, họ xác định ba cơ chế:

  • đột biến - hỗ trợ đệ quy và kế thừa ưu tiên tùy chọn. Cơ chế này thường được sử dụng để bảo vệ các phần quan trọng của dữ liệu theo cách mạch lạc.
  • semaphore nhị phân - không đệ quy, không thừa kế, loại trừ đơn giản, người nhận và người cho không phải là cùng một chủ đề, phát hành phát sóng có sẵn. Cơ chế này có thể được sử dụng để bảo vệ các phần quan trọng, nhưng cũng đặc biệt hữu ích cho việc truyền tín hiệu hoặc đồng bộ hóa giữa các luồng.
  • đếm semaphore - không có đệ quy hoặc thừa kế, hoạt động như một bộ đếm tài nguyên mạch lạc từ bất kỳ số đếm ban đầu mong muốn nào, các luồng chỉ chặn trong đó số đếm ròng đối với tài nguyên bằng không.

Một lần nữa, điều này thay đổi phần nào theo nền tảng - đặc biệt là những gì họ gọi là những thứ này, nhưng điều này nên đại diện cho các khái niệm và các cơ chế khác nhau đang chơi.


9
lời giải thích của bạn về mutex không đệ quy nghe có vẻ giống như một semaphore. Một mutex (cho dù đệ quy hay không đệ quy) có một khái niệm về quyền sở hữu.
Jay D

@JayD Thật khó hiểu khi mọi người tranh luận về những thứ như thế này .. vậy ai là thực thể định nghĩa những thứ này?
Pacerier

11
@Pacerier Các tiêu chuẩn có liên quan. Câu trả lời này là sai đối với posix (pthreads), trong đó việc mở khóa một mutex bình thường trong một luồng khác với luồng đã khóa nó là hành vi không xác định, trong khi thực hiện tương tự với kiểm tra lỗi hoặc mutex đệ quy dẫn đến mã lỗi có thể dự đoán được. Các hệ thống và tiêu chuẩn khác có thể hành xử rất khác nhau.
số

Có lẽ điều này là ngây thơ, nhưng tôi đã có ấn tượng rằng ý tưởng trung tâm của một mutex là chủ đề khóa mở khóa mutex và sau đó các chủ đề khác có thể làm tương tự. Từ tính
toán.llnl.gov/tutorials/pthreads

1
@cquilguy - một bản phát hành phát sóng giải phóng bất kỳ và tất cả các luồng bị chặn trên semaphore mà không cung cấp rõ ràng (vẫn trống) trong khi cung cấp nhị phân bình thường sẽ chỉ giải phóng chuỗi ở đầu hàng đợi (giả sử có một luồng bị chặn).
Cao Jeff

121

Câu trả lời là không hiệu quả. Mutexes không reentrant dẫn đến mã tốt hơn.

Ví dụ: A :: foo () mua lại khóa. Sau đó, nó gọi B :: bar (). Điều này làm việc tốt khi bạn viết nó. Nhưng đôi khi, sau đó ai đó thay đổi B :: bar () để gọi A :: baz (), cũng mua lại khóa.

Chà, nếu bạn không có đột biến đệ quy, thì bế tắc này. Nếu bạn có chúng, nó chạy, nhưng nó có thể bị hỏng. A :: foo () có thể đã để đối tượng ở trạng thái không nhất quán trước khi gọi thanh (), với giả định rằng baz () không thể chạy vì nó cũng có được mutex. Nhưng nó có lẽ không nên chạy! Người đã viết A :: foo () cho rằng không ai có thể gọi A :: baz () cùng một lúc - đó là toàn bộ lý do mà cả hai phương pháp đó đều có được khóa.

Mô hình tinh thần phù hợp để sử dụng mutexes: Mutex bảo vệ một bất biến. Khi mutex được tổ chức, bất biến có thể thay đổi, nhưng trước khi phát hành mutex, bất biến được thiết lập lại. Khóa reentrant rất nguy hiểm vì lần thứ hai bạn có được khóa, bạn không thể chắc chắn bất biến là đúng nữa.

Nếu bạn hài lòng với các khóa reentrant, đó chỉ là do bạn chưa phải gỡ lỗi một vấn đề như thế này trước đây. Java có khóa không reentrant những ngày này trong java.util.concản.locks, nhân tiện.


4
Phải mất một thời gian tôi mới hiểu được những gì bạn nói về sự bất biến không có giá trị khi bạn lấy khóa lần thứ hai. Điểm tốt! Điều gì xảy ra nếu đó là khóa đọc ghi (như ReadWriteLock của Java) và bạn đã có được khóa đọc và sau đó lấy lại khóa đọc lần thứ hai trong cùng một luồng. Bạn sẽ không làm mất hiệu lực bất biến sau khi có được khóa đọc đúng không? Vì vậy, khi bạn có được khóa đọc thứ hai, bất biến vẫn đúng.
dgrant

1
@Jonathan Java có khóa không reentrant những ngày này trong java.util.concản.locks ??
dùng454322

1
+1 Tôi đoán rằng, việc sử dụng phổ biến nhất cho khóa reentrant là bên trong một lớp duy nhất, trong đó một số phương thức có thể được gọi từ cả hai đoạn mã được bảo vệ và không được bảo vệ. Điều này có thể thực sự luôn luôn được bao gồm. @ user454322 Chắc chắn , Semaphore.
maaartinus

1
Xin tha thứ cho sự hiểu lầm của tôi, nhưng tôi không thấy điều này có liên quan đến mutex như thế nào. Giả sử không có đa luồng và khóa liên quan, A::foo()vẫn có thể khiến đối tượng ở trạng thái không nhất quán trước khi gọi A::bar(). Mutex, đệ quy hay không, có liên quan gì đến trường hợp này không?
Siyuan Ren

1
@SiyuanRen: Vấn đề là có thể suy luận cục bộ về mã. Mọi người (ít nhất là tôi) được đào tạo để nhận ra các vùng bị khóa là duy trì bất biến, đó là vào thời điểm bạn có được khóa, không có luồng nào khác đang sửa đổi trạng thái, do đó, bất biến trên vùng quan trọng giữ. Đây không phải là một quy tắc khó và bạn có thể viết mã với những bất biến không được lưu ý, nhưng điều đó sẽ khiến mã của bạn khó lý luận và duy trì hơn. Điều tương tự cũng xảy ra trong chế độ đơn luồng không có mutexes, nhưng ở đó chúng tôi không được đào tạo để lý luận cục bộ xung quanh khu vực được bảo vệ.
David Rodríguez - dribeas

92

Như được viết bởi chính Dave Butenhof :

"Vấn đề lớn nhất trong tất cả các vấn đề lớn với các đột biến đệ quy là chúng khuyến khích bạn hoàn toàn mất dấu vết về sơ đồ và phạm vi khóa của bạn. Điều này thật nguy hiểm. Ác quỷ. Đó là" kẻ ăn sợi ". Bạn giữ khóa trong thời gian ngắn nhất. Luôn luôn. Nếu bạn gọi một cái gì đó có khóa được giữ đơn giản chỉ vì bạn không biết nó bị giữ hoặc vì bạn không biết liệu callee có cần mutex hay không, thì bạn đã giữ nó quá lâu. nhắm một khẩu súng ngắn vào ứng dụng của bạn và bóp cò. Có lẽ bạn đã bắt đầu sử dụng các luồng để có được sự tương tranh, nhưng bạn chỉ cần TRƯỚC đồng thời. "


9
Cũng lưu ý phần cuối cùng trong phản hồi của Butenhof: ...you're not DONE until they're [recursive mutex] all gone.. Or sit back and let someone else do the design.
user454322

2
Anh ta cũng nói rằng sử dụng một mutex đệ quy toàn cầu duy nhất (ý kiến ​​của anh ta là bạn chỉ cần một) là một cái nạng để trì hoãn một cách có ý thức công việc khó hiểu của một thư viện bên ngoài khi bạn bắt đầu sử dụng nó trong mã đa luồng. Nhưng bạn không nên sử dụng nạng mãi mãi mà cuối cùng hãy đầu tư thời gian để hiểu và khắc phục các bất biến đồng thời của mã. Vì vậy, chúng tôi có thể diễn giải rằng sử dụng mutex đệ quy là nợ kỹ thuật.
FooF

14

Mô hình tinh thần phù hợp để sử dụng mutexes: Mutex bảo vệ một bất biến.

Tại sao bạn chắc chắn rằng đây là mô hình tinh thần thực sự đúng đắn khi sử dụng mutexes? Tôi nghĩ rằng mô hình đúng là bảo vệ dữ liệu nhưng không bất biến.

Vấn đề bảo vệ bất biến thể hiện ngay cả trong các ứng dụng đơn luồng và không có gì phổ biến với đa luồng và đột biến.

Hơn nữa, nếu bạn cần bảo vệ bất biến, bạn vẫn có thể sử dụng semaphore nhị phân không bao giờ được đệ quy.


Thật. Có những cơ chế tốt hơn để bảo vệ một bất biến.
ActiveTrayPrntrTagDataStrDrvr

8
Đây phải là một bình luận cho câu trả lời đưa ra tuyên bố đó. Mutexes không chỉ bảo vệ dữ liệu, chúng còn bảo vệ bất biến. Cố gắng viết một số container đơn giản (đơn giản nhất là một ngăn xếp) về mặt nguyên tử (nơi dữ liệu tự bảo vệ) thay vì mutexes và bạn sẽ hiểu câu lệnh.
David Rodríguez - dribeas

Mutexes không bảo vệ dữ liệu, chúng bảo vệ một bất biến. Điều đó bất biến có thể được sử dụng để bảo vệ dữ liệu.
Jon Hanna

4

Một lý do chính mà các mutex đệ quy là hữu ích là trong trường hợp truy cập các phương thức nhiều lần bởi cùng một luồng. Ví dụ: giả sử nếu khóa mutex đang bảo vệ ngân hàng A / c rút tiền, thì nếu có một khoản phí cũng liên quan đến việc rút tiền đó, thì phải sử dụng cùng một mutex.


3

Trường hợp sử dụng tốt duy nhất cho mutex đệ quy là khi một đối tượng chứa nhiều phương thức. Khi bất kỳ phương thức nào sửa đổi nội dung của đối tượng và do đó phải khóa đối tượng trước khi trạng thái được nhất quán trở lại.

Nếu các phương thức sử dụng các phương thức khác (ví dụ: addNewArray () gọi addNewPoint () và hoàn tất bằng recheckBound ()), nhưng bất kỳ chức năng nào trong số chúng cũng cần phải khóa mutex, thì mutex đệ quy là win-win.

Đối với bất kỳ trường hợp nào khác (chỉ giải quyết mã hóa xấu, sử dụng nó ngay cả trong các đối tượng khác nhau) rõ ràng là sai!


1

Đột biến không đệ quy tốt cho việc gì?

Chúng hoàn toàn tốt khi bạn phải chắc chắn rằng mutex đã được mở khóa trước khi làm điều gì đó. Điều này là do pthread_mutex_unlockcó thể đảm bảo rằng mutex chỉ được mở khóa nếu nó không được đệ quy.

pthread_mutex_t      g_mutex;

void foo()
{
    pthread_mutex_lock(&g_mutex);
    // Do something.
    pthread_mutex_unlock(&g_mutex);

    bar();
}

Nếu g_mutexkhông đệ quy, mã ở trên được đảm bảo để gọi bar()với mutex được mở khóa .

Do đó loại bỏ khả năng bế tắc trong trường hợp bar() xảy ra là một chức năng bên ngoài không xác định, có thể làm điều gì đó có thể dẫn đến một luồng khác đang cố gắng để có được cùng một mutex. Các kịch bản như vậy không phải là hiếm trong các ứng dụng được xây dựng trên nhóm luồng và trong các ứng dụng phân tán, trong đó một cuộc gọi liên tiến trình có thể sinh ra một luồng mới mà không cần lập trình viên máy khách nhận ra điều đó. Trong tất cả các kịch bản như vậy, tốt nhất chỉ gọi các chức năng bên ngoài đã nói sau khi khóa được phát hành.

Nếu g_mutexđược đệ quy, đơn giản là không có cách nào để đảm bảo nó được mở khóa trước khi thực hiện cuộc gọi.

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.