Tải bản đầy đủ (.doc) (20 trang)

Tạo lập thread - bài tập thread docx

Bạn đang xem bản rút gọn của tài liệu. Xem và tải ngay bản đầy đủ của tài liệu tại đây (219.73 KB, 20 trang )

I.1 Tạo lập thread.
I.1.1. Thiết lập Thuộc tính cho thread.
1) pthread_attr_init(&attr);
2) pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE);
3) pthread_create(&threads[0], &attr, inc_count, (void*)&thread_ids[0]);
Lệnh 1) tạo ra biến attr để nhớ thuộc tính, lệnh 2) nạp thuộc tính cho nó – ở đây thuộc tính
được chỉ định là “PTHREAD_CREATE_JOINABLE”, lệnh 3) chỉ định thread có thuộc
tính attr. Chúng ta thường sử dụng thuộc tính ngầm định NULL, chỉ thị này cho phép các
thread “join” được với trình chính.
I.1.2. Tạo lập, Giao nhiệm vụ & Truyền tham số cho thread.
I.1.2.A Các bước cơ bản.
Để tạo lập và điều kiển một thread ta cần phải thực hiện 5 bước cơ bản sau
1. Mô tả thread: void * proc_1(int x,y)
2. Đăng ký thread: pthread_t tid;
3. Khởi tạo (đăng ký thuộc tính), giao nhiệm vụ, và truyền dữ liệu.
pthread_create(&tid, NULL, &proc_1,
(void*)&arg);
4. Xác định thread pthread_equal(pthread_self(), )
5. Thu hồi pthread_join(tid, NULL);
“Mô tả thread” là việc tạo ra một trình con. Nó chính là thread sau này. Các lệnh còn lại
chỉ là thủ tục để cho trình con này biến thành thread, tức là chạy song song và có các thuộc
tính cần thiết.
Các lệnh này gồm có
1) Lệnh khởi tạo và giao nhiệm vụ pthread_create
Lệnh này kết nối một thread với một hàm con (được gọi tắt là thread
1
, hay hàm được kích
hoạt thành thread). Lệnh còn đảm nhận trách nhiệm truyền dữ liệu cho thread. Dữ liệu phải
được khai báo trước và để ở đâu đó trong bộ nhớ. Tuy nhiên không phải các dữ liệu này
được truyền cho thread, mà chỉ là địa chỉ của những khối dữ liệu cần phải truyền, mà thôi.
Các địa chỉ là các con trỏ – nó có thể là địa chỉ của một vùng nhớ, và cũng có thể là biến


địa chỉ chứa địa chỉ của vùng nhớ có dữ liệu. Ví dụ, nếu cần truyền giá trị chứa trong biến
X thì (do địa chỉ của X là &X) chỉ thị truyền là (void*)&X. Còn nếu địa chỉ của dữ liệu
được để trong con trỏ P thì chỉ thị truyền là (void*)P. Do chỉ được truyền cho địa chỉ, cho
nên, để có thể sử dụng được những dữ liệu này, thread phải tự định dạng các dữ liệu này
sao cho hợp với dạng vốn có của chúng.
2) Lệnh kết thúc hoạt động pthread_join
Lệnh này đóng vai trò đồng bộ thread với trình chính
2
. Số là, tiến trình chính, sau khi khởi
tạo ra các thread, thì vẫn tiếp tục hoạt động – và các thread do nó tạo ra thì chạy song song
với nó. Nếu trình chính mà muốn sử dụng một kết quả nào đó do thread tính ra, thì nó phải
biết được thời điểm mà thread kết thúc hoạt động của mình. Trình chính không thể tự biết
được thời điểm các thread kết thúc hoạt động, vì vậy cần phải có một lệnh – và đó chính là
lệnh pthread_join – bắt trình chính (nếu nó đến lệnh này sớm hơn) phải chờ để thread kết
thúc công việc của mình. Điều này hàm ý, ở lệnh tiếp theo sau lệnh nó, thread tương ứng
đã không còn nữa
3
. Lệnh pthread_joint chỉ thị chờ cho từng thread một, và chính vì vậy
trong chỉ thị lệnh phải có thông báo tên của thread cần “join” với trình chính – hay trình
chính phải chờ.
I.1.2.B Ví dụ về tạo lập & truyền dữ liệu cho thread.
Ví dụ 1. Tạo lập thread.
Trình chính (thread chính) tạo ra một thread để làm một việc gì đó cho mình. Trong ví dụ
này thread in “Hello, ” còn trình chính in “World!”. Do thread chạy song song với trình
chính mà các từ in ra không theo thứ tự nhất định, nó có thể là “Hello World” và cũng có
thể là “World Hello”.
#include <iostream.h>
#include <pthread.h>
void * print_hello(void *args) { cout<<"Hello, ";return NULL;}
void *main()

