Parallel Programing
Tìm hiểu những tính năng lập trình song song có trong delphi
Tìm hiểu những tính năng lập trình song song có trong delphi
Đa luồng (multi-threading) và parallel programing (lập trình song song) là hai thành phần không thể thiếu của một ứng dụng hiệu suất cao. Thông thường, ứng dụng sẽ chạy trên một thread chính và trên một lõi CPU. Với sự phát triển của phần cứng cho phép sử dụng đồng thời nhiều lõi CPU, việc cần phải phân bổ hợp lí công việc thành nhiều thread nhỏ giúp nâng cao hiệu năng làm việc của chương trình.
Đối với các app di động, chia công việc thành các task và xử lý riêng trong thread là rất quan trọng. Nếu không, công việc được thực hiện trên luồng chính (main thread), giao diện sẽ bị đóng băng và lỗi This app has stopped working sẽ được ném ra, ứng dụng bị dừng.
Delphi cung cấp một cách dễ dàng để viết mã song song. Những phiên bản đầu tiên, Delphi sử dụng một lớp TThread được override phương thức Execute để thực hiện. Tuy nhiên nhược điểm của nó là khó viết mã và dài dòng.
Bây giờ, việc viết mã song song trong delphi đã thuận tiện hơn những vẫn còn khá rắc rối. Bài này trình bày 3 vấn đề: ITask, TParallel.For và IFuture trong unit System.Threading.
Task là các nhiệm vụ cần thực hiện. Nói sơ qua về lập trình giao diện (GUI), bình thường, delphi sử dụng một thread (main thread) để vừa cập nhật giao diện, vừa xử lý dữ liệu.
Cách làm này không đúng. Đối với các dữ liệu lớn, thì thời gian xử lý sẽ lâu. Lúc đó chương trình sẽ tập trung vào dữ liệu mà không xử lý giao diện, dẫn đến ứng dụng bị treo (Not Responding trên Windows hoặc This app has stopped working trên android).
Để khắc phục điều này, chúng ta nên đẩy phần xử lý dữ liệu ra ngoài một thread riêng, thực hiện nó bằng task. Sau khi hoàn thành thì synchronize với giao diện.
Delphi có cung cấp interface ITask và class TTask, giúp cho việc này dễ dàng hơn. Hãy xét qua ví dụ sau, chúng ta có 2 nút Button1 và Button2, cùng thực hiện 1 việc (để việc lâu thì dùng sleep để tạm dừng).
procedure DoSomeThing;
begin
Sleep(4000);
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
DoSomeThing;
ShowMessage('Done');
end;
procedure TForm1.Button2Click(Sender: TObject);
var
aTask: ITask;
begin
aTask := TTask.Create(
procedure
begin
DoSomeThing;
TThread.Synchronize(nil,
procedure
begin
ShowMessage('Done');
end);
end);
aTask.Start;
end;
Rồi, hãy thử chạy chương trình và click vào từng button. Bạn sẽ thấy với mỗi button đều phải đợi 4 giây thì thông báo mới xuất hiện. Tuy nhiên, khác biệt là Button1 chương trình có thể bị treo, nhưng Button2 thì chương trình vẫn hoạt động bình thường.
Lý do là vì Button2 sử dụng một thread riêng để xử lý. Khi DoSomeThing thực hiện xong thì thread này sẽ đồng bộ (synchronize) bằng thông báo Done, thông báo thực hiện thread hoàn thành.
Đoạn code trên có sử dụng phương thức ẩn danh.
Vòng lặp For bình thường chỉ chạy trên luồng chính (main thread), nên tốc độ sẽ chậm và nếu thực hiện lâu sẽ gây đóng băng ứng dụng (not responding như trên).
Với hầu hết các máy tính hiện nay đều trang bị CPU với nhiều lõi. Do đó, chúng ta có thể sử dụng đồng thời nhiều thread, mỗi thread chạy trên 1 lõi CPU, từ đó tốc độ sẽ được cải thiện rất nhiều. Nếu bạn có 4 lõi CPU, tốc độ thực hiện sẽ tăng 4 lần.
Delphi hỗ trợ method TParallel.&For (có dấu & phía trước để tránh trùng với từ khóa For) dùng để lặp đa luồng. Hàm này sẽ tự động phân bổ nhiệm vụ cho các thread, thực hiện tuần tự, số thread bằng đúng số lõi CPU. Do đó, tốc độ được đẩy nhanh và sử dụng nhiều tài nguyên hơn.
begin
TParallel.&For(0, 1, 100,
procedure (i: Int64)
begin
...
end);
end;
TParallel.&For nhận vào 4 tham số:
Sender: Thường đặt là 0
Giá trị bắt đầu
Giá trị kết thúc
Một phương thức ẩn danh có delegate là reference to procedure (i: int64), trong đó có 1 tham số là kiểu int64 đại diện cho biến đếm i.
Chú ý: Không được dùng Sleep trong thread vì sẽ gây đứng ứng dụng. Thêm nữa, chỉ dùng để xử lý các lệnh ngắn, nếu lệnh thực hiện quá lâu thì sẽ không có tác dụng và có thể xảy ra lỗi.
Bạn cũng cần tránh thread thao tác trên các control. Điều này sẽ gây xung đột do có nhiều thread cùng tác động đến một control. Do vậy, TParallel.For chỉ dùng cho xử lý dữ liệu.
Mẹo: Bạn có thể tự kiểm tra tốc độ giữa 2 câu lệnh for thường và for parallel.
System.Threading cung cấp một interface IFuture. Chức năng của IFuture là trả về một giá trị (string, integer, ...) sau một số câu lệnh nhất định. Khi nó được khởi tạo, một thread được tạo ra và thực hiện tính toán.
IFuture có thể được sử dụng bởi các câu lệnh khác. Nếu future đang thực hiện mà được sử dụng thì phải đợi cho future chạy xong rồi mới đi tiếp. Nếu future đã tính toán xong giá trị thì giá trị sẽ được gửi đến cho lệnh yêu cầu ngay lập tức.
var
FS: IFuture<string>;
...
procedure TForm1.Button1Click(Sender: TObject);
begin
FS := TTask.Future<string>(function: string
begin
Sleep(3000);
Result := 'Hello';
end);
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
if FS = nil then
ShowMessage('Future chua khoi tao')
else
ShowMessage(FS.Value);
end;
Biến IFuture nên được đặt ở ngoài để có thể sử dụng chung cho nhiều lệnh.
Khi chúng ta click vào Button1, future FS được khởi chạy. Khi đó, chúng ta có thể có 2 cách xử lý:
Sau khi nhấn Button1, nhấn ngay Button2: Chương trình sẽ đợi một lát rồi hiển thị Hello.
Sau khi nhấn Button1, đợi 1 thời gian mới nhấn Button2: Chương trình hiển thị Hello ngay lập tức.
Lý do là gì ? Trường hợp 1 vì Button2Click yêu cầu FS.Value quá sớm nên phải đợi FS thread chạy xong mới trả về giá trị Hello. Từ đó mới ShowMessage ra.
Trường hợp 2, vì trước khi nhấn Button2 bạn đã nghỉ một lúc nên FS thread có thời gian thực hiện xong lệnh. Khi Button2Click yêu cầu FS.Value thì FS đã tính xong rồi, nên ShowMessage giá trị ra ngay lập tức.
Chú ý: Đừng để đợi lâu quá, ứng dụng sẽ bị đóng băng :(