Với sự phát triển của các máy tính PC, ngày càng nhiều dữ liệu được lưu trữ và được xử lý ở những máy tính cá nhân. Những dữ liệu này cần phải được chia sẻ và các tính toán ở nhiều nơi này cũng cần phải được kết hợp lại để giải quyết hiệu quả hơn những bài toán đặt ra trong thực tế. Từ đó xuất hiện mô hình tính toán phân tán [14].
Tính toán phân tán là những tính toán được thực hiện trên cơ sở kết hợp tính toán và truyền thông của hai hay nhiều máy tính trên mạng.
Mô hình tính toán phân tán có những ưu điểm:
- Cho phép chia sẻ dữ liệu được lưu ở nhiều máy tính khác nhau.
- Chia sẻ với nhau về một số chức năng chính của máy tính.
- Độ tin cậy và khả năng dung thứ lỗi cao hơn. Trong trường hợp có một máy tính bị trục trặc thì những máy tính khác có thể thay thế để hoàn thành nhiệm vụ của hệ thống.
- Tính kinh tế: thường đầu tư vào hệ phân tán sẽ thấp hơn đầu tư cho hệ tập trung.
Tuy nhiên, hệ tính toán phân tán cũng đứng trước nhiều thách thức, những vấn đề liên quan đến việc quản trị hệ thống, định vị tài nguyên, vấn đề đảm bảo an toàn, an ninh thông tin, v.v.
Xử lý trong các hệ thống phân tán không có bộ nhớ chia sẻ để trao đổi dữ liệu với nhau. Sự trao đổi được thực hiện bằng cách truyền thông điệp. Mô hình ở mức cao hơn, mô hình Client-Server cũng có thể sử dụng cơ chế này để cài đặt.
95 trang |
Chia sẻ: netpro | Lượt xem: 6437 | Lượt tải: 1
Bạn đang xem trước 20 trang tài liệu Luận văn Nghiên cứu các kiến trúc của máy tính song song, các mô hình và các thuật toán trong xử lý song song, để xem tài liệu hoàn chỉnh bạn click vào nút DOWNLOAD ở trên
ột bộ xử lý và thời điểm kết thúc của các bộ xử lý đối với bộ dữ liệu vào bất kỳ. Có hai loại thao tác khác nhau trong các thuật toán song song:
Các phép toán cơ sở như +, -, *, /, AND, OR, v.v.
Các phép toán truyền dữ liệu trên các kênh truyền.
Độ phức tạp thời gian của thuật toán song song được xác định bởi số các phép toán cơ sở và số các bước truyền tải dữ liệu giữa các bộ xử lý với nhau. Từ đó suy ra, độ phức tạp thời gian của thuật toán song song không chỉ phụ thuộc vào mô hình tính toán mà còn phụ thuộc vào số bộ xử lý được sử dụng.
Nói chung, chương trình tính toán song song thường bắt đầu bằng việc nhập dữ liệu vào bộ nhớ và kích hoạt một phần tử xử lý. Mỗi bước tính toán, phần tử xử lý này có thể đọc một số dữ liệu từ bộ nhớ, thực hiện một số phép toán cơ sở và ghi kết quả vào bộ nhớ riêng hoặc bộ nhớ chung. Đồng thời mỗi bước tính toán, một phần tử xử lý có thể kích hoạt một hay một số phần tử xử lý khác.
Thực tế thì các máy tính đều có số bộ xử lý là hữu hạn, nên những thuật toán song song không bị giới hạn chỉ có nghĩa sử dụng khi chúng có thể chuyển đổi về thuật toán song song bị giới hạn.
Có ba cách định nghĩa khái niệm [20] liên quan đến độ phức tạp của thuật toán song song:
Định nghĩa 1: Một thuật toán song song có độ phức tạp tính toán O(T) với P bộ xử lý khi nó thực hiện nhiều nhất là O(T* P) phép toán cơ sở (định lý Brent).
Định nghĩa 2: Một thuật toán song song có độ phức tạp tính toán O(T) sử dụng rất nhiều bộ xử lý để thực hiện O(e) phép toán cơ sở khi cài đặt với P bộ xử lý thì sẽ có độ phức tạp thời gian là O(ée/Pù + T).
Định nghĩa 3: Một thuật toán song song có độ phức tạp tính toán O(T) với P bộ xử lý có thể cài đặt với éP/pù , 1≤ p ≤ P bộ xử lý thì sẽ có độ phức tạp thời gian là O(p*T).
Định nghĩa 2 chỉ ra rằng khi số bộ xử lý được sử dụng giảm xuống trong một phạm vi nhất định thì thuật toán tiếp tục làm việc nhưng thời gian thực hiện sẽ tăng lên.
Định nghĩa 3 khẳng định rằng có cách để cài đặt thuật toán song song khi số các bộ xử lý được sử dụng bị giảm xuống.
Mức độ hiệu quả của thuật toán được thể hiện ở mức độ song song của thuật toán.
Mức độ song song của thuật toán là số lượng cực đại các phép toán độc lập có thể thực hiện đồng thời ở mỗi thời điểm thực hiện của thuật toán.
Ký hiệu P(W) là độ song song của thuật toán, thì thuật toán hiệu quả để giải bài toán có cỡ W là những thuật toán chỉ cần sử dụng nhiều nhất P(W) bộ xử lý.
Ngoài ra, để đánh giá được thuật toán song song chúng ta còn phải xét tới hệ số gia tốc của nó.
Hệ số gia tốc của thuật toán song song sử dụng p bộ xử lý được xác định như sau:
Sp = TS / Tp
Trong đó,
+ TS là thời gian thực hiện tính toán trên một bộ xử lý
+ Tp là thời gian thực hiện tính toán trên p bộ xử lý.
Với giả thiết là bộ xử lý tuần tự và bộ xử lý song song là như nhau.
II.2 Các mô hình lập trình song song
II.2.1 Lập trình chia sẻ bộ nhớ
Giả thiết rằng chúng ta có một hệ thống đa bộ xử lý đối xứng SMP. Đó là hệ thống trong đó tất cả các bộ xử lý là như nhau, không có những bộ xử lý đặc biệt để xử lý vào/ra, cũng không có bộ xử lý được gán cho nhiệm vụ đặc biệt nào khác. Đây là mô hình chung cho các hệ thống đa xử lý.
Để nghiên cứu về song song, chúng ta không nhất thiết phải có hệ đa bộ xử lý vật lý. Trong môi trường UNIX, chúng ta có thể tạo ra nhiều tiến trình khác nhau trong hệ thống và chúng được sử dụng để mô phỏng lập trình đa bộ xử lý. Hệ thống UNIX cung cấp những đặc tính như tín hiệu điều khiển Semaphore và bộ nhớ chia sẻ (bộ nhớ có thể chia sẻ cho các tiến trình khác nhau) [14] để các tiến trình có thể xử lý song song như chúng ta cần. Song, tốc độ xử lý bài toán khi chạy chương trình thì không tăng tốc được như mong muốn.
Một vấn đề quan trọng cần xem xét khi xử lý song song là cần bao nhiểu nút (bộ xử lý, tiến trình) để chương trình song song thực hiện hiệu quả nhất?
Nhiều chương trình được xây dựng phụ thuộc vào một cấu hình xác định. Nói chung, loại chương trình phụ thuộc như vậy là không tốt, vì khi một bộ xử lý bận thì có thể thay đổi chương trình hoặc chờ để bộ xử lý đó quay lại.
Trong những hệ thống đa nhiệm như UNIX, số bộ xử lý và số các tiến trình không nhất thiết phải bằng nhau. Hầu hết các hệ UNIX đều cho phép tạo ra một số các tiến trình bất kỳ và chúng được lập lịch cho những bộ xử lý thích hợp (sẵn sàng thực hiện chúng). Như vậy, về nguyên tắc, chương trình là tập các tiến trình và việc viết chương trình là độc lập với các bộ xử lý, việc phân chia các tiến trình cho các bộ xử lý là công việc của hệ điều hành. Tất nhiên vấn đề lập lịch trong hệ thống đa nhiệm là bài toán khó và chúng ta không thảo luận ở đây.
Trong lập trình thủ tục tuần tự (như với C, Pascal, Fortran), ta có thể mô hình lời giải một cách độc lập với các ngôn ngữ lập trình. Hầu hết các thuật toán đều dễ dàng cài đặt trên nhiều ngôn ngữ lập trình khác nhau. Điều này thực hiện được bởi hầu hết các ngôn ngữ lập trình thủ tục đều sử dụng những lệnh, cấu trúc điều khiển chuẩn như lệnh gán, rẽ nhánh if-then, các cấu trúc lặp (for, while, repeat), v.v. Tương tự như thế có thể nghĩ về các thành phần tổng quát của lập trình song song trong hệ thống bộ nhớ chia sẻ.
Trong môi trường lập trình chia sẻ bộ nhớ có hai ràng buộc quan trọng như sau:
Một tiến trình có thể chờ một khoảng thời gian bất kỳ giữa hai lệnh cần thực hiện. Giả sử bộ xử lý P thực hiện một chương trình có một 100 lệnh, bộ xử lý Q thực hiện chương trình có 10 lệnh và cùng bắt đầu thực hiện đồng thời. Thậm chí, tất các lệnh có tốc độ thực hiện như nhau cũng không thể nói rằng Q sẽ kết thúc trước P.
(ii) Không thể xem các lệnh thực hiện là nguyên tố ở mức các ngôn ngữ lập trình.
Ví dụ, một lệnh đơn giản như: a = a + 1 sẽ là một dãy từ một đến bốn lệnh trong ngôn ngữ máy. Mà ta cũng biết rằng, các tiến trình và hệ điều hành chỉ nhận biết được các câu lệnh của ngôn ngữ máy.
II.2.1.1 Lập trình chia sẻ bộ nhớ dựa vào tiến trình
Tạo lập và huỷ bỏ tiến trình
Yêu cầu đầu tiên của xử lý song song là khả năng tạo ra một số các tiến trình cần thiết cho bài toán. Tương tự là khả năng huỷ bỏ chúng khi phần việc xử lý song song kết thúc để giải phóng các tài nguyên mà các tiến trình đã chiếm giữ và không cản trở hoạt động của những tiến trình khác.
Để thêm N tiến trình, chúng ta viết:
id = create_process(N);
Lệnh này tạo thêm N tiến trình và một tiến trình cha nữa để thực hiện câu lệnh đó, kết quả là có N+1 tiến trình như nhau được tạo ra và mỗi giá trị của id được gán tương ứng cho một tiến trình.
Sử dụng những tiến trình đã được tạo ra chúng ta có thể viết chương trình song song dạng:
id = create_process(N);
switch(id){
case 0: … do NhiemVu0 …; break;
case 1: … do NhiemVu1 …; break;
case 2: … do NhiemVu2 …; break;
. . .
case N: … do NhiemVuN …; break;
}
Sau khi những công việc trên thực hiện xong, chúng ta muốn một tiến trình (như một tiến trình chủ) tiếp tục thực hiện những phần việc tuần tự còn lại, còn những tiến trình khác kết thúc. Khi đó chúng ta viết
join_process(N, id);
Chỉ tiến trình tương ứng với giá trị id còn tiếp tục hoạt động, những tiến trình là kết thúc sau lời gọi hàm trên. Nếu ta đặt sau nó một câu lệnh thì:
Lệnh này sẽ không được thực hiện cho đến khi tất cả các tiến trình đều thực hiện join_process().
Sau đó chỉ còn lại một tiến trình hoạt động, do vậy vấn đề xử lý song song không xuất hiện mà là xử lý tuần tự.
Vấn đề là khả năng các tiến trình được tạo lập nhìn thấy dữ liệu của nhau như thế nào? Một mặt một tiến trình có thể muốn giữ một phần dữ liệu cục bộ cho riêng mình, không cho những tiến trình khác nhìn thấy/truy cập tới những dữ liệu đó. Mặt khác, nó cũng muốn trao đổi thông tin với các tiến trình khác. Xử lý vấn đề che giấu hay chia sẻ thông tin như thế nào còn tuỳ thuộc vào mô hình mà chúng ta áp dụng, dựa vào tiến trình hay luồng.
Các tiến trình trong UNIX được sử dụng như các đơn vị tính toán độc lập, theo mặc định, việc tính toán và cập nhật bộ nhớ của một tiến trình không là không nhìn thấy được bởi các tiến trình khác.
Đối với các luồng, tất cả các thông tin, theo mặc định, là nhìn thấy được. Do vậy, trong mô hình này cần phải cố gắng rất nhiều để che giấu thông tin.
Bây giờ chúng ta xét một số hàm điều phối vấn đề chia sẻ bộ nhớ. Khi muốn sử dụng bộ nhớ chung, ta cần phải xin cấp phát bộ nhớ và sau khi sử dụng xong phải giải phóng chúng. Người lập trình phải có trách nhiệm giải phóng bộ nhớ chia sẻ một cách tường minh khi chúng không còn cần thiết sử dụng. Có hai hàm cơ sở:
shared(m, &id): giống như malloc(), nhưng cấp phát m byte bộ nhớ chia sẻ cho tiến trình id.
free_shm(): giải phóng bộ nhớ đã được cấp.
Ví dụ: Bài toán loại trừ nhau.
Trước tiên chúng ta xét đoạn chương trình sau:
main(){
int id, sid, *i, j;
i = (int*)shared(sizeof(int), &sid);
*i = 100; j = 100;
printf(“Before fork: %d, %d\n”, *i, j);
id = create_process(2);
*i = id; j = id * 2; // (1)
printf(“After fork: &d, %d\n”, *i, j);// (2)
join_process(3, id);
printf(“After join: &d, %d\n”, *i, j);
free_shm(sid);
}
Chúng ta hãy dự đoán xem các kết quả của hai lệnh printf(“After …”) cho kết quả như thế nào? Lưu ý rằng, i là biến chia sẻ, id là duy nhất (số hiệu) đối với mỗi tiến trình. Giả sử câu lệnh (1) thực hiện với id = 2, liệu dòng (2) có in ra là “After fork: 2, 4” hay không?
Câu trả lời là không nhất thiết. Các giá trị của *i, j in ra có thể là 2, 4; 4, 4; 6, 4; bởi vì khi tiến trình thứ i thực hiện xong câu lệnh (1), trước khi nó thực hiện (2) thì có thể các tiến trình khác đã cập nhật lại giá trị của i.
Đây là bài học quan trọng của lập trình chia sẻ bộ nhớ. Vấn đề truy cập vào bộ nhớ chia sẻ là phải có sự hợp tác chặt chẽ giữa các tiến trình. Nếu có một tiến trình truy cập vào một vùng nhớ với ý định cập nhật thì nó phải được đảm bảo rằng không một tiến trình nào khác đọc dữ liệu ở vùng đó cho đến khi việc cập nhật đó kết thúc.
Muốn giải quyết được vấn đề trên thì phải có cơ chế đảm bảo rằng, các khối lệnh của chương trình được thực thi chỉ bởi một tiến trình tại mỗi thời điểm. Nếu có một tiến trình bắt đầu vào thực hiện một khối lệnh thì những tiến trình khác không được vào khối lệnh đó. Những cơ chế như thế gọi là gài khoá (lock). Khi vào một vùng lệnh thì dùng chìa để khoá nó lại, sau đó mở khoá (unlock) khi ra khỏi vùng đó và trao chìa cho tiến trình khác có nhu cầu. Để thực hiện các yêu cầu đó chúng ta sử dụng:
init_lock(Id): Khởi động bộ khoá vùng nhớ chia sẻ, trong đó Id là tên của vùng nhớ sử dụng chung.
lock(Id): khoá lại vùng nhớ Id. Nếu một tiến trình đã dùng chìa để khoá lại một vùng nhớ chung thì những tiến trình khác muốn truy cập vào đó sẽ phải chờ. Giả thiết rằng chỉ có một chìa khoá, do vậy chỉ khi chiếc khoá đã được một tiến trình nào đó mở ra thì chìa của nó mới được giao cho tiến trình khác sử dụng.
unlock(Id): mở khoá vùng đã bị khoá và trả lại chìa cho tiến trình khác.
Sử dụng cơ chế gài khoá để viết lại chương trình trên cho đúng là như sau:
main(){
int *lock1,id, sid1, sid2, *i, j;
lock1 = (int*)shared(sizeof(int), &sid1);
init_lock(lock1);
i = (int*)shared(sizeof(int), &sid2);
*i = 100; j = 100;
printf(“Before fork: %d, %d\n”, *i, j);
id = create_process(2);
lock(lock1);
*i = id; j = id * 2; // (1)
printf(“After fork: &d, %d\n”, *i, j);// (2)
unlock(lock1);
join_process(3, id);
printf(“After join: &d, %d\n”, *i, j);
free_shm(sid1); free_shm(sid2);
}
Chúng ta nhận thấy cơ chế gài khoá giải quyết được bài toán loại trừ lẫn nhau, nghĩa là nó chỉ cho phép một tiến trình được vào thực hiện một vùng mã lệnh tại mỗi thời điểm.
Ví dụ: Cho trước một đoạn chương trình tính tổng của hai vector:
for(i = 0; i < N; i++){ // (1)
C[i] = A[i] + B[i];
}
Thực hiện song song hoá đoạn chương trình này như thế nào?
Tương tự như ví dụ nêu trên, giả sử ta có M tiến trình. Chúng ta có thể chia N phần tử thành M phần (thường ta giả thiết N chia hết cho M, nghĩa là N/M là số nguyên) và gán từng phần đó cho mỗi tiến trình. Chu trình trên có thể viết thành:
for(j = id * N/M; j < (id+1)*N/M; j++){
C[j] = A[j] + B[j];
}
Trong đó, id là số hiệu của tiến trình, chạy từ 0 đến M-1. Tiến trình thứ i xử lý N/M phần tử liên tiếp kể từ i*N/M+1, ví dụ hình 2-4 (a).
Hoặc ta có thể cho phép các tiến trình truy cập xen kẽ vào các phần tử của mảng như sau:
Tiến trình Pi bắt đầu từ phần tử thứ i, sau đó bỏ qua M phần tử để xử lý phần từ tiếp theo, nghĩa là nó truy cập đến i, i+M, i+2M, v.v., ví dụ hình 2-4 (b).
Chu trình (1) khi đó được viết như sau:
for(j = id; j < N; j+=M){
C[j] = A[j] + B[j];
}
Ví dụ: Khi N = 15 và M = 5 thì việc gán các phần tử của vector cho các tiến trình sẽ được thực hiện theo cách trên như sau:
P1 P2 P3 P4 P5
1 4 7 10 13
2 5 8 11 14
3 6 9 12 15
P1 P2 P3 P4 P5
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
(a) (b)
Hình 2-4 Các cách phân chia chu trình của một mảng tuần tự
II.2.1.2 Lập trình chia sẻ bộ nhớ dựa vào luồng
Nhiều hệ điều hành hiện nay đều hỗ trợ đa luồng, ví dụ Window, OS/2, và UNIX. Trong hệ thống SMP, những luồng khác nhau có thể được hệ điều hành lập lịch tự động cho những CPU khác nhau. Một số ngôn ngữ lập trình, ví dụ Java cũng hỗ trợ lập trình đa luồng.
Một tiến trình là bức tranh về sự hoạt động của một chương trình. Mỗi tiến trình được kết hợp với một hoặc nhiều luồng. Các luồng có thể xem như các tập con của một tiến trình [14].
Các luồng của một tiến trình có thể chia sẻ với nhau về không gian địa chỉ chương trình, các đoạn dữ liệu và môi trường xử lý, đồng thời cũng có vùng dữ liệu riêng để thao tác.
Để dễ hiểu hơn về mô hình này, chúng ta có thể hình dung tiến trình như một xí nghiệp và các luồng như là các công nhân làm việc trong xí nghiệp đó. Các công nhân của xí nghiệp cùng chia sẻ nhau diện tích mặt bằng và các tài nguyên của cả xí nghiệp. Song, mỗi công nhân lại có chỗ làm việc được xem như là chỗ riêng của họ và những người khác không truy cập được.
Việc tạo ra một công nhân (tuyển dụng lao động) dễ hơn nhiều việc tạo lập ra một xí nghiệp, vì một muốn có một xí nghiệp thì phải có ít nhất một công nhân nào đó theo qui định.
Tương tự chúng ta có thể quan sát mối quan hệ giữa tiến trình và luồng về phương diện thông tin. Các công nhân trong xí nghiệp, theo mặc định, được quyền biết về mọi sự thay đổi, mọi việc xảy ra trong xí nghiệp. Nhưng nói chung, những biến động của xí nghiệp thì bình thường những xí nghiệp khác không biết được, trừ khi nó được thông báo trực tiếp cho những xí nghiệp đó.
Các tiến trình và các luồng trong hệ thống song song cần phải được đồng bộ, song việc đồng bộ các luồng được thực hiện hiệu quả hơn đổi với các tiến trình. Đồng bộ các tiến trình đòi hỏi tốn thời gian hoạt động của hệ thống, trong khi đối với các luồng thì việc đồng bộ chủ yếu tập trung vào sự truy cập các biến chung của chương trình.
Nhiều hệ điều hành hiện nay hỗ trợ đa luồng như: SUN Solaris, Window NT, OS/2, v.v. Bên trong những hệ điều hành này, những đặc tính hỗ trợ cho NSD khai thác được các luồng trong chương trình của mình thường khác nhau. Rất may hiện nay đã có một chuẩn, đó là Pthread của IEEE Portable Operating System Interface, POSIX.
Thực hiện các luồng của Pthread
Trong Pthread, chương trình chính cũng chính là một luồng. Một luồng có thể được tạo lập và kết thúc bằng những chương trình con sau:
Pthread_t aThread; // Khai báo một luồng
pthread_create(&aThread,&status,(void*)proc1,(void*)arg);
pthread_join(athread, void *status);
Hoạt động của các chương trình con trên được mô tả như trong hình 2-5. Một luồng mới được tạo ra và hoạt động ở proc1 và được truyền danh sách các đối số là &arg. Luồng sẽ bị huỷ bỏ sau khi kết thúc hoạt động và giải phóng các tài nguyên. Trang thái kết thúc công việc được trả lại trong pthrread_join().
Chương trình chính
.
. athread
. proc1(&arg)
pthread_create(&aThread,&status,(void*)proc1,(void*)arg); {
. .
. .
pthread_join(athread, void *status); .
. return(*status);
.
. }
Hình 2-5 Hoạt động của luồng trong Pthread
Có thể tạo lập và kết thúc một dãy các luồng.
for(int i=0; i <n; i++)
pthread_create(&aThread[i],&status,(void*)proc1,(void*)arg);
. . .
for(int i=0; i <n; i++)
pthread_join(aThread[i],NULL);
Vấn đề thực hiện loại trừ nhau giữa các luồng
Vấn đề thực hiện loại trừ nhau giữa các luồng cũng tương tự như đối với các tiến trình. Chúng ta có bốn hàm nguyên thuỷ:
pthread_mutex_init(mutex, NULL)
pthread_mutex_lock(mutex)
pthread_mutex_unlockinit(mutex)
pthread_mutex_destroy(mutex)
II.2.1.3 Xử lý luồng trong Java
Java là ngôn ngữ lập trình hướng đối tượng hỗ trợ đa luồng, tiện lợi cho các ứng dụng web trên mạng. Trong mô hình hướng đối tượng, tiến trình và thủ tục là thứ yếu, mọi chức năng của chương trình được xác định thông qua các đối tượng. Khái niệm luồng trong Java giống như trong các hệ điều hành được tích hợp vào ngôn ngữ.
Cũng giống như trên, các luồng được tạo lập, sau đó thực hiện một số công việc và kết thúc hoạt động khi không còn vai trò sử dụng.
Tạo lập các luồng
Trong Java có một lớp được xây dựng sẵn là Thread, làm lớp cơ sở để xây dựng những lớp kết thừa mới.
class MyClass extends Thread{
. . .
}
Các tiến trình trong hệ thống bắt đầu thực hiện ở một địa chỉ đặc biệt được xác định bởi phương mức có tên là main() của một lớp chính. Tương tự khi một luồng của lớp MyClass được tạo ra thì nó gọi phương thức run() để thực hiện. Phương thức này được viết đè để thực thi những công việc yêu cầu trong mỗi luồng được tạo ra.
class MyClass extends Thread{
// Một số thuộc tính
public void run(){
// Các lệnh cần thực hiện theo luồng
}
// Một số phương thức khác được viết đè hay bổ sung
}
Khi chương trình chạy nó sẽ gọi một phương thức đặc biệt đã được khai báo trong Thread đó là start() để bắt đầu một luồng đã được tạo ra.
Java không hỗ trợ đa kế thừa. Do vậy, nếu người lập trình muốn tạo ra một lớp kế thừa từ một lớp cơ sở và để thực hiện được theo luồng thì nó cũng đồng thời phải kế thừa từ lớp Thread. Điều này không thực hiện được.
Java giải quyết hạn chế trên bằng cách xây dựng khái niệm Interface. Giao diện hỗ trợ thực hiện theo luồng là Runnable. Người lập trình thiết kế các lớp thực hiện theo luồng bằng cách cài đặt theo giao diện Runnable.
class MyClass implemnets Runnable{
. . . }
Các trạng thái của Thread
Một luồng có thể ở một trong các trạng thái sau:
new: khi một luồng mới được tạo ra với toán tử new().
runnable: khi chúng ta gọi phương thức start() để bắt đầu của một luồng.
blocked: từ trạng thái runnable chuyển sang trạng thái “bị chặn” khi gọi một trong các phương thức: sleep(), suspend(), wait(), hay bị chặn lại ở Input/output.
dead: luồng chuyển sang trạng thái “chết” khi nó kết thúc hoạt động bình thường, hoặc gặp phải ngoại lệ không thực hiện tiếp được.
Hình 2-6 mô tả sơ đồ chuyển trạng của các luồng trong hệ thống.
dead
new
blocked
start
stop
sleep
resume
Đánh thức
notify
wait
chặn lại bởi I/O
Kết thúc I/O
suspend
runnable
Hình 2-6 Sơ đồ trạng thái của Thread
II.2.2 Tính toán phân tán: mô hình truyền thông điệp
Với sự phát triển của các máy tính PC, ngày càng nhiều dữ liệu được lưu trữ và được xử lý ở những máy tính cá nhân. Những dữ liệu này cần phải được chia sẻ và các tính toán ở nhiều nơi này cũng cần phải được kết hợp lại để giải quyết hiệu quả hơn những bài toán đặt ra trong thực tế. Từ đó xuất hiện mô hình tính toán phân tán [14].
Tính toán phân tán là những tính toán được thực hiện trên cơ sở kết hợp tính toán và truyền thông của hai hay nhiều máy tính trên mạng.
Mô hình tính toán phân tán có những ưu điểm:
- Cho phép chia sẻ dữ liệu được lưu ở nhiều máy tính khác nhau.
- Chia sẻ với nhau về một số chức năng chính của máy tính.
- Độ tin cậy và khả năng dung thứ lỗi cao hơn. Trong trường hợp có một máy tính bị trục trặc thì những máy tính khác có thể thay thế để hoàn thành nhiệm vụ của hệ thống.
- Tính kinh tế: thường đầu tư vào hệ phân tán sẽ thấp hơn đầu tư cho hệ tập trung.
Tuy nhiên, hệ tính toán phân tán cũng đứng trước nhiều thách thức, những vấn đề liên quan đến việc quản trị hệ thống, định vị tài nguyên, vấn đề đảm bảo an toàn, an ninh thông tin, v.v.
Xử lý trong các hệ thống phân tán không có bộ nhớ chia sẻ để trao đổi dữ liệu với nhau. Sự trao đổi được thực hiện bằng cách truyền thông điệp. Mô hình ở mức cao hơn, mô hình Client-Server cũng có thể sử dụng cơ chế này để cài đặt.
II.2.2.1 Mô hình truyền thông điệp
Giống như mô hình chia sẻ bộ nhớ, các đơn vị xử lý song song trong mô hình truyền thông điệp là các tiến trình. Tuy nhiên cũng có một số điểm khác nhau giữa hai mô hình này. Trong mô hình truyền thông điệp:
Các tiến trình có thể thực hiện trên những bộ xử lý khác nhau và không truy cập được vào không gian địa chỉ chia sẻ. Vì lý do này, những chương trình trao đổi thông điệp với nhau còn được gọi là các chương trình phân tán.
Chỉ có kênh truyền là có thể chia sẻ cho các tiến trình, thường đó là LAN hoặc mạng diện rộng.
Việc truyền thông và đồng bộ hoá hoạt động của các tiến trình được thực hiện thông qua hai phương thức send() và receive().
Tất cả các biến là cục bộ của các tiến trình. Vì thế, những vấn đề về xung đột dữ liệu (cần phải khoá dữ liệu khi một tiến trình truy cập), hay tranh chấp thông tin (bài toán loại trừ nhau) không xuất hiện trong mô hình tính toán phân tán.
Việc đồng bộ hoá các tiến trình của một chương trình song song được thực hiện theo có chế truyền thông điệp. Khi một tiến trình muốn gửi một thông điệp thì nó phải chờ cho đến khi tiến trình nhận sẵn sàng nhận thông điệp đó và ngược lại, cũng tương tự. Ở đây không có các buffer để tạm lưu giữ các thông điệp cần trao đổi giữa các tiến trình.
II.2.2.2 Kênh truyền thông
Những hệ thống khác nhau sẽ cung cấp những kênh truyền ở những mức trừu tượng khác nhau. Hầu hết các hệ thống đều đảm bảo rằng các kênh truyền thông không có các lỗi thông thường. Nghĩa là, tất cả các thông điệp được gửi đi và sẽ đến được đích mà không bị sai lạc.
Kênh truyền thông có thể được xem như là hàng đợi, và thông điệp sẽ được gửi đến đích theo thứ tự nó được gửi đi. Kênh truyền thông cũng có thể được xem như là hộp thư (mailbox) của một tiến trình nhận/gửi thông điệp. Ở mức trừu tượng này sẽ loại bỏ được ràng buộc về thứ tự đến của các thông điệp. Tiến trình có thể chọn các thông điệp từ hộp thư theo một số tiêu chí nào đó.
Một số hệ thống đưa ra khái niệm thông điệp có cấu trúc. Các kênh được định nghĩa theo các kiểu của thông điệp và một số kênh đặc biệt chỉ truyền tải được một số kiểu nhất định. Một tiến trình muốn gia nhập vào một kênh có kiểu xác định thì phải tự gắn bó với kênh đó. Một kênh cũng có thể phục vụ cho một nhóm các tiến trình trong chương trình song song.
II.2.2.3 Truyền thông đồng bộ
Truyền thông điệp đồng bộ: Trong mô hình này, tiến trình gửi bị chặn lại cho đến khi tiến trình nhận sẵn sàng nhận. Ở đây, sự truyền thông và đồng bộ hoá luôn gắn chặt với nhau.
Hệ thống truyền thông điệp đồng bộ hoàn toàn giống như hệ điện thoại, kênh truyền bị chặn lại trong quá trình đàm thoại. Hệ truyền dị bộ lại giống nhiều hơn với hệ thống bưu chính, người nhận phải chờ cho đến khi có thư được gửi đến.
Chúng ta hãy phân tích thêm để hiểu rõ sự phát triển của hai mô hình trên. Một mặt, cơ chế truyền thông điệp đồng bộ làm cho nhiều vấn đề trong đồng bộ hoá và việc cấp phát bộ nhớ động trở lên đơn giản hơn. Mặt khác, việc gắn chặt các tiến trình với thời gian phân phát thông điệp cũng được xem như là điều kiện ràng buộc bổ sung đòi hỏi trong thiết kế và thực thi chương trình. Việc bắt tiến trình gửi phải chờ dẫn đến việc làm giảm tính đồng thời của hệ thống. Ngoài ra, để cài đặt hiệu quả các hệ thống truyền tin đồng bộ, đòi hỏi phải có những phần cứng đặc biệt để đảm bảo rằng sự truyền tin cực nhanh và sự trao đổi dữ liệu không ảnh hưởng tới sự tính toán. Mà các mạng truyền thông nhanh có nhiều nút trao đổi với nhau là rất đắt tiền. Vì những lý do trên, nên hệ truyền thông điệp dị bộ làm việc trên LAN sẽ ngày một phổ cập hơn.
Các chương trình truyền thông điệp hiển thị nhiều mẫu giao diện khác nhau. Sau đây là một số mẫu chung nhất:
Dòng dữ liệu một chiều từ các bộ lọc trên mạng
Các yêu cầu và trả lời qua lại giữa khách (Client) và chủ (Server)
Phát tin hay quảng bá tin (broadcast) cho các tiến trình trong đồ thị đầy đủ
Gửi các dấu hiệu (token) theo các cạnh của đồ thị, v.v.
II.2.2.4 Truyền thông dị bộ
Truyền thông điệp dị bộ: Trong mô hình này, một kênh truyền được giả thiết là có khả năng tiếp nhận không bị giới hạn. Khả năng không giới hạn được cài đặt trong thực tế bằng cách sử dụng bộ đệm (buffer) để tiếp nhận các thông điệp gửi đến cho mỗi tiến trình. Do vậy, tiến trình gửi sẽ không phải chờ để tiến trình nhận sẵn sàng nhận mà cứ gửi khi có yêu cầu. Như vậy, hai tiến trình gửi và nhận có thể hoạt động gần như độc lập với nhau và thông điệp có thể nhận được sau một khoảng thời gian nào đó (lâu bất kỳ) kể từ khi nó được gửi đi. Tuy nhiên, tiến trình nhận thì phải chờ cho đến khi có được thông điệp của một tiến trình khác gửi cho nó. Từ đó dẫn đến một số điều kiện:
- Khi tiến trình A gửi đi một thông điệp cho ti
Các file đính kèm theo tài liệu này:
- Download- Luận văn cao học- giải thuật song song để phân tích và cài đặt một số lớp giải bài toán phi tuyến.doc