Giải thuật Peterson do Gary Peterson ủể xuất năm 1981 cho bài toỏn ủoạn nguy hiểm. Cựng với giải thuật Dekker, giải thuật Peterson là giải phỏp thuộc nhúm phần mềm, tức là giải
phỏp phỏp khụng ủũi hỏi sự hỗ trợ từ phớa phần cứng hay hệ ủiều hành. So với giải thuật Dekker, giải thuật Peterson dễ hiểu hơn và ủược trỡnh bày ởủõy ủể ủại diện cho nhúm giải phỏp phần mềm.
Giải thuật Peterson ủược ủề xuất ban ủầu cho bài toỏn ủồng bộ hai tiến trỡnh. Giả sử cú hai tiến trỡnh P0 và P1 thực hiện ủồng thời với một tài nguyờn chung và một ủoạn nguy hiểm chung. Mỗi tiến trỡnh thực hiện vụ hạn và xen kẽ giữa ủoạn nguy hiểm với phần cũn lại của tiến trỡnh.
Giải thuật Peterson yờu cầu hai tiến trỡnh trao ủổi thụng tin với nhau qua hai biến chung. Biến thứ nhất int turn xỏc ủịnh ủến lượt tiến trỡnh nào ủược vào ủoạn nguy hiểm. Biến thứ hai bao gồm hai cờ cho mỗi tiến trỡnh bool flag[2], trong ủú flag[i] = true nếu tiến trỡnh thứ i yờu cầu ủược vào ủoạn nguy hiểm.
Giải thuật Peterson ủược thể hiện trờn hỡnh sau: Ầ bool flag[2]; int turn; void P0(){ //tiến trỡnh P0 for(;;){ //lặp vụ hạn flag[0]=true; turn=1;
while(flag[1] && turn==1);//lặp ủến khi ủiều kiện khụng thỏa <đoạn nguy hiểm> flag[0]=false; <Phần cũn lại của tiến trỡnh> } } void P1(){ //tiến trỡnh P1 for(;;){ //lặp vụ hạn flag[1]=true; turn=0;
while(flag[0] && turn==0);//lặp ủến khi ủiều kiện khụng thỏa <đoạn nguy hiểm> flag[1]=false; <Phần cũn lại của tiến trỡnh> } } void main(){ flag[0]=flag[1]=0; turn=0; //tắt tiến trỡnh chớnh, chạy ủồng thời hai tiến trỡnh P0 và P1 StartProcess(P0); StartProcess(P1); }
Cú thể nhận thấy giải thuật Peterson thỏa món cỏc yờu cầu ủối với giải phỏp cho ủoạn nguy hiểm (yờu cầu sinh viờn thử chứng minh như bài tập nhỏ).
Việc sử dụng giải thuật Peterson trờn thực tế tương ủối phức tạp. Ngoài ra nhúm giải phỏp này ủũi hỏi tiến trỡnh ủang yờu cầu vào ủoạn nguy hiểm phải nằm trong trạng thỏi chờ ủợi tớch cực (busy waiting). Chờủợi tớch cực là tỡnh trạng chờủợi trong ủú tiến trỡnh vẫn phải sử dụng CPU ủể kiểm tra xem cú thể vào ủoạn nguy hiểm hay chưa. đối với giải thuật Peterson, tiến trỡnh phải lặp ủi lặp lại thao tỏc kiểm tra trong vũng while trước khi vào ủược
ủoạn nguy hiểm, và do vậy gõy lóng phớ thời gian CPU.
2.4.4. Giải phỏp phần cứng
Phần cứng mỏy tớnh cú thểủược thiết kếủể giải quyết vấn ủề loại trừ tương hỗ và ủoạn nguy hiểm. Giải phỏp phần cứng thường dễ sử dụng và cú tốc ủộ tốt. Dưới ủõy, ta sẽ xem xột hai giải phỏp thuộc nhúm phần cứng.
2.4.4.1. Cấm cỏc ngắt
Trong trường hợp mỏy tớnh chỉ cú một CPU, tại mỗi thời ủiểm chỉ một tiến trỡnh ủược thực hiện. Tiến trỡnh ủang cú CPU sẽ thực hiện cho ủến khi tiến trỡnh ủú gọi dịch vụ hệ ủiều hành hoặc bị ngắt. Như vậy, ủề giải quyết vấn ủề ủoạn nguy hiểm ta chỉ cần cấm khụng ủể
xẩy ra ngắt trong thời gian tiến trỡnh ủang ở trong ủoạn nguy hiểm ủể truy cập tài nguyờn.
điều này ủảm bảo tiến trỡnh ủược thực hiện trọn vẹn ủoạn nguy hiểm và khụng bị tiến trỡnh khỏc vào ủoạn nguy hiểm trong thời gian ủú.
Mặc dự ủơn giản, việc cấm ngắt làm giảm tớnh mềm dẻo của hệ ủiều hành, cú thể ảnh hưởng tới khả năng ủỏp ứng cỏc sự kiện cần ngắt. Ngoài ra, giải phỏp cấm ngắt khụng thể sử
dụng ủối với mỏy tớnh nhiều CPU. Trong khi cấm ngắt ở CPU này, tiến trỡnh vẫn cú thểủược cấp CPU khỏc ủể vào ủoạn nguy hiểm. Việc cấm ngắt ủồng thời trờn tất cả CPU ủũi hỏi nhiều thời gian ủể gửi thụng ủiệp tới tất cả CPU, làm chậm việc vào ủoạn nguy hiểm.
2.4.4.2. Sử dụng lệnh mỏy ủặc biệt
Giải phỏp thứ hai là phần cứng ủược thiết kế cú thờm một số lệnh mỏy ủặc biệt. Cú nhiều dạng lệnh mỏy như vậy. Ở ủõy, ta sẽ xem xột một dạng lệnh tiờu biểu cú tớnh ủại diện cho lệnh mỏy dựng ủểủồng bộ tiến trỡnh.
Nguyờn tắc chung của giải phỏp này là hai thao tỏc kiểm tra giỏ trị và thay ủổi giỏ trị
cho một biến (một ụ nhớ), hoặc cỏc thao tỏc so sỏnh và hoỏn ủổi giỏ trị hai biến, ủược thực hiện trong cựng một lệnh mỏy và do vậy sẽủảm bảo ủược thực hiện cựng nhau mà khụng bị
xen vào giữa. đơn vị thực hiện khụng bị xen vào giữa như vậy ủược gọi là thao tỏc nguyờn tử
(atomic). Ta sẽ gọi lệnh như vậy là lệnh Ộkiểm tra và xỏc lậpỢ Test_and_Set. Lụ gic của lệnh Test_and_Set ủược thể hiện trờn hỡnh sau:
bool Test_and_Set(bool& val) {
bool temp = val; val = true; return temp;
}
Hỡnh 2.10. định nghĩa lệnh Test_and_Set
Ta cú thể sử dụng lệnh Test_and_Set ủể giải quyết vấn ủề ủoạn nguy hiểm ủồng thời cho n tiến trỡnh ký hiệu P(1) ủến P(n) như sau:
Ầ
const int n; //n là số lượng tiến trỡnh
bool lock;
void P(int i){ //tiến trỡnh P(i)
for(;;){ //lặp vụ hạn while(Test_and_Set(lock));//lặp ủến khi ủiều kiện khụng thỏa <đoạn nguy hiểm> lock = false; <Phần cũn lại của tiến trỡnh> } } void main(){ lock = false; //tắt tiến trỡnh chớnh, chạy ủồng thời n tiến trỡnh StartProcess(P(1)); ... StartProcess(P(n)); }
Hỡnh 2.11. Loại trừ tương hỗ sử dụng lệnh mỏy ủặc biệt Test_and_Set
Cú thể dễ dàng kiềm tra ủiều kiện loại trừ tương hỗủược bảo ủảm khi sử dụng giải phỏp với Test_and_Set như trờn. Thật vậy, tiến trỡnh chỉ cú thể vào ủược ủoạn giới hạn nếu lock=false. Do việc kiểm tra giỏ trị lock và thay ủổi lock=true ủược ủảm bảo thực hiện cựng nhau nờn tiến trỡnh ủầu tiờn kiểm tra thấy lock=false sẽủảm bảo thay ủổi lock thành true trước khi tiến trỡnh khỏc kiểm tra ủược biến này. điều này ủảm bảo duy nhất một tiến trỡnh vào
ủược ủoạn nguy hiểm.
Ngoài ra, tiến trỡnh ở ngoài ủoạn nguy hiểm khụng cú khả năng ảnh hưởng tới giỏ trị
của lock và do vậy khụng thể ngăn cản tiến trỡnh khỏc vào ủoạn nguy hiểm. Giải phỏp sử dụng lệnh phần cứng ủặc biệt cú một sốưu ủiểm sau: -Việc sử dụng tương ủối ủơn giản và trực quan.
-Giải phỏp cú thể dựng ủể ủồng bộ nhiều tiến trỡnh, tất cả ủề sử dụng chung lệnh Test_and_Set trờn một biến chung gắn với một tài nguyờn chung.
-Cú thể sử dụng cho trường hợp ủa xử lý với nhiều CPU nhưng cú bộ nhớ chung. Cần lưu ý là trong trường hợp này, mặc dự hai CPU cú thể cựng thực hiện lệnh Test_and_Set nhưng do hai lệnh cựng truy cập một biến chung nờn việc thực hiện vẫn diễn ra tuần tự.
Bờn cạnh ủú, giải phỏp dựng lệnh phần cứng cũng cú một số nhược ủiểm:
-Chờ ủợi tớch cực. Tiến trỡnh muốn vào ủoạn nguy hiểm phải liờn tục gọi lệnh Test_and_Set trong vũng lặp while cho tới khi nhận ủược kết quả lock=false.
-Việc sử dụng lệnh Test_and_Set cú thể gõy ủúi. Trong trường hợp cú nhiều tiến trỡnh cựng chờủể vào ủoạn giới hạn, việc lựa chọn tiến trỡnh tiếp theo khụng theo quy luật nào và cú thể làm cho một số tiến trỡnh khụng bao giờ vào ủược ủoạn giới hạn.
2.4.5. Cờ hiệu (semaphore)
Một giải phỏp loại trừ tương hỗ khỏc khụng phụ thuộc vào sự hỗ trợ của phần cứng (dưới dạng cỏc lệnh kiểm tra và xỏc lập trỡnh bày ở trờn), ủồng thời tương ủối dễ sử dụng là
cờ hiệu hay ủốn hiệu (semaphore) do Dijkstra ủề xuất.
Cờ hiệu S là một biến nguyờn ủược khởi tạo một giỏ trị ban ủầu nào ủú, bằng khả năng phục vụ ủồng thời của tài nguyờn. Trừ thao tỏc khởi tạo, giỏ trị của cờ hiệu S chỉ cú thể thay
ủổi nhờ gọi hai thao tỏc là Wait và Signal. Cỏc tài liệu trước ủõy sử dụng ký hiệu P - viết tắt cho từ Ộkiểm traỢ trong tiếng đức - cho thao tỏc Wait, và V - viết tắt của từ ỘtăngỢ trong tiếng
đức - cho thao tỏc Signal. Hai thao tỏc này cú ý nghĩa như sau:
- Wait(S): Giảm S ủi một ủơn vị. Nếu giỏ trị của S õm sau khi giảm thỡ tiến trỡnh gọi thao tỏc P(S) sẽ bị phong tỏa (blocked). Nếu giỏ trị của S khụng õm, tiến trỡnh sẽủược thực hiện tiếp.
- Signal(S): Tăng S lờn một ủơn vị. Nếu giỏ trị S nhỏ hơn hoặc bằng 0 sau khi tăng thỡ một trong cỏc tiến trỡnh ủang bị phong tỏa (nếu cú) sẽủược giải phúng và cú thể thực hiện tiếp.
điểm ủầu tiờn cần lưu ý là hai thao tỏc Wait và Signal là những thao tỏc nguyờn tử, khụng bị phõn chia. Trong thời gian tiến trỡnh thực hiện thao tỏc như vậy ủể thay ủổi giỏ trị cờ
hiệu, thao tỏc sẽ khụng bị ngắt giữa chừng.
Khi tiến trỡnh bị phong tỏa, tiến trỡnh sẽ chuyển sang trạng thỏi chờủợi cho ủến khi hết bị phong tỏa mới ủược phộp thực hiện tiếp. Cỏc tiến trỡnh bị phong tỏa ủược xếp vào hàng ủợi của cờ hiệu.
Xõy dựng cờ hiệu
Cờ hiệu cú thểủược xõy dựng dưới dạng một cấu trỳc trờn ngụn ngữ C với hai thao tỏc Wait và Signal như sau:
struct semaphore { int value;
process *queue;//danh sỏch chứa cỏc tiến trỡnh bị phong tỏa
};
void Wait(semaphore& S) {
S.value--;
if (S.value < 0) {
Thờm tiến trỡnh gọi Wait vào S.queue
block(); //phong tỏa tiến trỡnh
} void Signal(semaphore& S) { S.value++; if (S.value <= 0) { Lấy một tiến trỡnh P từ S.queue wakeup(P); } } Hỡnh 2.12. định nghĩa cờ hiệu trờn C
Mỗi cờ hiệu cú một giỏ trị và một danh sỏch queue chứa tiến trỡnh bị phong tỏa. Thao tỏc block() trong Wait phong tỏa tiến trỡnh gọi Wait và thao tỏc wakeup() trong Signal khụi phục tiến trỡnh phong tỏa về trạng thỏi sẵn sàng. Hai thao tỏc block và wakeup ủược thực hiện nhờ những lời gọi hệ thống của hệủiều hành.
Danh sỏch tiến trỡnh bị phong tỏa queue cú thể xõy dựng bằng những cỏch khỏc nhau, chẳng hạn dưới dạng danh sỏch kết nối cỏc PCB của tiến trỡnh. Việc chọn một tiến trỡnh từ
danh sỏch khi thực hiện Signal cú thể thực hiện theo nguyờn tắc FIFO hoặc theo những thứ tự
khỏc. Cần lưu ý rằng việc sử dụng FIFO sẽủảm bảo ủiều kiện chờủợi cú giới hạn, tức là tiến trỡnh chỉ phải chờủợi một thời gian giới hạn trước khi vào ủoạn nguy hiểm.
Cỏch sử dụng cờ hiệu
Cờ hiệu ủược tiến trỡnh sử dụng ủể gửi tớn hiệu trước khi vào ủoạn nguy hiểm và sau khi ra khỏi ủoạn nguy hiểm. đầu tiờn, cờ hiệu ủược khởi tạo một giỏ trị dương hoặc bằng khụng. Mỗi cờ hiệu với giỏ trịủầu dương thường dựng ủể kiểm soỏt việc truy cập một tài nguyờn với khả năng phục vụ ủồng thời một số lượng hữu hạn tiến trỡnh. Vớ dụ, tại mỗi thời ủiểm tài nguyờn như mỏy in chỉ cho phộp một tiến trỡnh ghi thụng tin, và cờ hiệu dựng cho mỏy in
ủược khởi tạo bằng 1.
Khi tiến trỡnh cần truy cập tài nguyờn, tiến trỡnh thực hiện thao tỏc Wait của cờ hiệu tương ứng. Nếu giỏ trị cờ hiệu õm sau khi giảm cú nghĩa là tài nguyờn ủược sử dụng hết khả
năng và tại thời ủiểm ủú khụng phục vụ thờm ủược nữa. Do vậy, tiến trỡnh thực hiện Wait sẽ
bị phong tỏa cho ủến khi tài nguyờn ủược giải phúng. Nếu tiến trỡnh khỏc thực hiện Wait trờn cờ hiệu, giỏ trị cờ hiệu sẽ giảm tiếp. Giỏ trị tuyệt ủối của cờ hiệu õm tương ứng với số tiến trỡnh bị phong tỏa.
Sau khi dựng xong tài nguyờn, tiến trỡnh thực hiện thao tỏc Signal trờn cựng cờ hiệu. Thao tỏc này tăng giỏ trị cờ hiệu và cho phộp một tiến trỡnh ủang phong tỏa ủược thực hiện tiếp.
Nhờ việc phong tỏa cỏc tiến trỡnh chưa ủược vào ủoạn nguy hiểm, việc sử dụng cờ hiệu trỏnh cho tiến trỡnh khụng phải chờủợi tớch cực và do vậy tiết kiệm ủược thời gian sử dụng CPU.
Loại trừ tương hỗ ủược thực hiện bằng cỏch sử dụng cờ hiệu như thể hiện trờn hỡnh 2.13.
Ầ
const int n; //n là số lượng tiến trỡnh
semaphore S = 1;
void P(int i){ //tiến trỡnh P(i)
for(;;){ //lặp vụ hạn Wait(S); <đoạn nguy hiểm> Signal(S); <Phần cũn lại của tiến trỡnh> } } void main(){ //tắt tiến trỡnh chớnh, chạy ủồng thời n tiến trỡnh StartProcess(P(1)); ... StartProcess(P(n)); } Hỡnh 2.13. Loại trừ tương hỗ sử dụng cờ hiệu 2.4.6. Một số bài toỏn ủồng bộ
để tiện minh họa cho việc sử dụng giải phỏp ủồng bộ, trong phần này sẽ trỡnh bày một số bài toỏn ủồng bộ kinh ủiển. đõy là những bài toỏn hoặc cú ứng dụng trờn thực tế hoặc khụng cú ứng dụng nhưng rất thuận tiện trong việc mụ tả vấn ủề xảy ra giữa cỏc quỏ trỡnh
ủồng thời và do vậy thường ủược sử dụng ủể minh họa hoặc kiểm tra giải phỏp ủồng bộ húa.
a. Bài toỏn triết gia ăn cơm
Tỡnh huống trong bài toỏn như sau. Cú năm triết gia ngồi trờn ghế quanh một bàn trũn, giữa bàn là thức ăn, xung quanh bàn cú năm chiếc ủũa sao cho bờn phải mỗi người cú một ủũa và bờn trỏi cú một ủũa (2.14).
Hỡnh 2.14. Bài toỏn cỏc triết gia ăn cơm
Cụng việc của mỗi triết gia là suy nghĩ. Khi người nào ủú cần ăn, người ủú dừng suy nghĩ, nhặt hai chiếc ủũa nằm gần hai phớa và ăn. Triết gia cú thể nhặt hai chiếc ủũa theo thứ tự
bất kỳ nhưng bắt buộc phải nhặt từng chiếc một với ủiều kiện ủũa khụng nằm trong tay người khỏc. Sau khi cầm ủược cả hai ủũa, triết gia bắt ủầu ăn và khụng ủặt ủũa xuống trong thời gian ăn. Sau khi ăn xong, triết gia ủặt hai ủũa xuống bàn và suy nghĩ tiếp.
Cú thể coi năm triết gia như năm tiến trỡnh ủồng thời với tài nguyờn nguy hiểm là ủũa và ủoạn nguy hiểm là ủoạn dựng ủũa ủểăn.
Cờ hiệu cho phộp giải quyết bài toỏn này như sau. Mỗi ủũa ủược biểu diễn bằng một cờ
hiệu. Thao tỏc nhặt ủũa sẽ gọi Wait ủối với cờ hiệu tương ứng và thao tỏc ủặt ủũa xuống bàn gọi Signal. Toàn bộ giải phỏp sử dụng cờ hiệu cho bài toỏn triết gia ăn cơm thể hiện trờn 2.15.
Ầ
semaphore chopstick[5] = {1,1,1,1,1,1};
void Philosopher(int i){ //tiến trỡnh P(i)
for(;;){ //lặp vụ hạn Wait(chopstick[i]); //lấy ủũa bờn trỏi Wait(chopstick[(i+1)%5]); //lấy ủũa bờn phải <Ăn cơm> Signal(chopstick[(i+1)%5]); Signal(chopstick[i]); <Suy nghĩ> } } void main(){ // chạy ủồng thời 5 tiến trỡnh StartProcess(Philosopher(1)); ... StartProcess(Philosopher (5)); }
Hỡnh 2.15. Bài toỏn triết gia ăn cơm sử dụng cờ hiệu
Lưu ý rằng giải phỏp trờn 2.15 cho phộp thực hiện loại trừ tương hỗ, tức là trỏnh trường hợp hai triết gia cựng nhặt một ủũa. Tuy nhiờn, giải phỏp này cú thể gõy bế tắc nếu cả năm người cựng nhặt ủược ủũa bờn trỏi và khụng thể tiếp tục vỡ ủũa bờn phải ủó bị người bờn phải