C# - Khi Nào Nên Dùng Lớp Parallel


Sau bài "Parallel Class Trong C# và Vấn Đề Xử lý Song Song" tôi nhận được khá nhiều câu hỏi về vấn đề này, ví dụ như chúng ta có nên dùng hoàn toàn Parallel.* thay cho cách truyền thống để được lợi về thời gian cũng như đỡ tốn công đồng bộ Data khi dùng Multi Thread. Hôm nay tôi sẽ nói về vấn đề này





I> Parallel và Multi Thread



Chúng ta có thể thấy rằng bản chất của Parallel cũng chỉ là Multi-Thread, song tại sao lại có Parallel khi trước đó đã có Multi Thread:



- Chúng ta được lợi nhiều hơn khi dùng Parallel thay cho Multi Thread bởi việc đồng bộ Data, quản lý Thread, các vấn đề về deadlock... đều đã có người làm việc đó thay mình(Microsoft chịu hoàn toàn trách nhiệm về sản phẩm của họ .. hi)

- Là multi Thread nhưng việc Executeing Thread đó trên hệ thống mà CPU là đa nhân thì làm sao cho hiệu quả cao nhất, Microsoft cũng giải quyết vấn đề này ở tần Framework rồi sau đó "đẻ" ra Parallel hay còn gọi là Multi-Core.

Vậy nên, Parallel xét về mặt tiện lợi và an toàn(phó thác sự tin tưởng) cũng như sẳn dùng cũng như multi-core thì hơn hẳn Multi Thread, xét về việc mà Dev có thể linh hoạt quản lý các Thread và data thì multi Thread đáng dùng hơn.



II> Parallel.* và cách duyệt/lặp truyền thống



Hình trên cho ta thấy cách mà Parallel.* làm việc, nếu mới biết về khái niệm Parallel một số bạn sẽ ưu ái sử dụng nó thay cho cách truyền thống(Parallel.For/ForEach thay cho for/foreach) nhưng chúng ta cần hiểu hơn về cách mà nó làm việc. Ví dụ:


- Dưới đây là một ví dụ mà Pararallel khai thác hết hiệu xuất CPU. Chúng ta có một công việc "my Job", parallel.* sẽ chia công việc của chúng ta ra n phần( tức for chạy từ 1 - n), những phần này không có sự ràng buộc với nhau về dữ liệu trong suốt time xử lý vì thế sẽ không có sự đợi chờ giữa các Thread và các Core. Chúng ta rất lợi về thời gian khi kết quả "my Result" không cần sự tuân thủ về thứ tự các Thread.




Thực tế: Một đám ruộng cần phải gặt song càng sớm càng tốt, chúng ta có 5 người( tức Parallel.For chạy từ 1 - 5) , mỗi người sẽ gặt 1/5 đám ruộng, ai song trước thì nghĩ( là Parallel.For/ForEach) . Y vậy, nếu việc gặt lúa song song cho ta lợi 3t về time và việc chia phần đám ruộng cho 5 người mất 1t thì chúng ta vẫn lợi 2t về thời gian hơn so với việc 5 người lần lược thay nhau gặt ( là for/foreach). => Parallel.* lợi hơn


=> Lưu ý rằng trong trường hợp trên nhưng time bị mất cho việc chia phần công việc lớn hơn nhiều so với tổng chi phí công việc phải làm thì Parallel không phát huy hiệu quả =>  rất chậm. Chia phần công việc được hiểu là việc hệ thống sẽ tiến hành phân tích và chia Thread chia Core, tính toán đủ thứ.



parallel tuanpham



- Dưới đây là ví dụ về việc dùng Parallel không mang lại hiệu quả. Chúng ta có một công việc, Parallel sẽ chia công việc thành n phần, chúng xử lý song song nhưng thực tế không phải vậy khi mà các Thread phải đợi nhau để đồng bộ dữ liệu hoặc truyền data cho nhau. Kết quả phụ thuộc vào thứ tự công việc của từng Thread.




Thực tế: Một đám ruộng cần phải gặt song càng sớm càng tốt, chúng ta có 5 người( tức Parallel.For chạy từ 1 - 5) , vấn đề ở đây là chiếc "Liềm" của 5 người đều bị "Cùn" nên cần mài cho "Bén", ngan trái thay chỉ có một cục đá mài nên phải Share nhau. Giả sử trường hợp tệ nhất là 5 cái "Liềm" cùng cần mài trong một thời điểm, thế thì phải xếp hàng đợi mài liềm rồi mới đi gặt(t2); cộng thêm thời gian chia phần đám ruộng(t1) cho 5 người trước khi gặt thì ta có t1 + t2 thời gian. Trong khi nếu trước đó chúng ta chọn phương án là thay nhau gặt lúa thì chúng ta chỉ mất có t2 thời gian. => for.foreach lợi hơn



