Gần đây, tôi đã hỏi một câu hỏi, với tiêu đề là "Chuỗi malloc có an toàn không?" , và bên trong đó tôi hỏi, "Malloc có tái gia nhập không?"
Tôi có ấn tượng rằng tất cả những người tham gia lại đều an toàn.
Giả định này có sai không?
Gần đây, tôi đã hỏi một câu hỏi, với tiêu đề là "Chuỗi malloc có an toàn không?" , và bên trong đó tôi hỏi, "Malloc có tái gia nhập không?"
Tôi có ấn tượng rằng tất cả những người tham gia lại đều an toàn.
Giả định này có sai không?
Câu trả lời:
Các hàm truy xuất lại không dựa vào các biến toàn cục được hiển thị trong tiêu đề thư viện C .. lấy ví dụ như strtok () vs strtok_r () trong C.
Một số chức năng cần một nơi để lưu trữ 'công việc đang thực hiện', các chức năng đăng nhập lại cho phép bạn chỉ định con trỏ này trong bộ nhớ riêng của luồng, không phải trong toàn cục. Vì bộ nhớ này dành riêng cho hàm gọi, nó có thể bị gián đoạn và nhập lại ( đăng nhập lại) và vì trong hầu hết các trường hợp, loại trừ lẫn nhau ngoài những gì mà hàm triển khai không cần thiết để điều này hoạt động, chúng thường được coi là chủ đề an toàn . Tuy nhiên, điều này không được đảm bảo theo định nghĩa.
errno, tuy nhiên, là một trường hợp hơi khác trên hệ thống POSIX (và có xu hướng là điều kỳ quặc trong bất kỳ lời giải thích nào về cách tất cả điều này hoạt động) :)
Nói tóm lại, người đăng nhập lại thường có nghĩa là an toàn cho chuỗi (như trong "sử dụng phiên bản đăng nhập lại của chức năng đó nếu bạn đang sử dụng chuỗi"), nhưng an toàn chuỗi không phải lúc nào cũng có nghĩa là tham gia lại (hoặc ngược lại). Khi bạn đang xem xét sự an toàn của luồng, tính đồng thời là điều bạn cần phải suy nghĩ. Nếu bạn phải cung cấp một phương tiện khóa và loại trừ lẫn nhau để sử dụng một hàm, thì hàm này vốn dĩ không an toàn theo chuỗi.
Tuy nhiên, không phải tất cả các chức năng đều cần được kiểm tra. malloc()
không cần phải đăng nhập lại, nó không phụ thuộc vào bất kỳ thứ gì ngoài phạm vi của điểm vào cho bất kỳ luồng nào đã cho (và bản thân nó là luồng an toàn).
Các hàm trả về giá trị được cấp phát tĩnh không an toàn cho chuỗi nếu không sử dụng mutex, futex hoặc cơ chế khóa nguyên tử khác. Tuy nhiên, họ không cần phải nhập lại nếu họ không bị gián đoạn.
I E:
static char *foo(unsigned int flags)
{
static char ret[2] = { 0 };
if (flags & FOO_BAR)
ret[0] = 'c';
else if (flags & BAR_FOO)
ret[0] = 'd';
else
ret[0] = 'e';
ret[1] = 'A';
return ret;
}
Vì vậy, như bạn thấy, có nhiều luồng sử dụng mà không có một số loại khóa sẽ là một thảm họa .. nhưng nó không có mục đích là tham gia lại. Bạn sẽ gặp phải điều đó khi bộ nhớ được cấp phát động là điều cấm kỵ trên một số nền tảng nhúng.
Trong lập trình chức năng thuần túy, reentrant thường không ngụ ý luồng an toàn, nó sẽ phụ thuộc vào hành vi của các hàm được xác định hoặc ẩn danh được chuyển đến điểm nhập hàm, đệ quy, v.v.
Một cách tốt hơn để đặt 'an toàn luồng' là an toàn cho truy cập đồng thời , điều này minh họa rõ hơn sự cần thiết.
TL; DR: Một chức năng có thể được nhập lại, an toàn theo luồng, cả hai hoặc không.
Các bài viết Wikipedia cho thread-an toàn và reentrancy là cũng có giá trị đọc. Dưới đây là một vài trích dẫn:
Một hàm an toàn theo chuỗi nếu:
nó chỉ thao tác các cấu trúc dữ liệu được chia sẻ theo cách đảm bảo thực thi an toàn bởi nhiều luồng cùng một lúc.
Một hàm được nhập lại nếu:
nó có thể bị gián đoạn tại bất kỳ thời điểm nào trong quá trình thực thi và sau đó được gọi lại một cách an toàn ("được nhập lại") trước khi các lệnh gọi trước đó hoàn tất việc thực thi.
Như ví dụ về khả năng truy cập lại, Wikipedia đưa ra ví dụ về một hàm được thiết kế để được gọi bởi ngắt hệ thống: giả sử nó đang chạy khi một ngắt khác xảy ra. Nhưng đừng nghĩ rằng bạn an toàn chỉ vì bạn không viết mã khi bị gián đoạn hệ thống: bạn có thể gặp sự cố nhập lại trong chương trình một luồng nếu bạn sử dụng hàm gọi lại hoặc hàm đệ quy.
Chìa khóa để tránh nhầm lẫn là reentrant đề cập đến chỉ một luồng thực thi. Đó là một khái niệm từ thời chưa có hệ điều hành đa nhiệm.
Ví dụ
(Sửa đổi một chút từ các bài viết trên Wikipedia)
Ví dụ 1: không an toàn theo chuỗi, không đăng nhập lại
/* As this function uses a non-const global variable without
any precaution, it is neither reentrant nor thread-safe. */
int t;
void swap(int *x, int *y)
{
t = *x;
*x = *y;
*y = t;
}
Ví dụ 2: chuỗi an toàn, không đăng nhập lại
/* We use a thread local variable: the function is now
thread-safe but still not reentrant (within the
same thread). */
__thread int t;
void swap(int *x, int *y)
{
t = *x;
*x = *y;
*y = t;
}
Ví dụ 3: không an toàn theo chuỗi, người đăng nhập lại
/* We save the global state in a local variable and we restore
it at the end of the function. The function is now reentrant
but it is not thread safe. */
int t;
void swap(int *x, int *y)
{
int s;
s = t;
t = *x;
*x = *y;
*y = t;
t = s;
}
Ví dụ 4: an toàn chuỗi, người đăng nhập lại
/* We use a local variable: the function is now
thread-safe and reentrant, we have ascended to
higher plane of existence. */
void swap(int *x, int *y)
{
int t;
t = *x;
*x = *y;
*y = t;
}
t = *x
cuộc gọi swap()
, thì t
sẽ bị ghi đè, dẫn đến kết quả không mong muốn.
swap(5, 6)
bị gián đoạn bởi a swap(1, 2)
. Sau khi t=*x
, s=t_original
và t=5
. Bây giờ, sau sự gián đoạn, s=5
và t=1
. Tuy nhiên, trước khi swap
trả về lần thứ hai, nó sẽ khôi phục ngữ cảnh, làm cho t=s=5
. Bây giờ, chúng ta quay lại phần đầu swap
với t=5 and s=t_original
và tiếp tục phần sau t=*x
. Vì vậy, chức năng dường như được đăng nhập lại. Hãy nhớ rằng mọi cuộc gọi đều có bản sao riêng của nó s
được phân bổ trên ngăn xếp.
Nó phụ thuộc vào định nghĩa. Ví dụ Qt sử dụng như sau:
Một hàm an toàn luồng * có thể được gọi đồng thời từ nhiều luồng, ngay cả khi các lệnh gọi sử dụng dữ liệu được chia sẻ, vì tất cả các tham chiếu đến dữ liệu được chia sẻ đều được tuần tự hóa.
Một hàm reentrant cũng có thể được gọi đồng thời từ nhiều luồng, nhưng chỉ khi mỗi lệnh gọi sử dụng dữ liệu riêng của nó.
Do đó, một thread-safe chức năng luôn là lõm, nhưng một reentrant chức năng không phải lúc nào thread-safe.
Theo phần mở rộng, một lớp được cho là có thể nhập lại nếu các hàm thành viên của nó có thể được gọi một cách an toàn từ nhiều luồng, miễn là mỗi luồng sử dụng một thể hiện khác nhau của lớp. Lớp an toàn theo luồng nếu các hàm thành viên của nó có thể được gọi một cách an toàn từ nhiều luồng, ngay cả khi tất cả các luồng sử dụng cùng một thể hiện của lớp.
nhưng họ cũng cảnh báo:
Lưu ý: Thuật ngữ trong miền đa luồng không hoàn toàn được chuẩn hóa. POSIX sử dụng các định nghĩa về reentrant và thread-safe hơi khác cho các API C của nó. Khi sử dụng các thư viện lớp C ++ hướng đối tượng khác với Qt, hãy chắc chắn rằng các định nghĩa được hiểu.