1
Trên thực tế mỗi khi lệnh này được gọi thì có 2 thread được tạo ra. Thread “con” dùng để chạy
trình con và thread “mẹ” để chạy trình chính. Như vậy thread và trình chính đều là các thread.
Thread “con” có thể có được những tính chất như quyền ưu tiên, lịch hoạt động, thời điểm kết thúc
hoạt động (đồng bộ hay không)
2
Thời điểm kết thúc hoạt động của một thread có thể là đồng bộ hay là không đồng bộ. Nếu một
thread được khai báo là “kết hợp” (joined) thì nó chỉ thực sự kết thúc hoạt động (hủy bỏ hoàn toàn
bộ nhớ do nó tạo ra như stack và header) khi lệnh trình chính gọi lệnh “pthread_join()”. Ngược lại
nếu nó thuộc loại “tách rời” (detached) thì nó sẽ giải phóng tất cả bộ nhớ do nó tạo ra khi nó kết
thúc hoạt động
3
Lưu ý lệnh này không hủy bỏ vùng bộ nhớ mà thread đã xin cấp.
{ pthread_t tid; pthread_create(&tid, NULL, &print_hello, NULL);
cout<<"World! ";
pthread_join(tid, NULL);
return NULL;
}
Ví dụ 2. Truyền dữ liệu.
Do đặc thù của tính song-song trong xử lý mà chúng ta cần phải tin chắc rằng khoảng thời
gian kể từ lúc một thread được tạo ra cho đến lúc nó hoạt động là phải được khống chế và
là phải là không đáng kể. Điều này hàm ý là tổng thời gian thực tế cần thiết để truyền dữ
liệu cho nó phải là rất ít – diễn ra trong khoảng một vài lệnh asambly. Như vậy, giao nhiệm
vụ cho thread cũng như cấp dữ liệu cho nó đều là thông qua con trỏ. Con trỏ sẽ trỏ tới địa
chỉ, nơi lưu giữ trình con cần phải được kích hoạt để chạy song song; con trỏ sẽ trỏ tới
vùng dữ liệu nơi lưu giữ dữ liệu cần cho thread. Tóm lại nguyên lý truyền dữ liệu cho một
thread là “truyền con trỏ đến vùng dữ liệu”.
Sau đây là ví dụ về việc truyền nói trên. Hàm được kích hoạt là “Say”. Đó là một hàm
được khai báo ở dạng con trỏ, và chính con trỏ này sẽ được truyền cho thread để thông báo
hàm cần phải được kích hoạt. Hàm này sẽ in ra màn hình lời chào bằng ở các ngôn ngữ

khác nhau. Dữ liệu cần truyền cho thread để nó hoạt động là các dòng chữ và chúng ta
không truyền hẳn cho các thread cả dòng chữ, bở dung lượng dữ liệu phải truyền sẽ rất
nhiều vì các dòng này tương đối dài, mà chúng ta chỉ truyền cho nó địa chỉ nơi lưu giữ các
lời chào đó. Trong ví dụ sau, trình chính tạo ra và truyền dữ liệu cho 3 thread chạy song
song. Mỗi thread in ra một dòng thông báo trên màn hình. Các dòng chữ đó là các câu chào
(đã để sẵn trong bộ nhớ, trong một mảng) ở các ngôn ngữ khác nhau: "English: Hello
World!", "French: Bonjour, le monde!", "Vietnam: Chao tat ca". Để làm được việc này,
mỗi thread nhận được địa chỉ ứng với vị trí dòng chữ tương ứng.
#include <iostream.h>
#include "pthread.h"
#include <string>
using namespace std;
#define NUM 3
string mess[NUM];
void *Say(void *messages)
{ cout<< (char *) messages << "\n";
pthread_exit(NULL);
return NULL;
}
void *main(int argc, char *argv[])
{ pthread_t threads[NUM];
mess [0] = "English: Hello World!";
mess [1] = "French: Bonjour, le monde!";
mess [2] = "Vietnam: Chao tat ca!";
for(int t=0;t<NUM;t++) {pthread_create(&threads[t], NULL, &Say,(void *)
mess[t].c_str());}
for(t=0;t<NUM;t++) pthread_join(threads[t],NULL);
return NULL;
}
Dữ liệu có thể được lưu dưới dạng mảng và truyền cho các thread chỉ số của mảng. Việc

làm này thường dùng khi sử dụng mảng thread, nó cho phép chúng ta sử dụng vòng lặp
“for” để tạo lập giao nhiệm vụ và truyền dữ liệu cho các thread.
#include <stdio.h>
#include <malloc.h>
#include "pthread.h"
#define NUM 3
char *messages[NUM];
void *PrintHello(void *threadid)
{ int *id_ptr, taskid;
id_ptr = (int *) threadid;
taskid = *id_ptr;
printf("Thread %d: %s\n", taskid, messages[taskid]);
pthread_exit(NULL);
return NULL;
}
void main(int argc, char *argv[])
{ pthread_t threads[NUM];
int *taskids[NUM];
int t;
messages[0] = "English: Hello World!";
messages[1] = "French: Bonjour, le monde!";
messages[2] = "Vietnam: Chao tat ca!";
for(t=0;t<NUM;t++)
{ taskids[t] = (int *) malloc(sizeof(int)); *taskids[t] = t;
pthread_create(&threads[t], NULL, PrintHello, (void *) taskids[t]);
}
for(t=0;t<NUM;t++) pthread_join(threads[t],NULL);
}
/*Result:
Thread 0: English: Hello World!

Thread 1: French: Bonjour, le monde!
Thread 2: Vietnam: Chao tat ca!
Press any key to continue
*/
Lưu ý rằng, việc truyền trực tiếp giá trị “t” sẽ dẫn đến sai lầm, bởi vì, vào thời điểm thread
hoạt động thì (do vòng for làm nó tăng lên) giá trị “t” ấy, có thể, đã khác đi rồi!
Như vậy khúc trình sau đây là sai.
for(t=0;t<NUM;t++)
{ printf("Creating thread %d\n", t);
rc = pthread_create(&threads[t], NULL, PrintHello, (void *) &t);
if (rc) { printf("ERROR; return code from pthread_create() is %d\n", rc); exit(-1); }
}
Ví dụ 3. Truyền nhiều dữ liệu cho thread.
Có thể truyền nhiều dữ liệu bằng cách liệt kê, và cũng có thể truyền con trỏ – trỏ tới tới
một cấu trúc hay một Object.
Ví dụ 4. Truyền dữ liệu cho thread thông qua việc truyền con trỏ tới một cấu trúc có chứa
nhiều trường với định dạng khác nhau.
#include <iostream.h>
#include "pthread.h"
#include <string>
#define NUM 3
using namespace std;
string mess[NUM];
struct wrapper
{ int id;
string message;
} data_array [NUM];
void * PrintHello(void* data)
{ cout<< ((wrapper*)data)->id << " > ";
cout<< ((wrapper*)data)->message.c_str() << "\n";

return NULL;
}
void *main()
{ pthread_t threads[NUM];
for(int t=0;t<NUM;t++)
{
switch (t){
case 0: data_array[0].message="Thread 0";
case 1: data_array[1].message="Thread 1";
case 2: data_array[2].message="Thread 2";
}
data_array[t].id = t;
pthread_create(&threads[t], NULL, PrintHello, (void *) & data_array[t]);
}
for(t=0;t<NUM;t++){pthread_join(threads[t], NULL);}
return NULL;
}
/*Result:
0 > Thread 0
1 > Thread 1
2 > Thread 2
*/
Ví dụ 5. Các thread có thể là hàm của một class, nhưng hàm này phải thuộc loại được khai
báo “static”.
#include <iostream.h>
#include "pthread.h"
class Hello{
public:
static void* print(void* msg);
};

