Parallel.Invoke():并行调用
重载:
- Invoke(Action[]) : 尽可能并行执行提供的每个操作。
- Invoke(ParallelOptions, Action[]) :执行所提供的每个操作,而且尽可能并行运行,除非用户取消了操作。
需要并行调用一批方法,并且这些方法(大部分)是互相独立的。
如果多个任务并行运行,就可以使用 Parallel.Invoke() 方法。他提供了任务并行的模式。Parallel.Invoke()允许传递一个 Action 委托数组,在其中可以指定运行方法。
Dome1:
static void Main(string[] args)
{
Parallel.Invoke(F1, F2);
Console.ReadKey();
}
static void F1()
{
Console.WriteLine("方法1");
}
static void F2()
{
Console.WriteLine("方法2");
}
简单的 方法直接写 lambda 表达式更好。
Dome2: 实例
static void Main(string[] args)
{
double[] arr = { 12.34, 234.12, 5.445, 1200.002, 349.22, 736.294, 482, 48.11, 1.0098, 3.222 };
Console.Write("源数据:");
foreach (var item in arr)
{
Console.Write(item+" ");
}
Console.WriteLine();
ProcessArray(arr);
Console.Write("改变后:");
foreach (var item in arr)
{
Console.Write(item + " ");
}
Console.ReadKey();
}
static void ProcessArray(double[] array)
{
Parallel.Invoke(
() => ProcessPartialArray(array, 0, array.Length / 2),
() => ProcessPartialArray(array, array.Length / 2, array.Length)
);
}
static void ProcessPartialArray(double[] array, int begin, int end)
{
if (begin == 0 && end == array.Length / 2)
{
for (int i = 0; i < array.Length / 2; i++)
{
array[i] = array[i] / 10;
}
}
else if (begin == array.Length / 2 && end == array.Length)
{
for (int i = array.Length / 2; i < array.Length; i++)
{
array[i] = array[i] * 10;
}
}
}
输出:
如果在运行之前都无法确定调用的数量,就可以在Parallel.Invoke 函数中输入一个委托
数组:
static void DoAction20Times(Action action)
{
Action[] actions = Enumerable.Repeat(action, 20).ToArray();
Parallel.Invoke(actions);
}
就像Parallel 类的其他成员一样,Parallel.Invoke 也支持取消操作:
static void DoAction20Times(Action action, CancellationToken token)
{
Action[] actions = Enumerable.Repeat(action, 20).ToArray();
Parallel.Invoke(new ParallelOptions { CancellationToken = token }, actions);
}
对于简单的并行调用,Parallel.Invoke 是一个非常不错的解决方案。然而在以下两种情况中使Parallel.Invoke 并不是很合适:要对每一个输入的数据调用一个操作(改用Parallel.Foreach),或者每一个操作产生了一些输出(改用并行LINQ)。
动态并行:
问题: 并行任务的结构和数量要在运行时才能确定,这是一种更复杂的并行编程。
解决方案:任务并行库(TPL) :任务并行库(TPL)是以Task 类为中心构建的。Task 类的功能很强大,Parallel 类和并行LINQ 只是为了使用方便,从而对Task 类进行了封装。实现动态并行最简单的做法就是直接使用Task 类。
下面的例子对二叉树的每个节点进行处理,并且该处理是很耗资源的。二叉树的结构在运行时才能确定,因此非常适合采用动态并行。Traverse 方法处理当前节点,然后创建两个子任务,每个子任务对应一个子节点(本例中,假定必须先处理父节点,然后才能处理子节点)。ProcessTree 方法启动处理过程,创建一个最高层的父任务,并等待任务完成:
void Traverse(Node current)
{
DoExpensiveActionOnNode(current);//执行复杂的操作在当前节点上
if (current.Left != null)
{
Task.Factory.StartNew(() => Traverse(current.Left),
CancellationToken.None,
TaskCreationOptions.AttachedToParent,
TaskScheduler.Default);
}
if (current.Right != null)
{
Task.Factory.StartNew(() => Traverse(current.Right),
CancellationToken.None,
TaskCreationOptions.AttachedToParent,
TaskScheduler.Default);
}
}
public void ProcessTree(Node root)
{
var task = Task.Factory.StartNew(() => Traverse(root),
CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.Default);
task.Wait();
}
如果这些任务没有“父/ 子”关系,那可以使用任务延续(continuation)的方法,安排任务一个接着一个地运行。这里continuation 是一个独立的任务,它在原始任务结束后运行:
Task task = Task.Factory.StartNew(
() => Thread.Sleep(TimeSpan.FromSeconds(2)),
CancellationToken.None,
TaskCreationOptions.None,
TaskScheduler.Default);
Task continuation = task.ContinueWith(
t => Trace.WriteLine("Task is done"),
CancellationToken.None,
TaskContinuationOptions.None,
TaskScheduler.Default);
// 对continuation 来说,参数“t”相当于“task”
上面的例子使用了CancellationToken.None 【取消令(cancellationtoken)】 和TaskScheduler.Default【任务调度器(task scheduler)】。最好在StartNew和ContinueWith 中明确指定用TaskScheduler。
在动态并行中,通常以“父/ 子”的方式对任务进行安排,但也不是必须要这么做。把每个新任务存储在线程安全的集合中,然后用Task.WaitAll 等待所有任务完成,这种做法同样可行。
在并行处理中使用Task 类,和在异步处理中使用完全不同:
1:在并发编程中,Task 类有两个作用:作为并行任务,或作为异步任务。并行任务可以使用阻塞的成员函数,例如Task.Wait、Task.Result、Task.WaitAll 和Task.WaitAny。并行任务通常也使用AttachedToParent 来建立任务之间的“父/ 子”关系。并行任务的创建需要用Task.Run 或者Task.Factory.StartNew。
2:相反,异步任务应该避免使用阻塞的成员函数,而应该使用await、Task.WhenAll 和Task.WhenAny。异步任务不使用AttachedToParent,但可以通过await 另一个任务,建立一种隐式的“父/ 子”关系。
Parallel 能简化同步状态下的 Task ,但不等同于 Task默认行为。
也就是说在运行 Parallel 中的 For , ForEach 方法时,调用者线程(在实例中就是主线程)是被阻滞的。 Parallel 虽然将让任务交给 Task 去处理,即交给 CLR 线程池处理,不过调用者会一直等到线程池的相关工作全部完成。表示并行的静态类 Parallel 只提供了 Invoke 方法,而没有同时提供BeginInvoke 方法,这也从一定程度说明这个问题。
在使用 Task 时,我们经常使用的是: Start 方法( Task 也提供了 RunSynchronously),它不会阻滞调用者线程。
Dome3 实例:
static void Main(string[] args)
{
Task t = new Task(() =>
{
while(true)
{
}
});
t.Start();
Console.WriteLine("主线程即将结束!");
Console.ReadKey();
}
使用 Parallel 相近的功能,主线程被阻滞。
static void Main(string[] args)
{
Parallel.Invoke(() =>
{
while(true)
{
}
});
Console.WriteLine("主线程即将结束!");
Console.ReadKey();
}
如果执行这段代码,永远不会有输出。
并行编程,意味者运行时在后台将任务分配到尽量多的CPU上,虽然它在后台使用 Task 进行管理,但这并不意味者它等同于异步。