Tôi đang đọc lên pthread.h
; các hàm điều kiện liên quan đến biến điều kiện (như pthread_cond_wait(3)
) yêu cầu một mutex làm đối số. Tại sao? Theo như tôi có thể nói, tôi sẽ tạo ra một mutex chỉ để sử dụng như đối số đó? Mutex đó phải làm gì?
Tôi đang đọc lên pthread.h
; các hàm điều kiện liên quan đến biến điều kiện (như pthread_cond_wait(3)
) yêu cầu một mutex làm đối số. Tại sao? Theo như tôi có thể nói, tôi sẽ tạo ra một mutex chỉ để sử dụng như đối số đó? Mutex đó phải làm gì?
Câu trả lời:
Đó chỉ là cách các biến điều kiện (hoặc ban đầu) được triển khai.
Mutex được sử dụng để bảo vệ chính biến điều kiện . Đó là lý do tại sao bạn cần khóa nó trước khi bạn chờ đợi.
Chờ đợi sẽ "nguyên tử" mở khóa mutex, cho phép người khác truy cập vào biến điều kiện (để báo hiệu). Sau đó, khi biến điều kiện được báo hiệu hoặc phát đến, một hoặc nhiều luồng trong danh sách chờ sẽ được đánh thức và mutex sẽ bị khóa một cách kỳ diệu cho luồng đó.
Bạn thường thấy thao tác sau với các biến điều kiện, minh họa cách chúng hoạt động. Ví dụ sau đây là một luồng công nhân được đưa ra công việc thông qua tín hiệu đến một biến điều kiện.
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
do the work.
unlock mutex.
clean up.
exit thread.
Công việc được thực hiện trong vòng lặp này với điều kiện là có sẵn khi chờ đợi trở lại. Khi luồng đã được gắn cờ để dừng thực hiện công việc (thường bởi một luồng khác thiết lập điều kiện thoát sau đó khởi động biến điều kiện để đánh thức luồng này), vòng lặp sẽ thoát, mutex sẽ được mở khóa và luồng này sẽ thoát.
Mã ở trên là một mô hình người tiêu dùng duy nhất vì mutex vẫn bị khóa trong khi công việc đang được thực hiện. Đối với một biến thể nhiều người tiêu dùng, bạn có thể sử dụng, làm ví dụ :
thread:
initialise.
lock mutex.
while thread not told to stop working:
wait on condvar using mutex.
if work is available to be done:
copy work to thread local storage.
unlock mutex.
do the work.
lock mutex.
unlock mutex.
clean up.
exit thread.
cho phép người tiêu dùng khác nhận được công việc trong khi người này đang làm việc.
Biến điều kiện giúp bạn giảm bớt gánh nặng của việc bỏ phiếu một số điều kiện thay vì cho phép một luồng khác thông báo cho bạn khi có điều gì đó cần xảy ra. Một chủ đề khác có thể nói rằng chủ đề đó có sẵn như sau:
lock mutex.
flag work as available.
signal condition variable.
unlock mutex.
Đại đa số những gì thường được gọi là đánh thức giả thường nói chung luôn luôn là do nhiều luồng đã được báo hiệu trong phạm vi của chúng pthread_cond_wait
cuộc gọi (phát sóng), một người sẽ quay lại với mutex, thực hiện công việc, sau đó chờ lại.
Sau đó, luồng tín hiệu thứ hai có thể đi ra khi không có việc gì phải làm. Vì vậy, bạn phải có thêm một biến chỉ ra rằng công việc nên được thực hiện (điều này vốn đã được bảo vệ bằng mutex với cặp condvar / mutex ở đây - các luồng khác cần thiết để khóa mutex trước khi thay đổi nó).
Đó là về mặt kỹ thuật có thể cho một thread để trở về từ một chờ đợi điều kiện mà không bị đuổi bởi một tiến trình khác (đây là một wakeup giả mạo chính hãng) nhưng, trong tất cả nhiều năm tôi làm việc trên pthreads, cả về phát triển / dịch vụ của mã và như một người dùng trong số họ, tôi chưa bao giờ nhận được một trong số này. Có lẽ đó chỉ là do HP đã triển khai tốt :-)
Trong mọi trường hợp, cùng một mã xử lý trường hợp sai sót cũng xử lý các đánh thức giả mạo chính hãng cũng vì cờ có sẵn cho công việc sẽ không được đặt cho những trường hợp đó.
do something
bên trong while
vòng lặp?
Một biến điều kiện khá hạn chế nếu bạn chỉ có thể báo hiệu một điều kiện, thông thường bạn cần xử lý một số dữ liệu liên quan đến điều kiện được báo hiệu. Việc báo hiệu / đánh thức phải được thực hiện một cách nguyên tử để đạt được điều đó mà không đưa ra các điều kiện chủng tộc, hoặc quá phức tạp
pthreads cũng có thể cung cấp cho bạn, vì lý do kỹ thuật, một sự thức tỉnh giả . Điều đó có nghĩa là bạn cần kiểm tra một vị ngữ, vì vậy bạn có thể chắc chắn tình trạng thực sự đã được báo hiệu - và phân biệt điều đó với một đánh thức giả. Kiểm tra một điều kiện như vậy liên quan đến việc chờ đợi nó cần được bảo vệ - vì vậy một biến điều kiện cần một cách để chờ / thức dậy nguyên tử trong khi khóa / mở khóa một mutex bảo vệ điều kiện đó.
Hãy xem xét một ví dụ đơn giản khi bạn được thông báo rằng một số dữ liệu được tạo ra. Có thể một luồng khác đã tạo một số dữ liệu mà bạn muốn và đặt một con trỏ tới dữ liệu đó.
Hãy tưởng tượng một luồng sản xuất cung cấp một số dữ liệu cho một luồng tiêu dùng khác thông qua một con trỏ 'some_data'.
while(1) {
pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
char *data = some_data;
some_data = NULL;
handle(data);
}
Bạn tự nhiên nhận được rất nhiều điều kiện cuộc đua, điều gì sẽ xảy ra nếu luồng khác đã làm some_data = new_data
ngay sau khi bạn thức dậy, nhưng trước khi bạn làmdata = some_data
Bạn thực sự không thể tạo mutex của riêng bạn để bảo vệ trường hợp này .eg
while(1) {
pthread_cond_wait(&cond); //imagine cond_wait did not have a mutex
pthread_mutex_lock(&mutex);
char *data = some_data;
some_data = NULL;
pthread_mutex_unlock(&mutex);
handle(data);
}
Sẽ không hoạt động, vẫn có cơ hội xảy ra tình trạng chạy đua giữa lúc thức dậy và chộp lấy mutex. Đặt mutex trước pthread_cond_wait không giúp ích gì cho bạn, vì bây giờ bạn sẽ giữ mutex trong khi chờ đợi - tức là nhà sản xuất sẽ không bao giờ có thể lấy được mutex. (lưu ý, trong trường hợp này bạn có thể tạo biến điều kiện thứ hai để báo hiệu cho nhà sản xuất rằng bạn đã hoàn thànhsome_data
- mặc dù điều này sẽ trở nên phức tạp, đặc biệt là nếu bạn muốn nhiều nhà sản xuất / người tiêu dùng.)
Vì vậy, bạn cần một cách để giải phóng nguyên tử / lấy mutex khi chờ đợi / thức dậy từ điều kiện. Đó là những gì biến điều kiện pthread làm, và đây là những gì bạn làm:
while(1) {
pthread_mutex_lock(&mutex);
while(some_data == NULL) { // predicate to acccount for spurious wakeups,would also
// make it robust if there were several consumers
pthread_cond_wait(&cond,&mutex); //atomically lock/unlock mutex
}
char *data = some_data;
some_data = NULL;
pthread_mutex_unlock(&mutex);
handle(data);
}
(nhà sản xuất đương nhiên sẽ cần phải thực hiện các biện pháp phòng ngừa tương tự, luôn bảo vệ 'some_data' với cùng một mutex và đảm bảo rằng nó không ghi đè some_data nếu hiện tại some_data! = NULL)
while (some_data != NULL)
là một vòng lặp do-while để nó chờ biến điều kiện ít nhất một lần?
while(some_data != NULL)
nên while(some_data == NULL)
?
Các biến điều kiện POSIX là không trạng thái. Vì vậy, trách nhiệm của bạn là duy trì nhà nước. Vì trạng thái sẽ được truy cập bởi cả hai luồng chờ và luồng xử lý các luồng khác dừng chờ, nên nó phải được bảo vệ bởi một mutex. Nếu bạn nghĩ rằng bạn có thể sử dụng các biến điều kiện mà không có mutex, thì bạn đã không nắm bắt được rằng các biến điều kiện là không trạng thái.
Các biến điều kiện được xây dựng xung quanh một điều kiện. Chủ đề chờ trên một biến điều kiện đang chờ một số điều kiện. Chủ đề mà các biến điều kiện tín hiệu thay đổi điều kiện đó. Ví dụ, một luồng có thể đang chờ một số dữ liệu đến. Một số chủ đề khác có thể nhận thấy rằng dữ liệu đã đến. "Dữ liệu đã đến" là điều kiện.
Đây là cách sử dụng cổ điển của một biến điều kiện, được đơn giản hóa:
while(1)
{
pthread_mutex_lock(&work_mutex);
while (work_queue_empty()) // wait for work
pthread_cond_wait(&work_cv, &work_mutex);
work = get_work_from_queue(); // get work
pthread_mutex_unlock(&work_mutex);
do_work(work); // do that work
}
Xem làm thế nào các chủ đề đang chờ làm việc. Công việc được bảo vệ bởi một mutex. Chờ đợi giải phóng mutex để một luồng khác có thể cung cấp cho chủ đề này một số công việc. Đây là cách nó sẽ được báo hiệu:
void AssignWork(WorkItem work)
{
pthread_mutex_lock(&work_mutex);
add_work_to_queue(work); // put work item on queue
pthread_cond_signal(&work_cv); // wake worker thread
pthread_mutex_unlock(&work_mutex);
}
Lưu ý rằng bạn cần mutex để bảo vệ hàng đợi công việc. Lưu ý rằng chính biến điều kiện không biết liệu có hoạt động hay không. Nghĩa là, một biến điều kiện phải được liên kết với một điều kiện, điều kiện đó phải được duy trì bởi mã của bạn và vì nó được chia sẻ giữa các luồng, nên nó phải được bảo vệ bởi một mutex.
Không phải tất cả các hàm biến điều kiện đều yêu cầu một mutex: chỉ các hoạt động chờ thực hiện. Các hoạt động tín hiệu và phát sóng không yêu cầu một mutex. Một biến điều kiện cũng không liên quan vĩnh viễn với một mutex cụ thể; các mutex bên ngoài không bảo vệ các biến điều kiện. Nếu một biến điều kiện có trạng thái bên trong, chẳng hạn như một chuỗi các luồng chờ, thì điều này phải được bảo vệ bằng một khóa bên trong bên trong biến điều kiện.
Các hoạt động chờ kết hợp một biến điều kiện và một mutex, bởi vì:
Vì lý do này, hoạt động chờ đợi làm đối số cả mutex và condition: để nó có thể quản lý việc chuyển nguyên tử của một luồng từ sở hữu mutex sang chờ, để luồng không trở thành nạn nhân của điều kiện cuộc đua đánh thức bị mất .
Một điều kiện cuộc đua đánh thức bị mất sẽ xảy ra nếu một luồng từ bỏ một mutex, và sau đó chờ đợi một đối tượng đồng bộ hóa không trạng thái, nhưng theo cách không phải là nguyên tử: tồn tại một cửa sổ thời gian khi luồng không còn khóa và có chưa bắt đầu chờ đợi trên đối tượng. Trong cửa sổ này, một luồng khác có thể đi vào, làm cho điều kiện chờ đợi thành đúng, báo hiệu sự đồng bộ hóa không trạng thái và sau đó biến mất. Đối tượng không trạng thái không nhớ rằng nó đã được báo hiệu (nó không trạng thái). Vì vậy, chủ đề ban đầu đi ngủ trên đối tượng đồng bộ hóa không trạng thái và không thức dậy, mặc dù điều kiện mà nó cần đã trở thành sự thật: mất thức tỉnh.
Các hàm chờ biến điều kiện tránh đánh thức bị mất bằng cách đảm bảo rằng chuỗi cuộc gọi được đăng ký để bắt kịp thức dậy một cách đáng tin cậy trước khi nó từ bỏ mutex. Điều này là không thể nếu hàm chờ biến điều kiện không lấy mutex làm đối số.
Tôi không tìm thấy các câu trả lời khác ngắn gọn và dễ đọc như trang này . Thông thường mã chờ đợi trông giống như thế này:
mutex.lock()
while(!check())
condition.wait()
mutex.unlock()
Có ba lý do để bọc wait()
trong một mutex:
signal()
trướcwait()
và chúng tôi sẽ bỏ lỡ sự thức dậy này.check()
phụ thuộc vào sửa đổi từ một chủ đề khác, vì vậy dù sao bạn cũng cần loại trừ lẫn nhau.Điểm thứ ba không phải lúc nào cũng là một mối quan tâm - bối cảnh lịch sử được liên kết từ bài viết đến cuộc trò chuyện này .
Đánh thức giả thường được đề cập liên quan đến cơ chế này (tức là chuỗi chờ được đánh thức mà không signal()
được gọi). Tuy nhiên, các sự kiện như vậy được xử lý bởi các vòng lặp check()
.
Các biến điều kiện được liên kết với một mutex bởi vì đó là cách duy nhất nó có thể tránh được cuộc đua mà nó được thiết kế để tránh.
// incorrect usage:
// thread 1:
while (notDone) {
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable
pthread_mutex_unlock(&mutex);
if (ready) {
doWork();
} else {
pthread_cond_wait(&cond1); // invalid syntax: this SHOULD have a mutex
}
}
// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);
Now, lets look at a particularly nasty interleaving of these operations
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable;
pthread_mutex_unlock(&mutex);
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond1);
if (ready) {
pthread_cond_wait(&cond1); // uh o!
Tại thời điểm này, không có luồng nào sẽ báo hiệu biến điều kiện, vì vậy thread1 sẽ đợi mãi, mặc dù bảo vệReadyToRunVariable nói rằng nó đã sẵn sàng!
Cách duy nhất để giải quyết vấn đề này là các biến điều kiện sẽ giải phóng mutex một cách nguyên tử trong khi đồng thời bắt đầu chờ đợi biến điều kiện. Đây là lý do tại sao hàm cond_wait yêu cầu một mutex
// correct usage:
// thread 1:
while (notDone) {
pthread_mutex_lock(&mutex);
bool ready = protectedReadyToRunVariable
if (ready) {
pthread_mutex_unlock(&mutex);
doWork();
} else {
pthread_cond_wait(&mutex, &cond1);
}
}
// signalling thread
// thread 2:
prepareToRunThread1();
pthread_mutex_lock(&mutex);
protectedReadyToRuNVariable = true;
pthread_cond_signal(&mutex, &cond1);
pthread_mutex_unlock(&mutex);
Mutex được cho là bị khóa khi bạn gọi pthread_cond_wait
; Khi bạn gọi nó là nguyên tử, cả hai đều mở khóa mutex và sau đó chặn với điều kiện. Khi điều kiện được báo hiệu, nó sẽ khóa lại và trả lại.
Điều này cho phép thực hiện lập lịch dự đoán nếu muốn, trong đó luồng xử lý tín hiệu có thể đợi cho đến khi mutex được giải phóng để xử lý và sau đó báo hiệu điều kiện.
Tôi đã thực hiện một bài tập trong lớp nếu bạn muốn một ví dụ thực tế về biến điều kiện:
#include "stdio.h"
#include "stdlib.h"
#include "pthread.h"
#include "unistd.h"
int compteur = 0;
pthread_cond_t varCond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex_compteur;
void attenteSeuil(arg)
{
pthread_mutex_lock(&mutex_compteur);
while(compteur < 10)
{
printf("Compteur : %d<10 so i am waiting...\n", compteur);
pthread_cond_wait(&varCond, &mutex_compteur);
}
printf("I waited nicely and now the compteur = %d\n", compteur);
pthread_mutex_unlock(&mutex_compteur);
pthread_exit(NULL);
}
void incrementCompteur(arg)
{
while(1)
{
pthread_mutex_lock(&mutex_compteur);
if(compteur == 10)
{
printf("Compteur = 10\n");
pthread_cond_signal(&varCond);
pthread_mutex_unlock(&mutex_compteur);
pthread_exit(NULL);
}
else
{
printf("Compteur ++\n");
compteur++;
}
pthread_mutex_unlock(&mutex_compteur);
}
}
int main(int argc, char const *argv[])
{
int i;
pthread_t threads[2];
pthread_mutex_init(&mutex_compteur, NULL);
pthread_create(&threads[0], NULL, incrementCompteur, NULL);
pthread_create(&threads[1], NULL, attenteSeuil, NULL);
pthread_exit(NULL);
}
Nó dường như là một quyết định thiết kế cụ thể chứ không phải là một nhu cầu khái niệm.
Theo các tài liệu pthreads, lý do khiến mutex không bị tách rời là có sự cải thiện hiệu suất đáng kể bằng cách kết hợp chúng và họ hy vọng rằng vì điều kiện chủng tộc thông thường nếu bạn không sử dụng mutex, dù sao thì nó cũng sẽ luôn được thực hiện.
https://linux.die.net/man/3/pthread_cond_wait
Các tính năng của Mutexes và Biến điều kiện
Nó đã được đề xuất rằng việc mua lại và phát hành mutex được tách rời khỏi điều kiện chờ đợi. Điều này đã bị từ chối vì đó là bản chất kết hợp của hoạt động, trên thực tế, tạo điều kiện cho việc triển khai thời gian thực. Những triển khai đó về nguyên tử có thể di chuyển một luồng ưu tiên cao giữa biến điều kiện và mutex theo cách trong suốt đối với người gọi. Điều này có thể ngăn chặn các chuyển đổi ngữ cảnh bổ sung và cung cấp khả năng thu được một mutex xác định hơn khi luồng chờ được báo hiệu. Do đó, các vấn đề công bằng và ưu tiên có thể được giải quyết trực tiếp bằng kỷ luật lập lịch. Hơn nữa, điều kiện chờ hoạt động hiện tại phù hợp với thực tế hiện có.
Có hàng tấn exeges về điều đó, nhưng tôi muốn tóm tắt nó bằng một ví dụ sau đây.
1 void thr_child() {
2 done = 1;
3 pthread_cond_signal(&c);
4 }
5 void thr_parent() {
6 if (done == 0)
7 pthread_cond_wait(&c);
8 }
Có gì sai với đoạn mã? Chỉ cần suy ngẫm phần nào trước khi đi trước.
Vấn đề là thực sự tinh tế. Nếu cha mẹ gọi
thr_parent()
và sau đó bác bỏ giá trị của done
nó, nó sẽ thấy rằng đó là 0
và do đó cố gắng đi ngủ. Nhưng ngay trước khi nó gọi chờ đi ngủ, bố mẹ bị gián đoạn giữa các dòng 6-7 và con chạy. Đứa trẻ thay đổi biến trạng thái
done
thành 1
và tín hiệu, nhưng không có luồng nào đang chờ và do đó không có luồng nào được đánh thức. Khi cha mẹ chạy lại, nó ngủ mãi, điều này thực sự rất nghiêm trọng.
Điều gì nếu chúng được thực hiện trong khi có được khóa riêng lẻ?