void* main()
{ pthread_t thread1, thread2;
char *message1="Hello ";
char *message2="World \n";
pthread_create(&thread1,NULL,Hello::print,(void *)message1);
pthread_create(&thread2,NULL,Hello::print,(void *)message2);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return NULL;
}
void * Hello::print(void* msg)
{ char *message;
message= (char *)msg;
cout<<message;
return NULL;
}
/*Result:
Hello World
*/
Ví dụ 6. Các thread có thể là hàm của một object, nhưng hàm này phải thuộc loại được
khai báo “static”.
#include <iostream.h>
#include "pthread.h"
class Hello{
public:
static void* print(void* msg);
};
void * main()
{ pthread_t thread1, thread2;
Hello *H=new Hello;

char *message1="Hello ";
char *message2="World \n";
pthread_create(&thread1,NULL,H->print,(void *)message1);
pthread_create(&thread2,NULL,H->print,(void *)message2);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return NULL;
}
void * Hello::print(void* msg)
{ char *message;
message= (char *)msg;
cout <<message;
return NULL;
}
/*
Hello World
*/
Ví dụ 7. Thủ tục của một Object được kích hoạt thành thread.
#include <iostream.h>
#include "pthread.h"
class ThreadClass;
struct wrapper
{ ThreadClass* Me;
int a;
};
class ThreadClass
{public:
int a;
static void* function(void* arg)
{ wrapper* Unpack = (wrapper*) arg;

ThreadClass* Hidden= Unpack->Me; Hidden->a = Unpack->a;
cout<<"I got: " << Hidden->a <<"\n";
return NULL;
}
};
int main()
{ pthread_t Thread1; int one = 1;
ThreadClass Test;
wrapper ToPass; ToPass.Me = &Test; ToPass.a = one;
pthread_create(&Thread1, NULL, Test.function, &ToPass);
pthread_join(Thread1, NULL);
return 0;
}

/*Result:
I got: 1
Press any key to continue
*/
Ví dụ 8. Sử dụng thủ tục “static” để gọi các thủ tục “non static”.
#include <iostream.h>
#include "pthread.h"
class Hello{
public:
void* print(void* msg);
static void * test(void* arg); //the entry static function
};
struct pack_data{
Hello * self;
char * str_content;
}; //the wrapper

void *main()
{ pthread_t thread1, thread2;
Hello *H= new Hello;
struct pack_data *pk1 = new pack_data; pk1->self=H; pk1->str_content="Hello ";
struct pack_data *pk2 = new pack_data; pk2->self=H; pk2->str_content="World \n";
pthread_create(&thread1,NULL,H->test,(void *)pk1);
pthread_create(&thread2,NULL,H->test,(void *)pk2);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
return NULL;
}
void * Hello::print(void* msg) //non static function
{ char *message;
message= ((pack_data *)msg)->str_content;
cout<< message;
return NULL;
}
void * Hello::test(void *arg) //static function
{ Hello *ptr= new Hello;
ptr= ((pack_data *)arg)->self;
ptr->print(arg); //call a static function
return NULL;
}

/*Result:
Hello World
Press any key to continue
*/
I.1.2.C Tạo mảng thread
Ví dụ 1. Trong ví dụ sau một mảng thread được tạo ra và được giao nhiệm vụ. Nhiệm vụ

chỉ đơn thuần là mỗi thread chạy 4 lần, in ra màn hình số thứ tự của mình, rồi kết thúc
nhiệm.
#include <stdlib.h>
#include <malloc.h>
#include <stdio.h>
#include "pthread.h"
void *do_one_thing(void *pnum)
{ int i; for (i = 0; i < 4; i++) { printf("%d is doing a thing\n", (int *)pnum); }return
NULL;}
void main()
{ int i, n=5;
pthread_t *thread1;
int* Nthread;
thread1 = (pthread_t *) calloc(n, sizeof(pthread_t)); Nthread = (int *) calloc(n,
sizeof(int));
for(i=0;i<n;i++){
Nthread[i]=i;
pthread_create(&(thread1[i]), NULL, &do_one_thing, (void*)Nthread[i]);
}
for(i=0;i<n;i++){pthread_join(thread1[i], NULL);}
free(Nthread); free(thread1);
return;
}
Ví dụ 2. Trong ví dụ này mảng 10 Object, đánh số từ 0 9, thuộc Class “ThreadClass” được
tạo ra. Trong mỗi Object có một hàm được kích hoạt thành thread – khi chạy nó sẽ in ra
màn hình số hiệu của Object. Có thể quan niệm 10 Object này tồn tại và vận hành độc lâp.
Lệnh pthread_join chỉ kết thúc hoạt động của các hàm, trong khi 10 Object này vẫn tồn tại.
#include <iostream.h>
#include "pthread.h"
class ThreadClass

