1. Làm thế nào được xác định một cách an toàn ?
Về mặt ngữ nghĩa. Trong trường hợp này, đây không phải là một thuật ngữ khó xác định. Nó chỉ có nghĩa là "Bạn có thể làm điều đó, không có rủi ro".
2. Nếu một chương trình có thể được thực hiện đồng thời một cách an toàn, điều đó luôn có nghĩa là nó được reentrant?
Không.
Ví dụ: chúng ta có một hàm C ++ có cả khóa và gọi lại làm tham số:
#include <mutex>
typedef void (*callback)();
std::mutex m;
void foo(callback f)
{
m.lock();
// use the resource protected by the mutex
if (f) {
f();
}
// use the resource protected by the mutex
m.unlock();
}
Một chức năng khác cũng có thể cần phải khóa cùng một mutex:
void bar()
{
foo(nullptr);
}
Ngay từ cái nhìn đầu tiên, mọi thứ có vẻ ổn.
int main()
{
foo(bar);
return 0;
}
Nếu khóa trên mutex không được đệ quy, thì đây là điều sẽ xảy ra, trong luồng chính:
main
sẽ gọi foo
.
foo
sẽ có được khóa.
foo
sẽ gọi bar
, sẽ gọi foo
.
- thứ 2
foo
sẽ cố gắng để có được khóa, thất bại và chờ đợi nó được phát hành.
- Bế tắc.
- Giáo sư…
Ok, tôi đã lừa dối, sử dụng điều gọi lại. Nhưng thật dễ để tưởng tượng những đoạn mã phức tạp hơn có tác dụng tương tự.
3. Chính xác thì chủ đề chung giữa sáu điểm được đề cập mà tôi nên ghi nhớ trong khi kiểm tra mã của mình để biết khả năng reentrant là gì?
Bạn có thể ngửi thấy một vấn đề nếu chức năng của bạn có / cấp quyền truy cập vào tài nguyên liên tục có thể sửa đổi hoặc có / cung cấp quyền truy cập vào một chức năng có mùi .
( Ok, 99% mã của chúng tôi sẽ có mùi, sau đó Xem phần cuối để xử lý điều đó )
Vì vậy, nghiên cứu mã của bạn, một trong những điểm đó sẽ cảnh báo bạn:
- Hàm này có một trạng thái (tức là truy cập một biến toàn cục hoặc thậm chí là một biến thành viên lớp)
- Hàm này có thể được gọi bởi nhiều luồng hoặc có thể xuất hiện hai lần trong ngăn xếp trong khi quá trình đang thực thi (tức là hàm có thể tự gọi, trực tiếp hoặc gián tiếp). Chức năng nhận cuộc gọi lại như tham số mùi rất nhiều.
Lưu ý rằng không reentrancy là virus: Một chức năng có thể gọi là chức năng không reentrant có thể có thể được coi là reentrant.
Cũng lưu ý rằng các phương thức C ++ có mùi vì chúng có quyền truy cập this
, vì vậy bạn nên nghiên cứu mã để chắc chắn rằng chúng không có tương tác hài hước.
4.1. Có phải tất cả các hàm đệ quy reentrant?
Không.
Trong các trường hợp đa luồng, một hàm đệ quy truy cập vào một tài nguyên được chia sẻ có thể được gọi bởi nhiều luồng cùng một lúc, dẫn đến dữ liệu xấu / bị hỏng.
Trong các trường hợp đơn lẻ, một hàm đệ quy có thể sử dụng hàm không reentrant (như khét tiếng strtok
) hoặc sử dụng dữ liệu toàn cầu mà không xử lý thực tế dữ liệu đã được sử dụng. Vì vậy, chức năng của bạn là đệ quy vì nó tự gọi trực tiếp hoặc gián tiếp, nhưng nó vẫn có thể được đệ quy-không an toàn .
4.2. Có phải tất cả các chức năng an toàn chủ đề được reentrant?
Trong ví dụ trên, tôi đã chỉ ra làm thế nào một chức năng chủ đề rõ ràng không được phát lại. OK, tôi đã gian lận vì tham số gọi lại. Nhưng sau đó, có nhiều cách để bế tắc một luồng bằng cách lấy nó hai lần một khóa không đệ quy.
4.3. Có phải tất cả các hàm đệ quy và an toàn luồng được reentrant?
Tôi sẽ nói "có" nếu bằng "đệ quy" bạn có nghĩa là "an toàn đệ quy".
Nếu bạn có thể đảm bảo rằng một hàm có thể được gọi đồng thời bởi nhiều luồng và có thể gọi chính nó, trực tiếp hoặc gián tiếp, mà không gặp vấn đề gì, thì nó sẽ được phát lại.
Vấn đề là đánh giá sự đảm bảo này ^ ^ ^
5. Các thuật ngữ như reentence và thread safe có tuyệt đối không, tức là chúng có các định nghĩa cụ thể cố định không?
Tôi tin rằng họ làm, nhưng sau đó, việc đánh giá một chức năng là an toàn theo luồng hoặc reentrant có thể khó khăn. Đây là lý do tại sao tôi sử dụng thuật ngữ mùi ở trên: Bạn có thể tìm thấy một hàm không được reentrant, nhưng có thể khó chắc chắn một đoạn mã phức tạp được reentrant
6. Một ví dụ
Giả sử bạn có một đối tượng, với một phương thức cần sử dụng tài nguyên:
struct MyStruct
{
P * p;
void foo()
{
if (this->p == nullptr)
{
this->p = new P();
}
// lots of code, some using this->p
if (this->p != nullptr)
{
delete this->p;
this->p = nullptr;
}
}
};
Vấn đề đầu tiên là nếu bằng cách nào đó, hàm này được gọi là đệ quy (nghĩa là hàm này tự gọi, trực tiếp hoặc gián tiếp), mã có thể sẽ bị sập, bởi vì this->p
sẽ bị xóa vào cuối cuộc gọi cuối cùng và vẫn có thể được sử dụng trước khi kết thúc của cuộc gọi đầu tiên.
Vì vậy, mã này không an toàn đệ quy .
Chúng tôi có thể sử dụng một bộ đếm tham chiếu để sửa lỗi này:
struct MyStruct
{
size_t c;
P * p;
void foo()
{
if (c == 0)
{
this->p = new P();
}
++c;
// lots of code, some using this->p
--c;
if (c == 0)
{
delete this->p;
this->p = nullptr;
}
}
};
Bằng cách này, mã trở nên an toàn đệ quy Nhưng nó vẫn không được phát hành lại do các vấn đề đa luồng: Chúng ta phải chắc chắn rằng các sửa đổi c
và p
sẽ được thực hiện một cách nguyên tử, sử dụng một mutex đệ quy (không phải tất cả các mutex đều được đệ quy):
#include <mutex>
struct MyStruct
{
std::recursive_mutex m;
size_t c;
P * p;
void foo()
{
m.lock();
if (c == 0)
{
this->p = new P();
}
++c;
m.unlock();
// lots of code, some using this->p
m.lock();
--c;
if (c == 0)
{
delete this->p;
this->p = nullptr;
}
m.unlock();
}
};
Và tất nhiên, tất cả điều này giả định rằng lots of code
chính nó được reentrant, bao gồm cả việc sử dụng p
.
Và đoạn mã trên thậm chí không an toàn từ xa , nhưng đây là một câu chuyện khác.
7. Hey 99% mã của chúng tôi không được nhận lại!
Nó khá đúng với mã spaghetti. Nhưng nếu bạn phân vùng chính xác mã của mình, bạn sẽ tránh được các vấn đề reentrancy.
7.1. Đảm bảo tất cả các chức năng không có trạng thái
Họ chỉ phải sử dụng các tham số, biến cục bộ của riêng mình, các hàm khác không có trạng thái và trả về các bản sao của dữ liệu nếu chúng trả về.
7.2. Hãy chắc chắn rằng đối tượng của bạn là "đệ quy an toàn"
Một phương thức đối tượng có quyền truy cập this
, vì vậy nó chia sẻ trạng thái với tất cả các phương thức của cùng một thể hiện của đối tượng.
Vì vậy, hãy chắc chắn rằng đối tượng có thể được sử dụng tại một điểm trong ngăn xếp (tức là gọi phương thức A), và sau đó, tại một điểm khác (tức là gọi phương thức B), mà không làm hỏng toàn bộ đối tượng. Thiết kế đối tượng của bạn để đảm bảo rằng khi thoát khỏi một phương thức, đối tượng đó ổn định và chính xác (không có con trỏ lơ lửng, không có các biến thành viên mâu thuẫn, v.v.).
7.3. Đảm bảo tất cả các đối tượng của bạn được đóng gói chính xác
Không ai khác có quyền truy cập vào dữ liệu nội bộ của họ:
// bad
int & MyObject::getCounter()
{
return this->counter;
}
// good
int MyObject::getCounter()
{
return this->counter;
}
// good, too
void MyObject::getCounter(int & p_counter)
{
p_counter = this->counter;
}
Ngay cả việc trả về một tham chiếu const cũng có thể nguy hiểm nếu người dùng lấy địa chỉ của dữ liệu, vì một số phần khác của mã có thể sửa đổi nó mà không cần mã giữ tham chiếu const được nói.
7.4. Đảm bảo người dùng biết đối tượng của bạn không an toàn cho chuỗi
Do đó, người dùng có trách nhiệm sử dụng mutexes để sử dụng một đối tượng được chia sẻ giữa các luồng.
Các đối tượng từ STL được thiết kế để không an toàn cho luồng (vì vấn đề về hiệu năng) và do đó, nếu người dùng muốn chia sẻ std::string
giữa hai luồng, người dùng phải bảo vệ quyền truy cập của nó bằng các nguyên hàm đồng thời;
7.5. Đảm bảo mã an toàn luồng của bạn là đệ quy an toàn
Điều này có nghĩa là sử dụng các mutex đệ quy nếu bạn tin rằng cùng một tài nguyên có thể được sử dụng hai lần bởi cùng một luồng.