Khóa Re-Entrant và khái niệm nói chung là gì?


91

Tôi luôn luôn bối rối. Ai đó có thể giải thích ý nghĩa của Reentrant trong các ngữ cảnh khác nhau không? Và tại sao bạn muốn sử dụng reentrant so với non-reentrant?

Giả sử pthread (posix) khóa các nguyên thủy, chúng có tham gia lại hay không? Những cạm bẫy nào cần tránh khi sử dụng chúng?

Mutex có tái gia nhập không?

Câu trả lời:


157

Khóa người đăng ký lại

Khóa tái nhập là một trong đó một quy trình có thể yêu cầu khóa nhiều lần mà không tự chặn. Nó hữu ích trong những trường hợp không dễ theo dõi xem bạn đã nắm được ổ khóa chưa. Nếu một ổ khóa không có khả năng nhập lại, bạn có thể lấy khóa, sau đó chặn khi bạn lấy lại, làm bế tắc quá trình của chính bạn.

Reentrancy nói chung là một thuộc tính của mã mà nó không có trạng thái có thể thay đổi trung tâm có thể bị hỏng nếu mã được gọi trong khi nó đang thực thi. Một cuộc gọi như vậy có thể được thực hiện bởi một luồng khác hoặc nó có thể được thực hiện đệ quy bởi một đường dẫn thực thi bắt nguồn từ chính mã.

Nếu mã dựa vào trạng thái được chia sẻ có thể được cập nhật ở giữa quá trình thực thi, nó không được đăng nhập lại, ít nhất là không nếu bản cập nhật đó có thể phá vỡ nó.

Một trường hợp sử dụng để khóa lại người đăng ký

Một ví dụ (hơi chung chung và có nguồn gốc) về ứng dụng cho khóa đăng nhập lại có thể là:

  • Bạn có một số tính toán liên quan đến một thuật toán duyệt qua một đồ thị (có thể với các chu trình trong đó). Một đường truyền có thể truy cập vào cùng một nút nhiều hơn một lần do các chu kỳ hoặc do nhiều đường dẫn đến cùng một nút.

  • Cấu trúc dữ liệu có thể truy cập đồng thời và có thể được cập nhật vì một số lý do, có lẽ bởi một luồng khác. Bạn cần có khả năng khóa các nút riêng lẻ để đối phó với khả năng bị hỏng dữ liệu do điều kiện chủng tộc. Vì một số lý do (có lẽ là hiệu suất) bạn không muốn khóa toàn bộ cấu trúc dữ liệu.

  • Máy tính của bạn không thể giữ lại thông tin đầy đủ về những nút bạn đã truy cập hoặc bạn đang sử dụng cấu trúc dữ liệu không cho phép các câu hỏi 'tôi đã từng ở đây trước đây' được trả lời nhanh chóng.

    Ví dụ về tình huống này sẽ là một triển khai đơn giản của thuật toán Dijkstra với hàng đợi ưu tiên được triển khai dưới dạng một đống nhị phân hoặc tìm kiếm theo chiều rộng sử dụng danh sách liên kết đơn giản làm hàng đợi. Trong những trường hợp này, quét hàng đợi cho các phần chèn hiện có là O (N) và bạn có thể không muốn thực hiện việc này trên mỗi lần lặp lại.

Trong tình huống này, việc theo dõi những ổ khóa bạn đã mua là rất tốn kém. Giả sử bạn muốn thực hiện khóa ở cấp độ nút, cơ chế khóa tham gia lại sẽ giảm bớt nhu cầu cho biết liệu bạn đã truy cập vào một nút trước đó hay chưa. Bạn chỉ có thể khóa nút một cách mù quáng, có thể mở khóa nó sau khi bạn bật nó ra khỏi hàng đợi.

Mutexes đăng nhập lại

Một mutex đơn giản không tham gia lại vì chỉ có một luồng có thể nằm trong phần quan trọng tại một thời điểm nhất định. Nếu bạn lấy mutex và sau đó cố gắng lấy lại, mutex đơn giản không có đủ thông tin để biết ai đã nắm giữ nó trước đây. Để thực hiện điều này một cách đệ quy, bạn cần một cơ chế trong đó mỗi luồng có một mã thông báo để bạn có thể biết ai đã lấy mutex. Điều này làm cho cơ chế mutex đắt hơn một chút vì vậy bạn có thể không muốn thực hiện nó trong mọi tình huống.

