Threadsafe so với người tham gia lại


89

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:


42

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.


2
Reentrant không ngụ ý an toàn cho chuỗi. Các chức năng thuần túy ngụ ý an toàn luồng.
Julio Guerra

Câu trả lời tuyệt vời Tim. Chỉ cần làm rõ, sự hiểu biết của tôi từ "thường xuyên" của bạn là an toàn theo luồng không có nghĩa là reentrant, nhưng reentrant không ngụ ý an toàn cho luồng. Bạn có thể tìm thấy một ví dụ về một hàm reentrant không an toàn cho chuỗi không?
Riccardo

@ Tim Post "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à người tham gia lại." qt nói ngược lại: "Do đó, một chức năng an toàn luồng luôn được đưa vào lại, nhưng một chức năng quay lại không phải lúc nào cũng an toàn cho luồng."
4pie0

và wikipedia lại nói một điều gì đó khác: "Định nghĩa về lần truy cập lại này khác với định nghĩa về an toàn luồng trong môi trường đa luồng. Một chương trình con của người đăng nhập lại có thể đạt được độ an toàn của luồng, [1] nhưng chỉ riêng việc đăng nhập lại có thể không đủ để an toàn cho luồng trong mọi tình huống. Ngược lại, mã an toàn chuỗi không nhất thiết phải được nhập lại (...) "
4pie0

@Riccardo: Các hàm được đồng bộ hóa thông qua các biến dễ bay hơi nhưng không phải là hàng rào bộ nhớ đầy đủ để sử dụng với trình xử lý tín hiệu / ngắt thường là tham gia lại nhưng an toàn theo luồng.
doynax

77

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ànreentrancy 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;
}

10
Tôi biết tôi không nên bình luận chỉ để nói lời cảm ơn, nhưng đây là một trong những minh họa tốt nhất thể hiện sự khác biệt giữa chức năng đăng nhập lại và chức năng an toàn luồng. Đặc biệt, bạn đã sử dụng các thuật ngữ rõ ràng rất ngắn gọn và chọn một hàm ví dụ tuyệt vời để phân biệt giữa 4 loại. Vì vậy, Cảm ơn!
ryyker

11
Có vẻ như đối với tôi thì exemple 3 không phải là reentrant: nếu một trình xử lý tín hiệu, ngắt sau t = *xcuộc gọi swap(), thì tsẽ bị ghi đè, dẫn đến kết quả không mong muốn.
rom1v

1
@ SandBag_1996, hãy coi một cuộc gọi swap(5, 6)bị gián đoạn bởi a swap(1, 2). Sau khi t=*x, s=t_originalt=5. Bây giờ, sau sự gián đoạn, s=5t=1. Tuy nhiên, trước khi swaptrả 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 swapvới t=5 and s=t_originalvà 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.
urnonav

4
@ SandBag_1996 Giả định là nếu hàm bị gián đoạn (tại bất kỳ thời điểm nào), nó chỉ được gọi lại và chúng tôi đợi cho đến khi nó hoàn thành trước khi tiếp tục cuộc gọi ban đầu. Nếu bất cứ điều gì khác xảy ra, thì về cơ bản đó là đa luồng và chức năng này không an toàn cho chuỗi. Giả sử hàm thực hiện ABCD, chúng ta chỉ chấp nhận những thứ như AB_ABCD_CD hoặc A_ABCD_BCD, hoặc thậm chí A__AB_ABCD_CD__BCD. Như bạn có thể kiểm tra, ví dụ 3 sẽ hoạt động tốt theo những giả định này, vì vậy nó được sử dụng lại. Hi vọng điêu nay co ich.
MiniQuark

1
@ SandBag_1996, mutex thực sự sẽ không đăng nhập lại. Lời gọi đầu tiên khóa mutex. Đi kèm với lời kêu gọi thứ hai - bế tắc.
urnonav

56

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.


2
Định nghĩa về người quay lại này quá mạnh.
qweruiop

Một hàm vừa là reentrant và thread-safe nếu nó không sử dụng bất kỳ var toàn cục / tĩnh nào. Chủ đề - an toàn: khi nhiều luồng chạy chức năng của bạn cùng một lúc, liệu có cuộc đua nào không ?? Nếu bạn sử dụng global var, hãy sử dụng khóa để bảo vệ nó. vì vậy nó an toàn theo chủ đề. reentrant: nếu một tín hiệu xảy ra trong quá trình thực thi chức năng của bạn và gọi lại chức năng của bạn trong tín hiệu, liệu có an toàn không ??? trong trường hợp này, không có nhiều chủ đề. Đó là tốt nhất mà làm của bạn không sử dụng bất kỳ tĩnh / var toàn cầu để làm cho nó lõm, hoặc như trong ví dụ 3.
van keniee
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.