Posts Tagged multithreading
Đa luồng trong WPF
Đa luồng không phải là một khái niệm mới mẻ hay đặc trưng của WPF mà nó là một tính năng đã được hỗ trợ trong mọi phiên bản của .NET Framework. Tuy vậy, việc áp dụng kĩ thuật đa luồng vào WPF vẫn có những điểm riêng đáng lưu ý.
WPF sử dụng mô hình STA (Single Threaded Apartment) tương tự Windows Form trước đây với các quy tắc cơ bản:
- Các phần tử WPF được sở hữu bởi một luồng duy nhất là luồng tạo ra phần tử đó. Các luồng khác không thể tương tác trực tiếp với phần tử này.
- DispatcherObject là lớp cha của các phần tử WPF, cung cấp các phương thức đảm bảo các đối tượng WPF được sử dụng bởi đúng luồng sở hữu.
Vì sao lại có những hạn chế như vậy? Ban đầu, các nhà thiết kế WPF đã dự định xây dựng một mô hình luồng mới, trong đó các đối tượng giao diện có thể được truy xuất từ bất cứ luồng nào. Tuy nhiên, hướng tiếp cận này gây nên những phức tạp không cần thiết cho các ứng dụng đơn luồng, hơn nữa còn gây khó khăn khi phải giao tiếp với các thư viện cũ. Kết quả là kế hoạch trên đã bị hủy bỏ, và WPF lại quay trở về với mô hình STA như ngày nay.
Điều này có gì quan trọng? Một sai lầm phổ biến của những người mới làm quen với WPF và lập trình đa luồng là cố điều khiển WPF control từ một luồng không sở hữu nó.
private void Button1_Click(object sender, RoutedEventArgs e)
{
Thread thread = new Thread(ChangeText);
thread.Start();
}
private void ChangeText()
{
Thread.Sleep(TimeSpan.FromSeconds(10));
textbox.Text = "Warning! Exception will happen here!";
}
Giả định rằng có một cửa sổ WPF với một button và một textbox. Hàm Button1_Click xử lý sự kiện click chuột trên button, tạo ra một luồng mới và trong luồng đó thay đổi thuộc tính Text của button. Đoạn code trên sẽ gây ra InvalidOperationException. Cách làm đúng trong trường hợp này là sử dụng phương thức BeginInvoke() của DispatcherObject. BeginInvoke có nhiệm vụ điều phối để thực thi đoạn code trong cùng luồng của dispatcher, phương thức này nhận hai tham số: tham số thứ nhất xác định mức độ ưu tiên của tác vụ cần thực hiện, và tham số thứ hai là một delegate trỏ tới hàm thực hiện tác vụ đó. Đoạn code gây lỗi có thể được viết lại như sau:
private void ChangeText()
{
Thread.Sleep(TimeSpan.FromSeconds(10));
// Lấy về dispatcher của cửa sổ hiện tại và thay đổi text cho textbox
this.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (ThreadStart) delegate()
{
textbox.Text = "Hooray! No error!";
});
}
Lưu ý là DispatcherObject còn có một phương thức Invoke() với cùng tác dụng như trên, khác biệt duy nhất ở chỗ Invoke được thực thi đồng bộ, điều này có nghĩa là luồng gọi Invoke sẽ bị treo đến khi dispatcher thực thi xong tác vụ đó, trong khi lời gọi BeginInvoke() sẽ trả điều khiển ngay lập tức.
Ngoài ra, DispatcherObject cũng cung cấp hai hàm CheckAccess() và VerifyAccess(). CheckAccess trả về true khi đoạn code điều khiển các phần tử WPF được chạy trên đúng luồng, và false nếu ngược lại. Trong khi VerifyAccess() là hàm không trả trị, hàm này sẽ phát sinh InvalidOperationException trong trường hợp luồng thực thi là không hợp lệ. Đây cũng chính là hàm được WPF sử dụng để đảm bảo chúng ta không thao tác với các phần tử WPF từ các luồng không được phép.
Đơn giản hóa quản lý luồng với BackgroundWorker
Trong các ví dụ trước, chúng ta đã tạo và sử dụng luồng tường minh. Cách làm này có ưu điểm là sự linh động tối đa (không hạn chế số luồng được tạo, quản lý độ ưu tiên, trạng thái luồng…). Tuy nhiên trong nhiều trường hợp, chúng ta chỉ cần một giải pháp đơn giản nhất, và đó là nơi mà BackgroundWorker phát huy tác dụng. Về cơ bản, BackgroundWorker hoạt động trên một luồng riêng trong khi ẩn đi các chi tiết phức tạp của lập trình đa luồng. BackgroundWorker cung cấp các sự kiện:
- DoWork: được kích hoạt khi BackgroundWorker bắt đầu thực thi, ta đặt đoạn code cần xử lý bất đồng bộ vào trong hàm xử lý sự kiện này.
- RunWorkerCompleted: khi hoàn tất, BackgroundWorker phát sinh sự kiện này trên luồng của dispatcher, đây là nơi nhận kết quả xử lý và cập nhật các phần tử giao diện.
Sau đây là ví dụ trên viết lại dùng BackgroundWorker. BeginInvoke() bây giờ không còn cần thiết nữa, vì BackgroundWorker đã đảm nhận tất cả:
private void Button1_Click(object sender, RoutedEventArgs e)
{
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += new DoWorkEventHandler(DoSomething);
worker.RunWorkerCompleted += new RunWorkerCompletedEventHandler(SomethingDone);
worker.RunWorkerAsync();
}
private void DoSomething(object sender, DoWorkEventArgs e)
{
Thread.Sleep(10000);
}
private void SomethingDone(object sender, RunWorkerCompletedEventArgs e)
{
textbox.Text = "Hooray! No error!";
}
Hàm Button1_Click tạo đối tượng BackgroundWorker, gắn các hàm xử lý sự kiện, và sau đó thực thi BackgroundWorker theo kiểu bất đồng bộ bằng phương thức RunWorkerAsync (lưu ý là bạn có thể cần thêm khai báo using cho namespace System.ComponentModel). Bên cạnh hai sự kiện phổ biến DoWork và RunWorkerCompleted, BackgroundWorker còn cung cấp hai sự kiện khác là Disposed và ProgressChanged, chúng ta có thể bắt các sự kiện này để xử lý thêm khi có nhu cầu. Một điều thú vị cuối cùng là trong WPF, BackgroundWorker có thể được khai báo trực tiếp trong XAML thay vì phải viết code:
<Window.Resources>
<cm:BackgroundWorker x:Key="backgroundWorker" DoWork="DoSomething" RunWorkerCompleted="SomethingDone">
</cm:BackgroundWorker>
</Window.Resources>
Trong khuôn khổ giới hạn, bài viết chỉ trình bày một phần nhỏ về kĩ thuật lập trình đa luồng với WPF. Lập trình là một công việc luôn luôn mới mẻ và thú vị, chắc chắn vẫn còn rất nhiều vấn đề đang chờ bạn khám phá. Chúc các bạn thành công!
Lập trình đa luồng với C# 4.0
Từ khi ra đời với phiên bản 1.0 đến nay, C# không ngừng được cải tiến với nhiều tính năng mạnh mẽ. C# 3.0 nổi bật với những cú pháp mở rộng để hỗ trợ LINQ. Trong khi đó, C# 4.0 chú trọng nhiều đến lập trình đa luồng và lập trình động (dynamic programming). Bài viết này trình bày tổng quan về những điểm mới trong lập trình đa luồng với C# 4.
1. Lớp Task:
Lớp Task đơn giản hóa đáng kể việc sử dụng CLR Thread pool, thậm chí còn có thể giúp gia tăng tốc độ. Đó là nhờ bản hiện thực của lớp Task đã tận dụng các kĩ thuật đồng bộ không cần khóa (lock-free) để giảm tối đa số khóa trong chương trình. Sử dụng lớp Task đơn giản như sau:
Task myTask = Task.Factory.StartNew(() => {
// do something here
});
Phương thức StartNew() nhận vào một delegate không có tham số và không trả trị (ví dụ trên dùng cú pháp của toán tử lambda). Delegate này sau đó sẽ được thực thi trong một luồng riêng. Ngoài ra lớp Task còn có một số phương thức như ContinueWith() để thực hiện nhiều tác vụ nối tiếp nhau. Trong trường hợp đó, đoạn code thường được đặt trong khối try…catch:
try
{
Task myTask = Task.Factory.StartNew(() => {
// do something here
myTask.ContinueWith(() => {
// …
}
});
}
catch (AggregateException ex)
{
// do something here
}
Trong trường hợp này, exception có thể phát sinh tại task thứ nhất hoặc task thứ hai (task trong câu lệnh ContinueWith). AggregateException là kiểu exception đặc biệt để bắt một tập hợp các exception có thể xảy ra từ bất cứ task nào.
2. Lớp Parallel:
Lớp Parallel cung cấp các phương thức static cho phép thực hiện một số tác vụ truyền thống theo kiểu song song. Hai phương thức phổ biến nhất là For() và ForEach(), có chức năng như một vòng lặp thông thường nhưng có thể phân hoạch collection thành nhiều phần để xử lý đồng thời. Ví dụ:
int[] numbers = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
Parallel.For( 0, numbers.Length, (i) =>
{
numbers[i] = number[i] * 2;
Console.WriteLine(numbers[i]);
});
Lưu ý rằng kết quả xuất ra ở mỗi lần chạy có thể khác nhau và không giống với thứ tự ban đầu trong mảng, điều này chứng tỏ các thao tác được thực thi song song. Một điểm chú ý quan trọng khác là tốc độ. Việc thực thi song song không đồng nghĩa với việc gia tăng tốc độ, đặc biệt trong các trường hợp đơn giản như trên. Tốc độ thực thi phụ thuộc vào nhiều yếu tố khác nhau, và đa luồng sẽ hầu như không mang lại cải thiện tốc độ nào trên các hệ thống một bộ xử lý (các hệ thống đa nhân có thể coi như nhiều bộ xử lý). Mặc dù khả năng xử lý song song là rất hấp dẫn, chúng cần tránh bị lạm dụng.
3. Parallel LINQ:
LINQ hay ngôn ngữ truy vấn tích hợp (Language INtegrated Query) là một bổ sung đáng giá của C# 3.0, cho khả năng truy vấn dữ liệu từ nhiều nguồn khác nhau (trên bộ nhớ, trong dataset, file XML hay database) bằng một cú pháp thống nhất. Trong C# 4, LINQ được tăng cường thêm khả năng hỗ trợ đa luồng với toán tử AsParallel(). Toán tử mới này cho phép chuyển câu truy vấn LINQ bình thường thành câu truy vấn song song:
int[] numbers = new int[1000];
/*
initialize here: fill numbers array with values
...
*/
var result = numbers.AsParallel().Where((i) => i % 2 == 0);
Khi sử dụng toán tử AsParralel() cần lưu ý các vấn đề tương tự như với các phương thức của lớp Parallel.
4. Thread-safe collections:
Vấn đề đồng bộ hóa là một trong những vấn đề phức tạp nhất của lập trình đa luồng. Nhằm giảm bớt gánh nặng cho lập trình viên, .NET Framework 4 cung cấp sẵn các tập hợp an toàn luồng. Đây là các tập hợp được thiết kế để hỗ trợ việc truy vấn đồng thời từ nhiều luồng khác nhau. Ngoài việc đơn giản hóa đáng kể các thao tác với dữ liệu, các tập hợp này còn được tối ưu về mặt tốc độ với việc áp dụng các kĩ thuật đồng bộ lock-free. NET Framework 4 đi kèm theo các thread-safe collection sau: ConcurrentQueue, ConcurrentBag, ConcurrentStack, ConcurrentDictionary. Các tập hợp này có các phương thức cho phép thêm, xóa và lặp qua các phần tử trong môi trường đa luồng một cách an toàn và hiệu quả. Đây là các công cụ hữu ích mà lập trình viên nên tìm hiểu sử dụng để tiết kiệm thời gian và công sức.
Kết luận:
Lập trình đa luồng là một đề tài lớn và phức tạp. Trong khuôn khổ giới hạn, bài viết chỉ trình bày về những nét mới mà C# 4.0 đem lại, hi vọng ít nhiều sẽ giúp ích cho các lập trình viên phải thường xuyên đối mặt với công việc rắc rối nhưng đầy thú vị này.