{public:
int a;
static void* function(void* arg)
{ cout<<" I am Object: " << (int)((ThreadClass *) arg)->a <<"\n";return NULL;}
};
int main()
{ int i;
pthread_t Thread[10];
ThreadClass Test[10]; for (i=0; i<10;i++){Test[i].a=i;}
for (i=0; i<10;i++){ pthread_create(&Thread[i], NULL, Test[i].function, &Test[i]); }
for (i=0; i<10;i++) pthread_join(Thread[i], NULL);
return 0;
}

I.1.3. Sự khác nhau giữa thread và trình con.
I.1.3.A Thread là trình con được kích hoạt để chạy song song.
Trong ví dụ sau hai thread là các trình con first_function(), và second_function() nhưng
được kích hoạt để chạy song song.
pthread_create(&thread1, NULL, &first_function, &i);
pthread_create(&thread2, NULL, &second_function, &j);
Để thấy được dấu hiệu chạy song song của hai thread này, chúng ta ra lệnh cho mỗi thread
liên tiếp in ra 10 lần thông báo về mình. Khi chạy khúc trình sau chúng ta sẽ sớm nhận ra
các thông báo hiện ra trên màn hình đan xen kẽ vào nhau, chứng tỏ chúng hoạt động thật
sự song song.
Trong ví dụ này final_function() là trình con bình thường và chỉ khi nào gọi tới nó mới
chạy. Các thread được khởi tạo, và mỗi thread làm một việc. Khi kết thúc công việc,
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
kết quả tính toán của 2 thread được trình con gom lại. Khi trình con gom kết quả thì các
thread đã không còn tồn tại, tuy vậy kết quả của nó được để ở trong 2 ô nhớ “i” và “j” đã

tạo sẵn ngay trong trình chính. Trình final_function() sẽ dùng các kết quả này.
1. #include <stdio.h>
2. #include "pthread.h"
3. int i=0, j=0;
4. void * first_function(void *num)
5. { int x,y,z=0;
6. int * a =(int*)num;
7. for(x=0; x< 10; x++)
8. { printf("Inside the first_function %d\n",*a);
9. for (y=0; y< 1000; y++) z =z+1;
10. (*a)++;
11. }
12. num=a;
13. return NULL;
14. }
15. void * second_function(void *num)
16. { int x,y,z=0;
17. int * a =(int*)num;
18. for(x=0; x< 20; x++)
19. { printf("Inside the second_function %d\n",*a);
20. for (y=0; y< 1000; y++) z =z+1;
21. (*a)++;
22. }
23. num=a;
24. return NULL;
25. }
26. void final_function(int first, int second)
27. { int final_score;
28. final_score = first + second ;
29. printf("In final: first = %d, second = %d and final_score = %d\n", first, second,

final_score);
30. }
31. void main()
32. { pthread_t thread1, thread2;
33. pthread_create(&thread1, NULL, &first_function, &i);
34. pthread_create(&thread2, NULL, &second_function, &j);
35. pthread_join(thread1, NULL);
36. pthread_join(thread2, NULL);
37. final_function(i,j);
38. }
Kết quả hiện ra là
In final : first = 10, second = 20 and final_score = 30
đó là số lần lặp các vòng lặp trong hai thread. Sự việc sẽ không còn đơn giản như vậy nữa
nếu chúng ta bỏ các lệnh (35) và (36) và khi ấy trên màn hình sẽ hiện ra thông báo:
In final : first = 0, second = 0 and final_score = 0
Thông báo này chứng tỏ các thread, tuy có chạy, nhưng không rõ các thông số về chúng ra
sao.
I.1.3.B Thread, Trình con, và Màn hình.
Trong ví dụ sau một trình con được tạo ra để in lên màn hình các số từ 0 đến 9. Một lần gọi
nó, trên màn hình hiện lên các số từ 0 đến 9; hai lần gọi nó, trên màn hình sẽ hiện ra hai lần
in các số này. Các kết quả xuất hiện trên màn hình của “mỗi trình con” tương ứng với thứ
tự gọi chúng (được phân biệt dựa và các số “b” và “c” dùng để đánh dấu các trình con).
(1) #include <stdio.h>
(2) #include <pthread.h>
(3) void* do_loop(void* data)
(4) { int i, j;
(5) int me = *((int*)data); /* identifying number */
(6) for (i=0; i<10; i++) {for (j=0; j<500000; j++) /* delay loop */ ;
(7) printf("'%d' - Got '%d'\n", me, i);} return NULL;
(8) }

(9) int main(int argc, char* argv[])
(10) {
(11) int b = 2; /* thread 2 identifying number */
(12) int c = 3; /* thread 2 identifying number */
(13) do_loop((void*)&b);
(14) do_loop((void*)&c);
(15) return 0;}
Bây giờ chúng ta kích hoạt trình con này, biến nó thành một thread chạy song song với
trình chính “main()”.
(16) #include <stdio.h>
(17) #include <pthread.h>
(18) void* do_loop(void* data)
(19) { int i, j;
(20) int me = *((int*)data); /* thread identifying number */
(21) for (i=0; i<10; i++) {for (j=0; j<500000; j++) /* delay loop */ ;
(22) printf("'%d' - Got '%d'\n", me, i);}
(23) pthread_exit(NULL); /* exit the thread */
(24) return NULL;}
(25) int main(int argc, char* argv[])
(26) {
(27) int thr_id; /* thread ID for the newly created thread */
(28) pthread_t p_thread; /* thread's structure */
(29) int a = 1; /* thread 1 identifying number */
(30) int b = 2; /* thread 2 identifying number */
(31) thr_id = pthread_create(&p_thread, NULL, do_loop, (void*)&a);
(32) do_loop((void*)&b);
(33) pthread_join(p_thread, NULL);
(34) return 0;}
Trên màn hình các thông báo in ra xen kẽ chứng tỏ trình con và thread chạy song song với
nhau. Nếu chúng ta đảo thứ tự 2 lệnh (32) và (33) thì sự kiện song song sẽ chẳng còn! Điều