IIRC API chuỗi POSIX cung cấp tùy chọn mutex đăng nhập lại và không đăng nhập lại.


2
Mặc dù những tình huống như vậy thường nên tránh, vì nó cũng khiến bạn khó tránh khỏi bế tắc. Dù sao thì việc phân luồng cũng đủ khó mà không cần phải nghi ngờ về việc liệu bạn đã có khóa chưa.
Jon Skeet

+1, cũng hãy xem xét trường hợp khóa KHÔNG hoạt động trở lại, bạn có thể tự chặn nếu không cẩn thận. Ngoài ra trong C, bạn không có các cơ chế giống như các ngôn ngữ khác để đảm bảo khóa được Phát hành nhiều lần khi nó được Mua. Điều này có thể dẫn đến những vấn đề lớn.
user7116

1
mà của chính xác ly những gì xảy ra với tôi ngày hôm qua: Tôi không phải đưa vấn đề tái entrancy vào việc xem xét và kết thúc gỡ một bế tắc trong 5 giờ ...
vehomzzz

@Jon Skeet - Tôi nghĩ rằng có thể có những tình huống (xem ví dụ có phần mô tả của tôi ở trên) trong đó việc theo dõi các ổ khóa là không thực tế do hiệu suất hoặc các cân nhắc khác.
ConcernedOfTunbridgeWells

21

Khóa đăng nhập lại cho phép bạn viết một phương thức Mđặt khóa tài nguyên Avà sau đó gọi Mđệ quy hoặc từ mã đã khóa A.

Với khóa không tham gia lại, bạn sẽ cần 2 phiên bản M, một khóa và một khóa không, và logic bổ sung để gọi đúng.


Điều này có nghĩa là nếu tôi có các cuộc gọi đệ quy thu được cùng một khóa đối tượng nhiều hơn một lần - giả sử xnhiều lần bởi một chuỗi nhất định, tôi không thể xen kẽ việc thực thi mà không giải phóng tất cả các khóa được thu thập đệ quy (cùng một khóa nhưng với xsố lần)? Nếu đúng, thì về cơ bản, nó thực hiện tuần tự. Tui bỏ lỡ điều gì vậy?
DevdattaK

Đó không phải là một vấn đề lo lắng thực sự. Nó thiên về khóa chi tiết và một Chủ đề sẽ không tự khóa.
Henk Holterman

16

Khóa Reentrant được mô tả rất tốt trong hướng dẫn này .

Ví dụ trong hướng dẫn này ít phức tạp hơn nhiều so với trong câu trả lời về việc duyệt qua biểu đồ. Khóa reentrant rất hữu ích trong những trường hợp rất đơn giản.


3

Cái gì và tại sao của mutex đệ quy không nên là một điều phức tạp được mô tả trong câu trả lời được chấp nhận.

Tôi muốn viết ra sự hiểu biết của mình sau khi tìm hiểu kỹ trên mạng.


Đầu tiên, bạn nên nhận ra rằng khi nói về mutex , khái niệm đa luồng chắc chắn cũng có liên quan. (mutex được sử dụng để đồng bộ hóa. Tôi không cần mutex nếu tôi chỉ có 1 luồng trong chương trình của mình)


Thứ hai, bạn nên biết sự khác biệt giữa mutex bình thườngmutex đệ quy .

Trích dẫn từ APUE :

(Mutex đệ quy là a) Loại mutex cho phép cùng một luồng khóa nó nhiều lần mà không cần mở khóa trước.

Sự khác biệt chính là trong cùng một luồng , khóa lại một khóa đệ quy không dẫn đến deadlock, không chặn luồng.

Điều này có nghĩa là khóa lặp lại không bao giờ gây ra bế tắc?
Không, nó vẫn có thể gây ra deadlock như mutex bình thường nếu bạn đã khóa nó trong một chuỗi mà không mở khóa nó và cố gắng khóa nó trong các chuỗi khác.