parallel tuanphamdg




III> Kết luận


Đây là bài test lý tưởng với Parallel




[code language="csharp"]
namespace ParallelTests
{
class Program
{
private static int Fibonacci(int x)
{
if (x <= 1)
{
return 1;
}
return Fibonacci(x - 1) + Fibonacci(x - 2);
}

private static void DummyWork()
{
var result = Fibonacci(10);
// inspect the result so it is no optimised away.
// We know that the exception is never thrown. The compiler does not.
if (result > 300)
{
throw new Exception("failed to to it");
}
}

private const int TotalWorkItems = 2000000;

private static void SerialWork(int outerWorkItems)
{
int innerLoopLimit = TotalWorkItems / outerWorkItems;
for (int index1 = 0; index1 < outerWorkItems; index1++)
{
InnerLoop(innerLoopLimit);
}
}

private static void InnerLoop(int innerLoopLimit)
{
for (int index2 = 0; index2 < innerLoopLimit; index2++)
{
DummyWork();
}
}

private static void ParallelWork(int outerWorkItems)
{
int innerLoopLimit = TotalWorkItems / outerWorkItems;
var outerRange = Enumerable.Range(0, outerWorkItems);
Parallel.ForEach(outerRange, index1 =>
{
InnerLoop(innerLoopLimit);
});
}

private static void TimeOperation(string desc, Action operation)
{
Stopwatch timer = new Stopwatch();
timer.Start();
operation();
timer.Stop();

string message = string.Format("{0} took {1:mm}:{1:ss}.{1:ff}", desc, timer.Elapsed);
Console.WriteLine(message);
}

static void Main(string[] args)
{
TimeOperation("serial work: 1", () => Program.SerialWork(1));
TimeOperation("serial work: 2", () => Program.SerialWork(2));
TimeOperation("serial work: 3", () => Program.SerialWork(3));
TimeOperation("serial work: 4", () => Program.SerialWork(4));
TimeOperation("serial work: 8", () => Program.SerialWork(8));
TimeOperation("serial work: 16", () => Program.SerialWork(16));
TimeOperation("serial work: 32", () => Program.SerialWork(32));
TimeOperation("serial work: 1k", () => Program.SerialWork(1000));
TimeOperation("serial work: 10k", () => Program.SerialWork(10000));
TimeOperation("serial work: 100k", () => Program.SerialWork(100000));

TimeOperation("parallel work: 1", () => Program.ParallelWork(1));
TimeOperation("parallel work: 2", () => Program.ParallelWork(2));
TimeOperation("parallel work: 3", () => Program.ParallelWork(3));
TimeOperation("parallel work: 4", () => Program.ParallelWork(4));
TimeOperation("parallel work: 8", () => Program.ParallelWork(8));
TimeOperation("parallel work: 16", () => Program.ParallelWork(16));
TimeOperation("parallel work: 32", () => Program.ParallelWork(32));
TimeOperation("parallel work: 64", () => Program.ParallelWork(64));
TimeOperation("parallel work: 1k", () => Program.ParallelWork(1000));
TimeOperation("parallel work: 10k", () => Program.ParallelWork(10000));
TimeOperation("parallel work: 100k", () => Program.ParallelWork(100000));

Console.WriteLine("done");
Console.ReadLine();
}
}
}
[/code]

Kết quả




[code language="c"]
serial work: 1 took 00:02.31
serial work: 2 took 00:02.27
serial work: 3 took 00:02.28
serial work: 4 took 00:02.28
serial work: 8 took 00:02.28
serial work: 16 took 00:02.27
serial work: 32 took 00:02.27
serial work: 1k took 00:02.27
serial work: 10k took 00:02.28
serial work: 100k took 00:02.28

parallel work: 1 took 00:02.33
parallel work: 2 took 00:01.14
parallel work: 3 took 00:00.96
parallel work: 4 took 00:00.78
parallel work: 8 took 00:00.84
parallel work: 16 took 00:00.86
parallel work: 32 took 00:00.82
parallel work: 64 took 00:00.80
parallel work: 1k took 00:00.77
parallel work: 10k took 00:00.78
parallel work: 100k took 00:00.77
[/code]

Parallel là một giải pháp tốt cho multi-Thread và multi-Core, nhưng chúng ta không nên lạm dụng. Hãy hiểu rõ bản chất vấn đề và hiện thực bạn đang đối mặt để chọn gải pháp tốt nhất. Phải chắc chắn rằng bài toán ban đang giải quyết là một bài toán Parallelizable thì mới dùng parallel. Cuối cùng, nên nhớ rằng "đừng lấy dao mổ trâu đi giết gà" và ngược lại. Trên đây là sự hiểu biết cá nhân, rất mong được góp ý.


Phạm Tuân


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