này xảy ra là bởi “thread” kết thúc hoạt động của mình trước, rồi sau đó trình con mới bắt
đầu hoạt động. Nếu chúng ta bỏ cả hai lệnh (32) và (33) thì chỉ có mỗi trình “thread” là
hoạt động, nhưng trên màn hình chúng ta lại không thấy “thread” in ra các dòng thông báo
chứng tỏ về sự tồn tại của nó. Trên thực tế thì “thread” quả là có hoạt động và các thông
báo của nó có gửi ra màn hình, nhưng màn hình là thiết bị tuần tự và nó thì không thể biết
được khi nào sẽ phải cho hiện các thông số ấy ra! Chúng ta có thể nhận thấy sự hoạt động
của “thread” bằng cách chỉ bỏ mỗi lệnh (33), khi ấy, trình “tuần tự” sẽ buộc màn hình phải
cho hiện các thông báo của nó ra – và, vì thế mà, chúng ta cũng trông thấy các thông báo
của “thread”!
I.2 Thread Deadlock.
I.2.1. Khái niệm Deadlock và Stavation.
Định nghĩa
- “Deadlock” là hiện tượng khi mà tất cả các thread mãi mãi ở vào trạng thái chờ vô
thời hạn.
- “Stavation” là hiện tượng khi có một hoặc một vài thread nào đó bị rơi vào trạng
thái chờ vô thời hạn.
Khi hệ thống rơi vào trạng thái “stavation” thì, nhìn từ bên ngoài vào, nó vẫn hoạt động.
Và phải quan sát từ bên trong ta mới thấy có một hoặc một vài thread nào đó không sao
chạy được. Điều này giống như việc một người nào đó trong xã hội bị quên lãng – trong
khi những người khác thì liên tục được hưởng sự ưu đãi, đến độ, xã hội làm ra đến đâu thì
họ hưởng thụ hết đến đấy; và anh bạn bị quên lãng, không phải là không có quyền, mà là
bởi “số” anh ta rơi vào tình trạng bị chờ “vô vọng!” Nếu như mỗi thread được giao cho
một nhiệm vụ nào đấy thì các thread bị quên lãng, cho dù đã làm xong phần việc của mình
nhưng, vẫn không sao giao nộp được kết quả. Sự nguy hiểm của hiện tượng “stave” là ở
chỗ rất khó phát hiện ra nó, bởi vì, khi các thread khác kết thúc phần việc của mình, thì khi
ấy thread quên lãng mới được thực hiện (trong khi năng lực của chúng là như nhau). Việc
làm này dẫn đến hoặc là các tiến trình bị quên lãng sẽ mãi mãi chờ nếu như các tiến trình
còn lại không ngừng hoạt động, hoặc là (trong trường hợp tốt nhất) làm cho tốc độ sử lý
của hệ thống bị chậm lại – nếu như kết quả chung là phụ thuộc vào các các thread bình
thường và các thread bị quên lãng. Hiện tượng “stave” còn phức tạp ở chỗ các thread bị

quên lãng có thể không cố định, và không thể biết trước.
Trong phần sau đây chúng ta chỉ giới hạn xét về hiện tượng deadlock. Hiện tượng này có
thể phân làm 2 loại. Loại thứ nhất là deadlock chắc chắn, và loại thứ hai là deadlock tiềm
năng. Deadlock chắc chắn là deadlock sẽ xảy ra mỗi khi chúng ta chạy trình. Trong khi
deadlock tiềm năng thì không phải lần chạy trình nào cũng xảy ra. Bản chất xuất hiện
deadlock tiềm năng phần nhiều là ở chỗ có sự khác nhau về tốc độ của các thread khi chạy
trình; tuy nhiên, cũng có thể do các yếu tố phụ như không đủ bộ nhớ.
I.2.1.A Deadlock do dùng chung thiết bị tuần tự.
Hiện tượng tắc nghẽn xuất hiện do việc dùng chung thiết bị “tuần tự”, có thể là cùng ghi
vào một ô nhớ hay là truyền dữ liệu vào một dây dẫn. Bản chất của việc này là thiết bị
dùng chung ấy không thể sử dụng “song song”, tức là tại cùng một thời điểm chỉ có thể có
một tiến trình sử dụng được nó mà thôi.
Một tiến trình nào đó muốn sử dụng một thiết bị tuần tự, nó phải – Lock(), tức là thông báo
thiết bị “bận” và như vậy cấm các tiến trình khác vào. Sau khi đã hoàn tất công việc, nó
phải giải phóng – Unlock() thiết bị chuyển nó về trạng thái “không bận”, để cho các tiến
trình khác còn có thể dùng được. Qui trình này, xem ra có vẻ hợp lý, nhưng nó chứa đựng
nguy cơ gây ra deadlock. Ví dụ sau, một phần nào, minh họa điều ấy.
Có hai thread cùng sử dụng các thiết bị “tuần tự” (T) và (D). Chúng đều tuân thủ “qui
trình” sử dụng thiết bị.
Có thể có hai khả năng xảy ra:
- Tiến trình này sử dụng thiết bị thì tiến trình kia đã dùng xong. (T
2
< T
1
)
- Thời gian sử dụng thiết bị của hai tiến trình gần như trùng nhau (T
1
= T
2
).

