Bài viết nằm trong series Concurrent vs Parallel in Java
1) Sequential vs Parallel computing
Như chúng ta đã biết (hoặc có thể bạn chưa biết), một chương trình chạy trên máy tính là một tập các chỉ dẫn (instruction), giống như các bước cần thực hiện để có thể làm một đĩa salad tươi ngon . Ok bắt đầu thôi, công thức đã có rồi, bao gồm:
- Gọt rửa hoa quả, rau xanh.
- Cắt các loại kiwi, dưa chuột, cà rốt.. bất cứ gì bạn muốn thêm vào món salad của mình.
- Trộn tất cả lên sau đó thêm các loại nước sốt tùy ý.
- Trình bày ra đĩa và đã thành phẩm.
Mình sẽ đóng vai trò đầu bếp chính thực hiện tất cả các công đoạn trên để hoàn thành đĩa salad. Đó là ví dụ của single processor
(đơn nhân), có duy nhất mình thực hiện việc làm đĩa salad theo một công thức được cho trước từ đầu đến cuối (đã tách nhỏ thành các bước và thực hiện tuần tự).
Ngoài ra, mình chỉ có thể thực hiện 1 hành động (rửa rau, cắt kiwi...) tại một thời điểm. Có phải bạn đang nghĩ hoàn toàn làm được nhiều việc cùng lúc, nhưng thực tế trong lúc cắt kiwi bạn không thể rửa rau được (chúng ta sẽ nói cụ thể hơn về nó ở phần sau).
Bài toán làm salad ở trên là ví dụ của sequential programming
(lập trình tuần tự), cách mà chúng ta vẫn được học vì nó dễ tiếp cận. Nhưng nó cũng là hạn chế, thời gian để hoàn thành công việc sẽ phụ thuộc vào tốc độ làm việc của mình. Việc rửa rau, cắt kiwi càng nhanh thì món salad sẽ càng nhanh chóng được hoàn thành. Tốc độ làm các công việc đó cũng chính là giới hạn của mình, hay nói cách khác là tốc độ xử lý tối đa của processor. Sau rất nhiều nỗ lực, mình đã hoàn thành món salad trong ... 5 phút.
Dẫn đến kết luận đầu tiên: tốc độ xử lý của processor càng nhanh dẫn đến chương trình hoạt động càng nhanh.
Tất nhiên 1 đĩa salad là không đủ, mình tiếp tục vào bếp để làm đĩa thứ hai. Mình đã gọi thêm một bạn đầu bếp nữ xinh đẹp vào làm cùng. Chúng mình quyết định phân chia công việc để hoàn thành đĩa salad nhanh hơn. Mình sẽ cắt kiwi, cà chua, còn bạn nữ sẽ cắt rau củ, cà rốt, trộn sốt... Một lúc sau, bạn nữ đã cắt xong nguyên liệu... cô ấy quyết định trộn tất cả lên và thêm sốt vào trước. Tuy nhiên vì mình vẫn chưa thực hiện xong nên phải chờ chút đã... Ok, cả 2 đã thực hiện xong, bây giờ sẽ trộn lên và thêm sốt thôi.
Ví dụ trên chính là parallel execution
(xử lý song song), chúng ta sẽ chia công thức có sẵn thành các phần có thể thực hiện cùng lúc và mỗi người sẽ thực hiện công việc của mình. Bước cuối cùng (trộn sốt) sẽ thực hiện khi tất cả các công việc trước đó được hoàn thành. Rút ra kết luận thứ hai: có nhiều processor thì chương trình sẽ hoạt động nhanh hơn so với ít processor.
Note: Tất nhiên không phải cứ có nhiều processor thì sẽ nhanh hơn, mình sẽ giải thích kĩ hơn ở phần sau.
Đĩa salad thứ 2 đã hoàn thành trong vòng 3 phút. Hãy chú ý, khi có thêm một người nữa làm cùng thì không có nghĩa là thời gian được giảm đi một nửa. Thực tế, có thêm người tức là sẽ có thêm sự phức tạp, chúng ta sẽ cần nhiều effort hơn cho việc giao tiếp để hoàn thành công việc. Một lý do nữa, không phải việc nào cũng có thể chia ra làm cùng nhau, đôi khi sẽ phải chờ lẫn nhau để tiếp tục một công việc nào đó (trộn salad, thêm sốt).
Sẽ có rất nhiều vấn đề cần phải xử lý, đó chính là khó khăn khi lập trình multi thread so với single thread. Tuy nhiên, lợi ích mà nó sẽ rất lớn nếu biết áp dụng đúng cách, nhớ là đúng cách nhé. Và khi thực hiện đúng cách, nó sẽ giúp:
- Hoàn thành 1 công việc sớm hơn (so với single thread programming).
- Hoàn thành được nhiều công việc hơn trong cùng 1 khoảng thời gian (so với single thread programming).
2) Parallel computing architecture
Các CPU (bộ vi xử lý) ngày này bao gồm rất nhiều processors (đa nhân) bên trong, chúng ta thường nay nghe đến các bộ xử lý Intel i5, i7 với 4 nhân (core), 8 luồng (thread) và hyper-threading (sẽ có giải thích thuật ngữ ở các bài tiếp theo nhé). Nhờ có nhiều processor (core) như vậy nên chúng thật sự có thể xử lý các bài toán song song với nhau.
Có một câu hỏi đặt ra, vậy thì với các máy tính đời cũ chỉ có 1 core duy nhất, làm thế nào để chúng có thể thực hiện được nhiều thứ đến vậy: lướt web, nghe nhạc, xem phim (nếu chưa tìm được câu trả lời thì cứ bình tĩnh nhé).
Đi sâu hơn một chút, chúng ta tiếp tục tìm hiểu về các kiến trúc đa luồng mà máy tính đang có. Dựa trên cách phân loại Flynn (Flynn's taxonomy), phân chia với 2 yếu tố:
- Số lượng của các luồng chỉ dẫn (instruction) (the number of concurrent struction or control streams).
- Số lượng của các luồng dữ liệu (data) (the number of data streams).
Từ đó, sẽ có 4 loại kiến trúc:
- SISD: Single instruction stream, single data stream.
- SIMD: Single instruction stream, multiple data stream.
- MISD: Multiple instruction stream, single data stream.
- MIMD: Multiple instruction stream, multiple data stream.
Bắt đầu có nhiều thuật ngữ hơn rồi đây. Vậy thì 4 loại kiến trúc trên thực sự là gì, ví dụ thực tế như thế nào và chúng khác nhau ra sao?
2.1) SISD
Đầu tiên với kiến trúc SISD, trong bất kì thời điểm nào, chúng chỉ có thể xử lý duy nhất một instruction và tương tác với duy nhất một nguồn data. Ví dụ cho dễ hình dung, mình là SISD computer, mình chỉ có thể thực hiện duy nhất một hành động cắt (single instruction) với quả kiwi (single data) tại một thời điểm. Đây là kiến trúc được áp dụng phổ biến cho các máy tính đời đầu.
2.2) SIMD
Tiếp tục đến SIMD, một loại parallel computing với nhiều processor (core), hay chúng ta quen gọi là nhân của hệ điều hành (multiple processing units). Tại một thời điểm bất kì, tất cả các processors đều xử lý chung một instruction nhưng sẽ tương tác với nhiều nguồn data khác nhau. Ví dụ, SIMD bao gồm hai processors là 2 đầu bếp, cả 2 đều sẽ thực hiện hành động cắt (single instruction), tuy nhiên mỗi processor sẽ xử lý với nguồn data khác nhau. Mình cắt (instruction) kiwi (data) còn bạn nữ sẽ cắt (instruction) cà rốt (data). Câu hỏi nhỏ, SIMD được áp dụng ở đâu, cho cái gì (câu trả lời sẽ có ở cuối)?
2.3) MISD
Mỗi một processor sẽ xử lý riêng một instruction, tuy nhiên tất cả các processors sẽ thực hiện thao tác trên cùng một data.
Ví dụ, quay về bài toán đĩa salad, cụ thể ta sẽ có 2 instructions (cho 2 processors) là gọt vỏ và cắt, thao tác trên đối tượng là quả kiwi (data). Nghĩa là 1 người gọt vỏ kiwi, 1 người sẽ cắt kiwi, hai người làm đồng thời cùng một lúc. Nghe có vẻ không.. make sence lắm nhỉ. Thực tế đúng là như vậy, MISD không phải là kiến trúc phổ biến.
2.4) MIMD
Cuối cùng là MIMD, sắp đến đích rồi. Mỗi một processor sẽ thực hiện những instruction riêng biệt trên data riêng biệt. Ví dụ rất đơn giản, 1 người gọt vỏ kiwi trong người còn lại cắt cà rốt. Kiến trúc MIMD rất phổ biển và xuất hiện trong gần như toàn bộ các máy tính hiện đại ngày này từ laptop, máy tính để bàn cho đến smart phone...
Đi sâu vào MIMD, phần này liên quan đến cấu trúc máy tính nhiều hơn nên các bạn có thể bỏ qua nếu cảm thấy chưa cần thiết, tuy nhiên tích lũy thêm một chút kiến thức cũng ok. Chúng ta sẽ có hai cách xử lý bài toán trong kiến trúc MIMD đó là:
- SPMD: Single program, multiple data.
- MPMD: Multiple program, multiple data.
2.4.1) SPMD
Với SPMD, các processors sẽ xử lý 1 bài toán song song với nhau (2 người cùng làm 1 đĩa salad), tuy nhiên mỗi processor sẽ thao tác với dữ liệu khác nhau. Nghe có vẻ giống SIMD nhưng thực tế không phải vậy, mặc dù cả 2 processors cùng xử lý 1 bài toán nhưng không cùng thực hiện 1 instruction. Ví dụ, 2 người cùng nhìn vào công thức làm món salad, sau đó sẽ chia thành các nhiệm vụ nhỏ và mỗi người làm một phần khác nhau. Mô hình này rất phổ biến và nó được áp dụng trong chính chiếc máy vi tính mà chúng ta đang sử dụng.
2.4.2) MPMD
Cuối cùng là MPMD, thay vì làm 1 đĩa salad thì bọn mình làm 2 đĩa salad, mỗi một người sẽ làm một đĩa khác nhau dựa trên 2 công thức (series of instruction) và không liên quan đến công việc của người còn lại. MPMD sẽ phù hợp với các bài toán có thể chia nhỏ ra theo hướng chức năng.
3) Shared và Distributed memory
Phần cuối đây rồi. Nhưng thôi để dành viết cho bài sau .
Note
Với ví dụ trên về SIMD, nó sẽ phù hợp với các bài toán xử lý với lượng dữ liệu lớn nhưng lại cùng một instruction, ví dụ như xử lý ảnh. Đa số các máy tính hiện này sử dụng GPU (card đồ họa) với kiến trúc SIMD.