Hãy xem một số mã làm bằng chứng.

  1. mutex bình thường với bế tắc
#include <pthread.h>
#include <stdio.h>

pthread_mutex_t lock;


void * func1(void *arg){
    printf("thread1\n");
    pthread_mutex_lock(&lock);
    printf("thread1 hey hey\n");

}


void * func2(void *arg){
    printf("thread2\n");
    pthread_mutex_lock(&lock);
    printf("thread2 hey hey\n");
}

int main(){
    pthread_mutexattr_t lock_attr;
    int error;
//    error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
    error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_DEFAULT);
    if(error){
        perror(NULL);
    }

    pthread_mutex_init(&lock, &lock_attr);

    pthread_t t1, t2;

    pthread_create(&t1, NULL, func1, NULL);
    pthread_create(&t2, NULL, func2, NULL);

    pthread_join(t2, NULL);

}

đầu ra:

thread1
thread1 hey hey
thread2

ví dụ về bế tắc chung, không có vấn đề gì.

  1. đệ quy mutex với deadlock

Chỉ cần bỏ ghi chú dòng này
error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
và bình luận dòng kia.

đầu ra:

thread1
thread1 hey hey
thread2

Có, mutex đệ quy cũng có thể gây ra bế tắc.

  1. mutex bình thường, khóa lại trong cùng một chủ đề
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>

pthread_mutex_t lock;


void func3(){
    printf("func3\n");
    pthread_mutex_lock(&lock);
    printf("func3 hey hey\n");
}

void * func1(void *arg){
    printf("thread1\n");
    pthread_mutex_lock(&lock);
    func3();
    printf("thread1 hey hey\n");

}


void * func2(void *arg){
    printf("thread2\n");
    pthread_mutex_lock(&lock);
    printf("thread2 hey hey\n");
}

int main(){
    pthread_mutexattr_t lock_attr;
    int error;
//    error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_RECURSIVE);
    error = pthread_mutexattr_settype(&lock_attr, PTHREAD_MUTEX_DEFAULT);
    if(error){
        perror(NULL);
    }

    pthread_mutex_init(&lock, &lock_attr);

    pthread_t t1, t2;

    pthread_create(&t1, NULL, func1, NULL);
    sleep(2); 
    pthread_create(&t2, NULL, func2, NULL);

    pthread_join(t2, NULL);

}

đầu ra:

thread1
func3
thread2

Chốt lại thread t1, vào func3.
(Tôi sử dụng sleep(2)để làm cho nó dễ dàng hơn thấy rằng bế tắc đầu tiên là do khóa lại func3)

  1. mutex đệ quy, khóa lại trong cùng một chủ đề

Một lần nữa, bỏ ghi chú dòng mutex đệ quy và nhận xét dòng còn lại.

đầu ra:

thread1
func3
func3 hey hey
thread1 hey hey
thread2

Chốt lại thread t2, vào func2. Xem? func3kết thúc và thoát ra, khóa lại không chặn luồng hoặc dẫn đến bế tắc.


Vì vậy, câu hỏi cuối cùng, tại sao chúng ta cần nó?

Đối với hàm đệ quy (được gọi trong các chương trình đa luồng và bạn muốn bảo vệ một số tài nguyên / dữ liệu).

Ví dụ: Bạn có một chương trình đa luồng và gọi một hàm đệ quy trong luồng A. Bạn có một số dữ liệu mà bạn muốn bảo vệ trong hàm đệ quy đó, vì vậy bạn sử dụng cơ chế mutex. Việc thực thi hàm đó là tuần tự trong chuỗi A, vì vậy bạn chắc chắn sẽ khóa lại mutex trong đệ quy. Sử dụng mutex bình thường gây ra bế tắc. Và mutex đệ quy được phát minh để giải quyết điều này.

Xem ví dụ từ câu trả lời được chấp nhận Khi nào sử dụng mutex đệ quy? .

Wikipedia giải thích về mutex đệ quy rất tốt. Chắc chắn giá trị để đọc. Wikipedia: Reentrant_mutex

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.