Đối với khả năng thứ nhất deadlock không xảy ra, nhưng đối với khả năng thứ hai thì có.
Tiến trình “A” chiếm được quyền sử dụng thiết bị (D) và tiến trình “B” thì thiết bị (T). Khi
ấy việc các tiến trình cố gắng để chiếm quyền sử dụng thiết bị còn lại là không thể vì
chúng chưa được giải phóng! Khi ấy cả hai tiến trình đều chờ nhau, và hệ thống bị rơi vào
tình trạng chờ vô thời hạn – deadlock.
Đoạn trình sau minh họa sơ đồ trên. sẽ xảy ra deadlock nếu như “A” và “B” cùng lúc thực
hiện việc đặt khóa: “A” đặt khóa “lock1” bằng lệnh (1), và “B” đặt khóa “lock2” bằng lệnh
(5). Như vậy cả “A” lẫn “B” sẽ chẳng thể qua được lệnh tiếp theo. Xác suất để cả hai
thread “A” và “B” cùng thực hiện các lệnh như trên vào một thời điểm là rất nhỏ, tuy nhiên
chúng vẫn có thể xảy ra. Và, sự nguy hiểm là ở chỗ, khi xảy ra thì chương trình sẽ ngừng
hoạt động.
void *A()
{
(1) pthread_mutex_lock(&lock1);
// “A” làm việc gì đấy ở đây rất lâu.
(2) pthread_mutex_lock(&lock2);

(3) pthread_mutex_unlock(&lock2);
(4) pthread_mutex_unlock(&lock1);
}
void *B()
{
(5) pthread_mutex_lock(&lock2);
// “B” làm việc gì đấy ở đây rất lâu.
(6) pthread_mutex_lock(&lock1);

(7) pthread_mutex_unlock(&lock1);
(8) pthread_mutex_unlock(&lock2);
}


main()
{
pthread_create(&thread1, NULL, A, NULL);
pthread_create(&thread2, NULL, B, NULL);

}
Trên thực tế, khó có thể phân định gianh giới của deadlock tiềm năng và deadlock thật sự.
Xác suất xảy ra deadlock tiềm năng có thể rất bé và cũng có thể rất lớn. Trong ví dụ trên
“A” có thể đến trước “B” khá lâu và thực hiện lệnh (1) khóa “lock1”, nhưng nó lại làm gì
đó rất lâu ở sau lệnh (1). Thời gian này đủ để “B” đi đến lệnh (5), và “B” sẽ thực hiện
ngay lệnh đặt khóa “lock2”. Như vậy “A” sẽ chẳng bao giờ đi qua được lệnh (2) còn “B”
thì không qua được lệnh (6).
Câu chuyện khôi hài sau giúp chúng ta phần nào hiểu được khái niệm deadlock. Chuyện kể
rằng, có hai triết gia cùng ngồi ăn cơm mà chỉ có 1 đôi đũa. Vì chỉ có một đôi đũa, họ chỉ
có thể ăn một cách tuần tự, hết người này – dùng cả 2 chiếc đũa để gắp – rồi đến người kia.
Sự việc tồi tệ xảy ra khi họ tranh nhau, và mỗi người chỉ cầm được 1 chiếc đũa – khi ấy thì
chẳng ai gắp được cả! Chuyện này được kể ra là bởi, đó chính là tình trạng của hai thread
“A” và “B” nói trên. Ở đây hai chiếc đũa là hai khóa “lock1” và “lock2”. Nếu thread nào
đó, “A” chẳng hạn, mà giành được cả 2 chiếc đũa – tức là khóa được cả hai “lock1” và
“lock2” – thì nó sẽ đi được đến cuối chương trình: từ lệnh (1) tới hết lệnh (4); và phải sau
đó mới tới lượt thread còn lại.
Khúc trình sau là một ví dụ về sự xuất hiện các triết gia nói trên.
Nếu có hai thread “A” và “B” cùng đồng thời đi đến khúc trình này, thì kết quả là “A” đặt
khóa mutex_2 cản trở “”B, còn “B” thì đặt khóa mutext_1 cản trở “A”.

(1) pthread_mutex_lock(&mutex_1);
(2) while (pthread_mutex_trylock(&mutex_2))
4

{

(3) pthread_mutex_unlock(&mutex_1);

/* nơi tắc nghẽn */

(4) pthread_mutex_lock(&mutex_1);
}

(5) pthread_mutex_unlock(&mutex_2);
(6) pthread_mutex_unlock(&mutex_1);
4
pthread_mutex_trylock(&mutex_2) – trả lại ngay giá trị: 0 – “true” – nếu thành công trong việc đặt
khóa mutext_2 và giá trị 1 nếu không làm được điều ấy.

Trong kịch bản trên, giả sử “A” là thread vượt qua được mutex_1 (lệnh 1), khi ấy nó sẽ
vượt qua được (lệnh 2) khóa mutex_2 và thực hiện các lệnh (3 4). Ở (lệnh 3) “A” mở khóa
mutex_1 và “B” nào đó sẽ vào. “B” sẽ khóa mutex_1 lại. “B” không thực hiện các lệnh
(3 4) nhưng không thể vượt qua (lệnh 5) vì “A” khóa mutex_2, nhưng “A” không thể qua
(lệnh 6) vì “B” khóa mutex_1.
Những ai đã từng đi du ngoạn ra nước ngoài thì có thể hiểu được ví dụ trên thông qua câu
chuyện sau. Có hai du khách (mầu xanh, và mầu đỏ) cùng đi du lịch (đi từ trên xuống
dưới) qua hai quốc gia, có một vùng tranh chấp, như hình vẽ. Quốc gia thứ nhất màu xanh
với biên giới là đường dày, quốc gia thứ hai màu đỏ gạch cua với biên giới là đường mảnh
hơn. Các vị khách đều phải đến chào đức Vua – mà Vua, thì do già yếu, mỗi lần chỉ tiếp 1
vị khách, vì thế hải quan chỉ cấp ví da vào nước cho từng người thôi “người này ra khỏi
biên giới họ mới cấp ví da cho người khác vào!” Tại biên giới du khách được đóng dấu
nhập cảnh và khi ra họ sẽ được đóng dấu xuất cảnh. Để vào được phần lãnh thổ tranh chấp
du khách phải có thị thực nhập cảnh của cả hai quốc gia. Với tình trạng như hình vẽ thì du
khách mầu xanh sẽ chẳng thể vào được quốc gia mầu đỏ, và ngược lại du khách mầu đỏ thi
chẳng thể vào được quốc gia mầu xanh; bởi mỗi người đã đang ở trong một quốc gia.
I.2.2. Bữa ăn tối của 5 triết gia (Dining Philosophers).

