WPF - Các Vấn Đề Về Multi-Thread Trong WPF (Đa Luồng)


Đa số các chip xử lý (CPU) hiện còn đang hoạt động mà chúng ta biết đến đều có trên hai nhân(lõi) và tính năng ảo hóa - điều này làm cho các máy tính có được tốc độ xử lý nhanh hơn và khả năng đa nhiệm tốt hơn. Chính vì vậy để tận dụng hết khả năng của CPU chúng ta cần xử lý đồng thời trên nhiều lõi CPU cho nhiều bài toán khác nhau để tiết kiệm thời gian và nâng cao hiệu xuất. Trong vấn đề trên và trong lập trình ứng dụng .NET nói riêng chúng ta có thuật ngữ Thread (luồng xử lý/tiểu trình), chúng ta sẽ tìm hiểu về nó trong bài viết này.




Bạn không nên nhầm lẫn giữa process (tiến trình) và thread (tiểu trình). Process có thể hiểu là một instance của chương trình máy tính được thực thi, dựa trên hệ điều hành, hoàn toàn độc lập với các tiến trình khác. Còn thread là một nhóm lệnh được tạo ra để thực thi một tác vụ trong một process, chúng chia sẻ chung dữ liệu với nhau để xử lý, điều này là cần thiết nhưng cũng là nguyên nhân dễ gây ra lỗi nếu bạn không xử lý đúng cách. Các bạn có thể tham khảo về Class Thread tại đây




[caption id="" align="aligncenter" width="735"]tuanphamdg Mô tả sự tiết kiệm về thời gian[/caption]

Trong nội dung này tôi không nói sâu về Thread trong C#, các bạn có thể tìm hiểm thêm tại đây, tôi sẽ xoay quanh về cách Thread hoạt động với các thành phần của WPF, xong tôi sẽ vẫn nhắc các bạn nhớ về Thread và Cách tạo một Thread như sau




[code language="csharp"]
public void CreateThread()
{
Thread thread = new Thread(new ThreadStart(MethodJob));
//MethodJob có thể là một delegate

//Run thread
thread.Start();

//Buộc hoãn thực thi đến khi được phép
thread.Suspend();

//Gọi thread tiếp tục chạy
thread.Resume();

//Buộc thread phải dừng
thread.Abort();

//Đợi thread xử lý xong
thread.Join();

}

private void MethodJob()
{
for (int i = 0; i < 100; i++)
{
//Do something
}
}
[/code]

Vấn đề Thread trong WPF - Truy cập an toàn một Control
Trong winform, một Thread phụ được tạo ra không thể tự mình truy cặp được các Control của Thread chính một cách an toàn được, chúng ta sẽ phải dùng delegate, trong WPF cũng thế; Nhưng trong WPF, mỗi Control đều chứa một đối tượng là Dispatcher - đối tượng này giúp đồng bộ các truy cập đến Control hiện tại và tránh sảy ra lỗi(tương tực việc lock(object)). Vì vậy, khi muốn thực hiện một truy cập an toàn đến một Control bất kỳ chúng ta cần gọi Dispatcher của control đó và phương thức xử lý Invoke(delegate), phương thức này chứa một delegate để làm việc với các thuộc tính của Control.


Ví dụ dưới đây phục vụ nhu cầu đếm lần lược từ 0->99 và hiện số đó lên TextBox




[code language="csharp"]
private void BtnRun_OnClick(object sender, RoutedEventArgs e)
{
//thread là một biến toàn cục kiểu Thread
if (thread != null)
{
thread.Abort(); //Dừng công viện hiện tại
}

//delegate được tạo theo định nghĩa Lambeda
thread = new Thread(new ThreadStart(() =>
{
//nội dung delegate
for (int i = 0; i < 100; i++)
{
Thread.Sleep(1000);

//An toàn truy cập với Dispatcher
txtData.Dispatcher.Invoke(() => txtData.Text = i.ToString());

//Vùng giới hạn an toàn rộng hơn
//this.Dispatcher.Invoke(() => txtData.Text = i.ToString());
}
}));

thread.IsBackground = true;
thread.Start();
}
[/code]

Ví dụ dưới đây phục vụ nhu cầu đếm lần lược từ x->99 và hiện số đó lên TextBox, với x là một số được truyền vào, tôi truyền vào số 10. Lưu ý tôi có hai delegate(Counter,Counters) chịu trách nhiệm hiển thị số đếm lên TextBox nhưng chúng có cách hiển thị khác nhau, các bạn hãy chú ý về cách truyền đối số cho Thread và hai delegate. Tôi gọi phương thức Invoke(delegate, object[]).




[code language="csharp"]
private void BtnRun2_OnClick(object sender, RoutedEventArgs e)
{
if (thread2 != null)
{
thread2.Abort();
}

thread2 = new Thread(new ParameterizedThreadStart(Run2));

thread2.IsBackground = true;
thread2.Start(10);
}

private delegate void Counter(int num);
private delegate void Counters(int num,int limit);

private void Run2(object numStart)
{
for (int i = (int)numStart; i < 100; i++)
{
//Cách truyền một tham số
this.Dispatcher.Invoke(new Counter(StartCount), i);

//Cách truyền nhiều tham số
this.Dispatcher.Invoke(new Counters(StartCounts), new object[] {i, 40});

Thread.Sleep(800);
}
}

private void StartCount(int num)
{
if (num < 20)
{
txtData2.Text = num.ToString();
}
else
{
txtData2.Text = "---";
}
}
private void StartCounts(int num,int limit)
{
if (num > 30 && num < limit)
{
txtData2.Text = num.ToString();
}
}
[/code]

Vấn đề Thread trong WPF - Show một Window từ Thread phụ
Nếu nhu cầu là một thread phụ được tạo ra để thực hiện một công việc nào đó và trong đó cần Show một Window thì bạn sẽ làm thế nào. Nếu bạn có thói quen dùng delegate như đoạn code bênh dưới chắc chắn rằng bạn sẽ gặp lỗi "The calling thread must be STA, because many UI components require this."




[code language="csharp"]
private void BtnRun4_OnClick(object sender, RoutedEventArgs e)
{
Thread thread5 = new Thread(new ThreadStart(ShowSubForm));
thread5.IsBackground = true;
thread5.Start();
}

private void ShowSubForm()
{
new MainWindow().Show();
}
[/code]

Vậy ta khắc phục lỗi trên như sau, cần tạo ra một Thread STA




[code language="csharp"]
private void BtnRun4_OnClick(object sender, RoutedEventArgs e)
{
Thread thread5 = new Thread(new ThreadStart(ShowSubForm));

thread5.SetApartmentState(ApartmentState.STA);
thread5.IsBackground = true;
thread5.Start();
}

private void ShowSubForm()
{
new MainWindow().Show();

//Nói rằng đây là một Window độc lập
System.Windows.Threading.Dispatcher.Run();
}
[/code]

Chúng ta còn cách khác đơn giản hơn với Dispathcher, bạn hảy thử




[code language="csharp"]
private void BtnRun4_OnClick(object sender, RoutedEventArgs e)
{
Thread thread5 = new Thread(new ThreadStart(ShowSubForm));

thread5.IsBackground = true;
thread5.Start();
}

private void ShowSubForm()
{
this.Dispatcher.Invoke(() =>
{
new MainWindow().Show();
});
}
[/code]

Vậy là chúng ta đã biết thêm về các vấn đề về Thread trong WPF, các bạn có thể tham khảo SourceCode
Chúc các bạn thành công!
Phạm Tuân


Chúc các bạn thành công!
PHẠM TUÂN