Với sự ra đời của các CPU đa lõi trong những năm gần đây, lập trình song song là cách để tận dụng tối đa sức mạnh công việc xử lý mới. Lập trình song song đề cập đến việc thực thi đồng thời các quy trình do tính sẵn có của nhiều lõi xử lý. Về bản chất, điều này dẫn đến sự gia tăng đáng kể về hiệu suất và hiệu quả của các chương trình trái ngược với việc thực thi lõi đơn tuyến tính hoặc thậm chí đa luồng. Khung Fork / Join là một phần của API đồng thời Java. Khung này cho phép các lập trình viên song song hóa các thuật toán. Bài viết này khám phá khái niệm về lập trình song song với sự trợ giúp của Khuôn khổ Fork / Tham gia có sẵn trong Java.
Tổng quan
Lập trình song song có nội hàm rộng hơn nhiều và chắc chắn là một lĩnh vực rộng lớn cần giải thích trong một vài dòng. Điểm mấu chốt của vấn đề là khá đơn giản, nhưng khó đạt được về mặt hoạt động hơn nhiều. Nói một cách dễ hiểu, lập trình song song có nghĩa là viết các chương trình sử dụng nhiều hơn một bộ xử lý để hoàn thành một tác vụ, vậy thôi! Đoán xem; Nó nghe có vẻ quen thuộc, phải không? Nó gần như vần với ý tưởng về đa luồng. Tuy nhiên, lưu ý rằng có một số điểm khác biệt quan trọng giữa chúng. Nhìn bề ngoài, chúng giống nhau, nhưng dòng điện dưới mức hoàn toàn khác nhau. Trên thực tế, đa luồng đã được giới thiệu để cung cấp một loại ảo tưởng về xử lý song song mà không thực hiện song song thực sự nào cả. Đa luồng thực sự làm được gì là nó đánh cắp thời gian nhàn rỗi của CPU và sử dụng nó thành lợi thế của nó.
Nói tóm lại, đa luồng là một tập hợp các đơn vị logic rời rạc của các tác vụ chạy để lấy phần thời gian CPU của chúng trong khi một luồng khác có thể tạm thời chờ đợi, chẳng hạn như một số đầu vào của người dùng. Thời gian CPU nhàn rỗi được chia sẻ một cách tối ưu giữa các luồng cạnh tranh. Nếu chỉ có một CPU, đó là thời gian được chia sẻ. Nếu có nhiều lõi CPU, chúng cũng được chia sẻ tất cả thời gian. Do đó, một chương trình đa luồng tối ưu sẽ vắt kiệt hiệu suất của CPU bằng cơ chế chia sẻ thời gian thông minh. Về bản chất, nó luôn là một luồng sử dụng một CPU trong khi một luồng khác đang đợi. Điều này xảy ra theo cách tinh tế mà người dùng có được cảm giác xử lý song song, trong đó, trên thực tế, quá trình xử lý thực sự diễn ra liên tiếp nhanh chóng. Ưu điểm lớn nhất của đa luồng là nó là một kỹ thuật để khai thác tối đa các tài nguyên xử lý. Bây giờ, ý tưởng này khá hữu ích và có thể được sử dụng trong bất kỳ tập hợp môi trường nào, cho dù nó có một CPU hay nhiều CPU. Ý tưởng cũng giống nhau.
Mặt khác, lập trình song song có nghĩa là có nhiều CPU chuyên dụng được lập trình viên khai thác song song. Kiểu lập trình này được tối ưu hóa cho môi trường CPU nhiều lõi. Hầu hết các máy ngày nay đều sử dụng CPU nhiều lõi. Vì vậy, lập trình song song là khá phù hợp ngày nay. Ngay cả máy rẻ tiền nhất cũng được gắn với CPU nhiều lõi. Nhìn vào các thiết bị cầm tay; thậm chí chúng còn đa nhân. Mặc dù mọi thứ đều có vẻ khó hiểu với CPU đa lõi, đây cũng là một khía cạnh khác của câu chuyện. Nhiều lõi CPU hơn có nghĩa là máy tính nhanh hơn hoặc hiệu quả hơn? Không phải luôn luôn! Triết lý tham lam của "càng nhiều càng tốt" không áp dụng cho máy tính, cũng như trong cuộc sống. Nhưng chúng ở đó, không thể đoán trước - kép, tứ, tám, v.v. Họ ở đó chủ yếu là vì chúng ta muốn chúng chứ không phải vì chúng ta cần chúng, ít nhất là trong hầu hết các trường hợp. Trên thực tế, việc giữ cho dù chỉ một CPU bận rộn trong công việc tính toán hàng ngày là điều tương đối khó. Tuy nhiên, đa nhân có công dụng của chúng trong những trường hợp đặc biệt, chẳng hạn như trong máy chủ, trò chơi, v.v. hoặc giải quyết các vấn đề lớn. Vấn đề của việc có nhiều CPU là nó yêu cầu bộ nhớ phải phù hợp với tốc độ với sức mạnh xử lý, cùng với các kênh dữ liệu nhanh như chớp và các phụ kiện khác. Nói tóm lại, nhiều lõi CPU trong tính toán hàng ngày cung cấp cải thiện hiệu suất không thể lớn hơn số lượng tài nguyên cần thiết để sử dụng nó. Do đó, chúng tôi nhận được một chiếc máy đắt tiền chưa được sử dụng hết, có lẽ chỉ để được trưng bày.
Lập trình song song
Không giống như đa luồng, trong đó mỗi tác vụ là một đơn vị logic rời rạc của một tác vụ lớn hơn, các tác vụ lập trình song song là độc lập và thứ tự thực hiện của chúng không quan trọng. Các nhiệm vụ được xác định theo chức năng mà chúng thực hiện hoặc dữ liệu được sử dụng trong quá trình xử lý; điều này được gọi là tính song song chức năng hoặc song song dữ liệu , tương ứng. Trong song song chức năng, mỗi bộ xử lý hoạt động trên phần của vấn đề trong khi trong song song dữ liệu, bộ xử lý hoạt động trên phần dữ liệu của nó. Lập trình song song phù hợp với cơ sở vấn đề lớn hơn không phù hợp với kiến trúc CPU đơn lẻ hoặc có thể vấn đề quá lớn nên không thể giải quyết được trong một khoảng thời gian hợp lý. Do đó, các tác vụ, khi được phân phối giữa các bộ xử lý, có thể thu được kết quả tương đối nhanh.
Khuôn khổ Fork / Tham gia
Khung Fork / Join được định nghĩa trong java.util.concurrent bưu kiện. Nó bao gồm một số lớp và giao diện hỗ trợ lập trình song song. Những gì nó làm chủ yếu là nó đơn giản hóa quá trình tạo nhiều luồng, sử dụng chúng và tự động hóa cơ chế phân bổ quy trình giữa nhiều bộ xử lý. Sự khác biệt đáng chú ý giữa đa luồng và lập trình song song với khuôn khổ này rất giống với những gì chúng tôi đã đề cập trước đó. Ở đây, phần xử lý được tối ưu hóa để sử dụng nhiều bộ xử lý không giống như đa luồng, trong đó thời gian nhàn rỗi của một CPU được tối ưu hóa trên cơ sở thời gian dùng chung. Ưu điểm bổ sung với khuôn khổ này là sử dụng đa luồng trong môi trường thực thi song song. Không có hại ở đó.
Có bốn lớp cốt lõi trong khuôn khổ này:
- ForkJoinTask
: Đây là một lớp trừu tượng xác định một nhiệm vụ. Thông thường, một tác vụ được tạo với sự trợ giúp của fork () phương thức được định nghĩa trong lớp này. Tác vụ này gần giống với một chuỗi bình thường được tạo bằng Luồng đẳng cấp, nhưng nhẹ hơn nó. Cơ chế mà nó áp dụng là nó cho phép quản lý một số lượng lớn các tác vụ với sự trợ giúp của một số lượng nhỏ các chuỗi thực tham gia ForkJoinPool . Các ngã ba () phương thức cho phép thực thi không đồng bộ tác vụ đang gọi. The tham gia () phương thức cho phép đợi cho đến khi tác vụ mà nó được gọi cuối cùng được kết thúc. Có một phương thức khác, được gọi là invoke () , kết hợp ngã ba và tham gia hoạt động thành một cuộc gọi. - ForkJoinPool: Lớp này cung cấp một nhóm chung để quản lý việc thực thi ForkJoinTask các nhiệm vụ. Về cơ bản, nó cung cấp điểm nhập cảnh cho các bài gửi từ không phải ForkJoinTask khách hàng cũng như các hoạt động quản lý và giám sát.
- Hành động đệ quy: Đây cũng là một phần mở rộng trừu tượng của ForkJoinTask lớp. Thông thường, chúng tôi mở rộng lớp này để tạo tác vụ không trả về kết quả hoặc có void loại trả lại. compute () phương thức được định nghĩa trong lớp này bị ghi đè để bao gồm mã tính toán của tác vụ.
- Nhiệm vụ đệ quy
: Đây là một phần mở rộng trừu tượng khác của ForkJoinTask lớp. Chúng tôi mở rộng lớp này để tạo một tác vụ trả về một kết quả. Và, tương tự như ResursiveAction, nó cũng bao gồm một tính toán trừu tượng được bảo vệ () phương pháp. Phương thức này được ghi đè để bao gồm phần tính toán của tác vụ.
Chiến lược khung Fork / Tham gia
Khung này sử dụng một phân chia và chinh phục đệ quy chiến lược thực hiện xử lý song song. Về cơ bản, nó chia một nhiệm vụ thành các nhiệm vụ con nhỏ hơn; sau đó, mỗi nhiệm vụ con lại được chia thành nhiều nhiệm vụ con. Quá trình này được áp dụng đệ quy trên từng tác vụ cho đến khi nó đủ nhỏ để xử lý tuần tự. Giả sử chúng ta tăng các giá trị của một mảng N những con số. Đây là nhiệm vụ. Bây giờ, chúng ta có thể chia mảng cho hai tạo hai nhiệm vụ con. Chia mỗi người trong số họ một lần nữa thành hai nhiệm vụ phụ, và cứ tiếp tục như vậy. Bằng cách này, chúng ta có thể áp dụng chia để trị chiến lược đệ quy cho đến khi các nhiệm vụ được đơn lẻ thành một bài toán đơn vị. Vấn đề đơn vị này sau đó có thể được thực hiện song song bởi nhiều bộ xử lý lõi có sẵn. Trong một môi trường không song song, những gì chúng ta phải làm là duyệt qua toàn bộ mảng và thực hiện xử lý theo trình tự. Đây rõ ràng là một cách tiếp cận không hiệu quả theo quan điểm của quá trình xử lý song song. Nhưng, câu hỏi thực sự là mọi vấn đề có thể được phân chia và chinh phục ? Tất nhiên là không! Tuy nhiên, có những vấn đề thường liên quan đến một số loại mảng, tập hợp, nhóm dữ liệu đặc biệt phù hợp với cách tiếp cận này. Nhân tiện, có những vấn đề có thể chưa sử dụng bộ sưu tập dữ liệu có thể được tối ưu hóa để sử dụng chiến lược lập trình song song. Loại bài toán tính toán nào phù hợp để xử lý song song hoặc thảo luận về thuật toán song song nằm ngoài phạm vi của bài viết này. Hãy xem một ví dụ nhanh về ứng dụng của Khuôn khổ Fork / Tham gia.
Một ví dụ nhanh
Đây là một ví dụ rất đơn giản để cung cấp cho bạn ý tưởng về cách triển khai tính năng song song trong Java với Fork / Join Framework.
package org.mano.example; import java.util.concurrent.RecursiveAction; public class CustomRecursiveAction extends RecursiveAction { final int THRESHOLD = 2; double [] numbers; int indexStart, indexLast; CustomRecursiveAction(double [] n, int s, int l) { numbers = n; indexStart = s; indexLast = l; } @Override protected void compute() { if ((indexLast - indexStart) > THRESHOLD) for (int i = indexStart; i < indexLast; i++) numbers [i] = numbers [i] + Math.random(); else invokeAll (new CustomRecursiveAction(numbers, indexStart, (indexStart - indexLast) / 2), new CustomRecursiveAction(numbers, (indexStart - indexLast) / 2, indexLast)); } } package org.mano.example; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; public class Main { public static void main(String[] args) { final int SIZE = 10; ForkJoinPool pool = new ForkJoinPool(); double na[] = new double [SIZE]; System.out.println("initialized random values :"); for (int i = 0; i < na.length; i++) { na[i] = (double) i + Math.random(); System.out.format("%.4f ", na[i]); } System.out.println(); CustomRecursiveAction task = new CustomRecursiveAction(na, 0, na.length); pool.invoke(task); System.out.println("Changed values :"); for (inti = 0; i < 10; i++) System.out.format("%.4f ", na[i]); System.out.println(); } }
Kết luận
Đây là mô tả ngắn gọn về lập trình song song và cách nó được hỗ trợ trong Java. Việc có N lõi sẽ không tạo ra mọi thứ N nhanh hơn nhiều lần. Chỉ một phần ứng dụng Java sử dụng hiệu quả tính năng này. Mã lập trình song song là một khung khó. Hơn nữa, các chương trình song song hiệu quả phải xem xét các vấn đề như cân bằng tải, giao tiếp giữa các tác vụ song song, và những thứ tương tự. Có một số thuật toán phù hợp hơn với việc thực thi song song nhưng nhiều thuật toán thì không. Trong mọi trường hợp, Java API không thiếu sự hỗ trợ của nó. Chúng tôi luôn có thể sửa đổi các API để tìm ra những gì phù hợp nhất. Chúc bạn viết mã vui vẻ 🙂