Vào năm 1960 Edsger-Dijkstra đã đưa ra ví dụ nổi tiếng gọi là “bữa tối của các triết-gia”.
Ví dụ nói về một bữa ăn có 5 người với 5 bát cơm và chỉ có 5 chiếc đũa. Muốn gắp được
thì các triết gia phải sử dụng 1 đôi đũa, tức là 2 chiếc. Mỗi người chỉ có thể lấy đũa ở cạnh
bát mình mà thôi. Đặt giả sử cả đời các “bợm nhậu tới mức triết-gia(!)” này chỉ làm có 4
việc: (1) Nâng đũa lên, (2) Gắp một miếng thịt bỏ vào mồm, (3) Đặt đũa xuống, (4) Nhai
và nuốt miếng thịt! Quá trình tiếp theo lại tiếp diễn ra đúng như vậy: nâng đũa lên – gắp bỏ
mồm – hạ đũa xuống – nhai và nuốt; chỉ có điều là tất cả 5 “bợm” đều tranh nhau (do thiếu
đũa!) làm đúng như vậy. Do tranh nhau mà không phải lúc nào các triết-gia cũng có cái để
“nhai”, bởi vậy, phần lớn thời gian được các triết-gia dùng vào việc suy tính làm sao để
cướp được đũa.
Chúng ta mô tả quá trình trên bằng chương trình sau.
Mỗi triết gia ứng với một số nguyên: 0, 1, 2, 3, 4; và 5 chiếc đũa được mô phỏng bằng 5
mutex là chopsticks[0], chopsticks[1], chopsticks[2], chopsticks[3], chopsticks[4]. Việc lấy
đũa được mô tả bằng việc “lock” mutex chiếc đũa tương ứng, việc thả chiếc đũa xuống ứng
với lệnh “unlock”.
#include <pthread.h>
#include <stdio.h>
#include <windows.h>
#define NUM_PHILOSOPHERS 5
pthread_mutex_t chopsticks[NUM_PHILOSOPHERS];
void think () {Sleep(10);}
void eat () { /* Intentionally left blank. Simulate eating. */ }
void* philosopher (void* number)
{ int my_num = *((int*)number);
while (1)
{ think ();
/* Grab the chopsticks to my left and to my right */
pthread_mutex_lock (&chopsticks[my_num]);
pthread_mutex_lock (&chopsticks[(my_num + 1) % NUM_PHILOSOPHERS]);
eat (); printf ("Philosopher %d is eating!\n", my_num);

/* Put the chopsticks down. */
pthread_mutex_unlock (&chopsticks[(my_num + 1) % NUM_PHILOSOPHERS]);
pthread_mutex_unlock (&chopsticks[my_num]);
}
return NULL;
}
int main ()
{ int i; int a[NUM_PHILOSOPHERS];
pthread_t phils[NUM_PHILOSOPHERS];
void* return_val;
for (i = 0; i < NUM_PHILOSOPHERS; i++) pthread_mutex_init (&chopsticks[i],
NULL);
for (i = 0; i < NUM_PHILOSOPHERS; i++)
{a[i]=i;pthread_create (&phils[i], NULL, philosopher, &a[i]);}

for (i = 0; i < NUM_PHILOSOPHERS; i++) pthread_join (phils[i], &return_val);
return 0;
}
Chạy trình trên chúng ta nhận thấy là các triết gia sẽ “ăn” được một lúc thì tất cả đều dừng
lại, và dừng lại ở các thời điểm rất khác nhau. Vậy chuyện gì đã xảy ra vậy? Suy nghĩ kỹ
bài kịch bản của bài toán chúng ta thấy: rất có thể xảy ra trường hợp, khi mà cả 5 triết gia,
mỗi người đều cầm được 1 chiếc đũa của mình, thì chẳng ai ăn được! Cả 5 người đều “suy
nghĩ” để “cướp” cho được 1 chiếc đũa nữa, nhưng mà đũa thì chẳng còn! Điều này thì
tương ứng là các thread đều chạy đến lệnh
pthread_mutex_lock (&chopsticks[my_num]);
và lẽ dĩ nhiên cả 5 mutex đều đã bị locked, chẳng thể lock thêm được! Đây là hiện tượng
“tắc nghẽn” – dealock, tất cả các tiến trình đều ở trong trạng thái chờ.
Với cách phân tích trên thì sự kiện deadlock xảy ra là do “triết gia” đã tìm lấy từng cái đũa
một. Trong khi anh ta chưa kịp lấy chiếc đũa thứ 2 thì đã bị người khác lấy mất rồi. Vậy tốt
nhất, chúng ta bảo cho họ là “cùng một lúc phải lấy cả 2 chiếc đũa” – mỗi tay cầm một

