在Parallel下面有三个常用的方法invoke,for和forEach。
首先感受一下3个方法的调用情况:
// For
Parallel.For(0, 1000, (i) =>
{
//i是从0开始一直到1000结束
});
// ForEach
var lst = new List<string>();
Parallel.ForEach(lst, (s) =>
{
//do something
});
//Invoke
Parallel.Invoke(
() => { },
() => { },
() => { }
);
1: Parallel.Invoke
这是最简单,最简洁的将串行的代码并行化。
例如:可以这样调用
internal static void ParallelMothed()
{Parallel.Invoke(Run1, Run2); //这里的Run1 Run2 都是方法。
}
static void Run1()
{
Console.WriteLine("我是任务一,我跑了3s");
Thread.Sleep(3000);
}static void Run2()
{
Console.WriteLine("我是任务二,我跑了5s");
Thread.Sleep(5000);
}
在这个例子中可以获取二点信息:
第一:一个任务是可以分解成多个任务,采用分而治之的思想。
第二:尽可能的避免子任务之间的依赖性,因为子任务是并行执行,所以就没有谁一定在前,谁一定在后的规定了。
第三:主线程必须等Invoke中的所有方法执行完成后返回才继续向下执行;这样对我们以后设计并行的时候,要考虑每个Task任务尽可能差不多,如果相差很大,比如一个时间非常长,其他都比较短,这样一个线程可能会影响整个任务的性能。这点非常重要
第四:没有固定的顺序,每个Task可能是不同的线程去执行,也可能是相同的;
如果调用的方法是有参数的,如何处理?同理,直接带上参数就可以,
Parallel.Invoke(() => Task1("task1"), () => Task2("task2"), () => Task3("task3"));
例如:
class ParallelInvoke
{
// Invoke带参数 调用
public void Client1()
{
Stopwatch stopWatch = new Stopwatch();Console.WriteLine("主线程:{0}线程ID : {1};开始", "Client1", Thread.CurrentThread.ManagedThreadId);
stopWatch.Start();
Parallel.Invoke(() => Task1("task1"), () => Task2("task2"), () => Task3("task3"));
stopWatch.Stop();
Console.WriteLine("主线程:{0}线程ID : {1};结束,共用时{2}ms", "Client1", Thread.CurrentThread.ManagedThreadId, stopWatch.ElapsedMilliseconds);
Console.ReadKey();
}private void Task1(string data)
{
Thread.Sleep(5000);
Console.WriteLine("任务名:{0}线程ID : {1}", data, Thread.CurrentThread.ManagedThreadId);
}private void Task2(string data)
{
Console.WriteLine("任务名:{0}线程ID : {1}", data, Thread.CurrentThread.ManagedThreadId);
}private void Task3(string data)
{
Console.WriteLine("任务名:{0}线程ID : {1}", data, Thread.CurrentThread.ManagedThreadId);
}
}
}
执行运行后结果:
ps:如果其中有一个异常怎么办? 带做这个问题修改了增加了一个Task4.
public class ParallelInvoke
{
/// <summary>
/// Invoke方式一 action
/// </summary>
public void Client1()
{
Stopwatch stopWatch = new Stopwatch();
Console.WriteLine("主线程:{0}线程ID : {1};开始", "Client1", Thread.CurrentThread.ManagedThreadId);
stopWatch.Start();try
{
Parallel.Invoke(() => Task1("task1"), () => Task2("task2"), () => Task3("task3"), delegate () { throw new Exception("我这里发送了异常"); });
}
catch (AggregateException ae)
{
foreach (var ex in ae.InnerExceptions)
Console.WriteLine(ex.Message);
}
stopWatch.Stop();
Console.WriteLine("主线程:{0}线程ID : {1};结束,共用时{2}ms", "Client1", Thread.CurrentThread.ManagedThreadId, stopWatch.ElapsedMilliseconds);
}
}
主要看 delegate() { throw new Exception("我这里发送了异常");} 增加了这个委托Task3. 然后我们看结果:
这里我们发现即使有异常程序也会完成执行,而且不会影响其他Task的执行。
在异步的时候可以获取当前线程的id:代码如:Thread.CurrentThread.ManagedThreadId)
3、 重载方法ParallelOptions 的使用。
理解ParallelOptions建议大家异步编程:轻量级线程同步基元对象 讲的非常详细。
主要理解两个参数:
CancellationToken 控制线程的取消
MaxDegreeOfParallelism 设置最大的线程数,有时候可能会跑遍所有的内核,为了提高其他应用程序的稳定性,就要限制参与的内核
下面从代码上看效果如何?
public class ParallelInvoke
{
// 定义CancellationTokenSource 控制取消
readonly CancellationTokenSource _cts = new CancellationTokenSource();/// <summary>
/// Invoke方式一 action
/// </summary>
public void Client1()
{
Console.WriteLine("主线程:{0}线程ID : {1};开始{2}", "Client3", Thread.CurrentThread.ManagedThreadId, DateTime.Now);
var po = new ParallelOptions
{
CancellationToken = _cts.Token, // 控制线程取消
MaxDegreeOfParallelism = 3 // 设置最大的线程数3,仔细观察线程ID变化
};
Parallel.Invoke(po, () => Task1("task1"), ()=>Task5(po), Task6);
Console.WriteLine("主线程:{0}线程ID : {1};结束{2}", "Client3", Thread.CurrentThread.ManagedThreadId, DateTime.Now);
}private void Task1(string data)
{
Thread.Sleep(5000);
Console.WriteLine("任务名:{0}线程ID : {1}", data, Thread.CurrentThread.ManagedThreadId);
}// 打印数字
private void Task5(ParallelOptions po)
{
Console.WriteLine("进入Task5线程ID : {0}", Thread.CurrentThread.ManagedThreadId);
int i = 0;
while (i < 100)
{
// 判断是否已经取消
if (po.CancellationToken.IsCancellationRequested)
{
Console.WriteLine("已经被取消。");
return;
}Thread.Sleep(100);
Console.Write(i + " ");
Interlocked.Increment(ref i);
}}
/// <summary>
/// 10秒后取消
/// </summary>
private void Task6()
{
Console.WriteLine("进入取消任务,Task6线程ID : {0}", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(1000 * 10);
_cts.Cancel();
Console.WriteLine("发起取消请求...........");
}
}
执行结果:
从程序结果我们看到以下特点:
1、程序在执行过程中线程数码不超过3个。
2、CancellationTokenSource/CancellationToken控制任务的取消。
四、总结
Parallel.Invoke 的使用过程中我们要注意以下特点:
1、没有特定的顺序,Invoke中的方法全部执行完才返回,但是即使有异常在执行过程中也同样会完成,他只是一个很简单的并行处理方法,特点就是简单,不需要我们考虑线程的问题。
2、如果在设计Invoke中有个需要很长时间,这样会影响整个Invoke的效率和性能,这个我们在设计每个task时候必须去考虑的。
3、Invoke 参数是委托方法。
4、当然Invoke在每次调用都有开销的,不一定并行一定比串行好,要根据实际情况,内核环境多次测试调优才可以。
5、异常处理比较复杂。