chiếc!
Quyền được nhấc một lúc cả 2 chiếc đĩa được thực hiện thông qua một mutex mới.
pthread_mutex_lock (&chopstick_lock);
dùng để khóa hai lệnh “nhặt đũa”
pthread_mutex_lock (&chopstick_lock);
pthread_mutex_lock (&chopsticks[my_num]);
pthread_mutex_lock (&chopsticks[(my_num + 1) % NUM_PHILOSOPHERS]);
pthread_mutex_unlock (&chopstick_lock);
không cho ai có thể xem vào giữa. Vậy mỗi “triết gia” hoặc là tranh được quyền lấy đũa
(tức là lock thành công chopstick_lock) thì lấy cả 2 chiếc, hoặc là ngồi “suy nghĩ”.
Sau đây là đoạn trình mô phỏng bữa ăn của 5 “triết gia”.
#include <pthread.h>
#include <stdio.h>
#include <windows.h>
#define NUM_PHILOSOPHERS 5
pthread_mutex_t chopsticks[NUM_PHILOSOPHERS];
pthread_mutex_t chopstick_lock = PTHREAD_MUTEX_INITIALIZER;
void think () {Sleep(10);}
void eat () { /* Intentionally left blank. Simulate eating. */ }
void* philosopher (void* number)
{ int my_num = *((int*)number);
while (1)
{ /* First we think */
think ();
/* First get the lock on grabbing chopsticks.
Then, grab the chopstick to my left and to my right */
pthread_mutex_lock (&chopstick_lock);
pthread_mutex_lock (&chopsticks[my_num]);
pthread_mutex_lock (&chopsticks[(my_num + 1) % NUM_PHILOSOPHERS]);
/* Release the chopstick grabbing lock. */

pthread_mutex_unlock (&chopstick_lock);
eat (); printf ("Philosopher %d is eating!\n", my_num);
/* Put the chopsticks down */
pthread_mutex_unlock (&chopsticks[(my_num + 1) % NUM_PHILOSOPHERS]);
pthread_mutex_unlock (&chopsticks[my_num]);
}
return NULL;
}
int main ()
{ int i; int a[NUM_PHILOSOPHERS];
pthread_t phils[NUM_PHILOSOPHERS];
void* return_val;
for (i = 0; i < NUM_PHILOSOPHERS; i++) pthread_mutex_init (&chopsticks[i],
NULL);
for (i = 0; i < NUM_PHILOSOPHERS; i++)
{a[i]=i;pthread_create (&phils[i], NULL, philosopher, &a[i]);}
for (i = 0; i < NUM_PHILOSOPHERS; i++) pthread_join (phils[i], &return_val);
return 0;
}
Trình tuy không còn bị deadlock, nhưng có điều phải quan tâm – đó là bữa “ăn” kém vui!?
Kém vui là vì mỗi lần chỉ có một người được ăn mà thôi (bởi quyền cầm đũa cả đôi thì chỉ
có một người có – người lock thành công chopstick_lock!) Một trình song song, mà trên
thực tế lại diễn ra tuần tự như vậy, thì sẽ không còn hiệu quả.
Có thể có một phương án mới. Với phương án này các “triết gia” được dặn trước như sau.
“Cướp” đũa thì dùng 2 tay, tay trái và tay phải, mỗi tay cướp lấy 1 chiếc. Tay trái lấy chiếc
bên trái, tay phải lấy chiếc bên phải. Ông số 0, 2, 4 thì nhấc đũa bằng tay phải trước rồi
mới đến tay trái; và ông số 1, 3 thì ngược lại nhấc đũa bằng tay trái trước rồi mới đến tay
phải. Phương án này phần nào tránh được tính chất tuần tự của thuật toán trên, nhưng tính
tương đương của các triết gia thì không còn.
Việc làm trên được mô phỏng bằng trình sau

#include <pthread.h>
#include <stdio.h>
#include <windows.h>
#define NUM_PHILOSOPHERS 5
pthread_mutex_t chopsticks[NUM_PHILOSOPHERS];
pthread_mutex_t chopstick_lock = PTHREAD_MUTEX_INITIALIZER;
void think () {Sleep(10);}
void eat () { /* Intentionally left blank. Simulate eating. */ }
void* philosopher (void* number)
{
int my_num = *((int*)number);
while (1)
{ think ();
if (my_num % 2) /* If I'm an even philosopher, grab chopsticks one way. */
{ pthread_mutex_lock (&chopsticks[my_num]);
pthread_mutex_lock (&chopsticks[(my_num + 1) % NUM_PHILOSOPHERS]);
}
else /* Otherwise, grab them the other way. */
{ pthread_mutex_lock (&chopsticks[(my_num + 1) % NUM_PHILOSOPHERS]);
pthread_mutex_lock (&chopsticks[my_num]);
}
eat (); printf ("Philosopher %d is eating!\n", my_num); /* Eating */
/* Put the chopsticks down */
pthread_mutex_unlock (&chopsticks[(my_num + 1) % NUM_PHILOSOPHERS]);
pthread_mutex_unlock (&chopsticks[my_num]);
}
return NULL;
}
int main ()
{ int i; int a[NUM_PHILOSOPHERS];

pthread_t phils[NUM_PHILOSOPHERS];
void* return_val;
for (i = 0; i < NUM_PHILOSOPHERS; i++) pthread_mutex_init (&chopsticks[i],
NULL);
for (i = 0; i < NUM_PHILOSOPHERS; i++)
{a[i]=i;pthread_create (&phils[i], NULL, philosopher, &a[i]);}
for (i = 0; i < NUM_PHILOSOPHERS; i++) pthread_join (phils[i], &return_val);
return 0;
}
Như phương án trên thì các “triết gia” chắc chắn không bị chết đói (không bị deadlock),
nhưng mà có sự phân biệt họ với nhau. Do phải tuân thủ theo lời “dặn dò” mà cơ hội nhặt
đũa của họ không giống nhau, và vì thế dẫn đến tình trạng “bằng mặt mà chẳng bằng lòng”
– có lẽ người thì được ăn nhiều, kẻ thì ăn được ít!

×