C#通过Parallel
类提供并行任务支持,可以很简单的使用线程池。
要注意的是,Parallel
类不保障执行顺序。
最简单的使用方式莫过于使用Parallel.For
或ForEach
方法,它的语法和C#本身的循环类似。
public static void Log(string prefix)
{
WriteLine($"{prefix}, task: {Task.CurrentId}, " + $"thread: {Thread.CurrentThread.ManagedThreadId}");
}
public static void ParallelFor()
{
ParallelLoopResult result = Parallel.For(0, 10, i =>
{
Log($"S {i}");
Task.Delay(10).Wait();
Log($"E {i}");
});
//会等待并行任务完全结束才会继续程序流
WriteLine($"Is completed: {result.IsCompleted}");
}
下面是部分执行结果,说明执行顺序不可预期:
S 1, task: 9, thread: 3
S 0, task: 4, thread: 1
S 5, task: 1, thread: 7
S 7, task: 7, thread: 9
S 2, task: 8, thread: 4
S 8, task: 2, thread: 10
S 3, task: 5, thread: 5
上面代码会在并行任务执行完毕后才返回,也可以利用async及await来实现不等待任务执行完毕,而是只等待至并行任务创建完毕即返回,因此要注意生存期问题。
public static void ParallelForWithAsync()
{
ParallelLoopResult result = Parallel.For(0, 10, async i =>
{
Log($"S {i}");
await Task.Delay(10);
Log($"E {i}");
});
//任务创建完毕就继续程序流
WriteLine($"Is completed: {result.IsCompleted}");
}
其执行结果如下,会发现Log($"E {i}");语句结果已经没有了对象Take.CurrentId。
这是因为等待语句是异步的,所以当线程继续执行时,ParallelForWithAsync()方法已经返回。
如果前台线程在该方法返回此时已经结束,那么Is completed后面将不会有其他输出。
S 8, task: 6, thread: 10
S 2, task: 7, thread: 4
S 6, task: 9, thread: 8
S 1, task: 5, thread: 3
S 0, task: 3, thread: 1
S 5, task: 8, thread: 7
S 9, task: 6, thread: 10
S 4, task: 4, thread: 6
S 3, task: 1, thread: 5
S 7, task: 2, thread: 9
Is completed: True
E 7, task: , thread: 7
E 6, task: , thread: 9
E 8, task: , thread: 9
利用接受Action<int, ParallelLoopState>
作为第三个参数的重载方法,可以实现并行任务的提前结束。要注意的是,已经在运行的任务不会被强行终止。当ParallelLoopState.Break()
被调用后ParallelLoopState.ShouldExitCurrentIteration
属性会被设置为true
。
而每次Parallel
迭代前都会检查ShouldExitCurrentIteration
值是否为true
,如是则检查本次迭代是否大于ParallelLoopState.LowestBreakIteration
,如成立则本次迭代立即返回,不会被执行。
另外还有一种Stop()
方式,该方法与Break()
最大的不同在于,Stop()
调用后,任何后来的迭代都不会被启动,不论其迭代值为何,但Stop()
也不会强行终止正在运行的任务。
如果并行的任务每个都是长耗时的任务,而又需要Stop()
同时终止这些正在执行的任务,那么最好的方法是任务中的部分结点自己检查ParallelLoopState.IsStopped
属性来判断是否中止运行。
public static void StopParallelForEarly()
{
ParallelLoopResult result =
Parallel.For(10, 30, (int i, ParallelLoopState pls) =>
{
Log($"S {i}");
if( i > 15 )
{
pls.Break();
Log($"Break out... {i}");
}
Task.Delay(10).Wait();
Log($"E {i}");
});
WriteLine($"Is completed: {result.IsCompleted}");
WriteLine($"lowest break iteration: {result.LowestBreakIteration}");
}
下面是其执行结果,因为执行顺序不可预期,因此同时有多个线程调用了pls.break()
但他们都不会被强制结束。另外可以看到,迭代值小于设定的任务仍然可以正常的启动。
S 14, task: 3, thread: 4
S 20, task: 6, thread: 7
S 12, task: 2, thread: 3
S 16, task: 4, thread: 5
S 24, task: 9, thread: 9
Break out... 16, task: 4, thread: 5
Break out... 24, task: 9, thread: 9
Break out... 20, task: 6, thread: 7
S 18, task: 5, thread: 6
Break out... 18, task: 5, thread: 6
S 10, task: 1, thread: 1
S 22, task: 8, thread: 8
S 26, task: 7, thread: 10
Break out... 26, task: 7, thread: 10
Break out... 22, task: 8, thread: 8
E 18, task: 5, thread: 6
E 16, task: 4, thread: 5
E 24, task: 9, thread: 9
E 10, task: 1, thread: 1
S 13, task: 1, thread: 1
S 11, task: 10, thread: 12
E 20, task: 6, thread: 7
E 22, task: 8, thread: 8
E 26, task: 7, thread: 10
E 12, task: 2, thread: 3
E 14, task: 3, thread: 4
S 15, task: 11, thread: 3
E 11, task: 10, thread: 12
E 15, task: 11, thread: 3
E 13, task: 1, thread: 1
Is completed: False
lowest break iteration: 16
有时候线程需要被初始化,这时候可以使用另一个重载方法For<TLocal>(Int32, Int32, Func<TLocal>, Func<Int32,ParallelLoopState,TLocal,TLocal>, Action<TLocal>)
。
它看起来有一点复杂,仔细看一下它的参数列表:
类型参数
TLocal
The type of the thread-local data.参数
fromInclusive
Int32
The start index, inclusive.toExclusive
Int32
The end index, exclusive.localInit
Func
The function delegate that returns the initial state of the local data for each task.body
Func<Int32,ParallelLoopState,TLocal,TLocal>
The delegate that is invoked once per iteration.localFinally
Action
The delegate that performs a final action on the local state of each task.返回
ParallelLoopResult
A structure that contains information about which portion of the loop completed.
实际上是将并行任务分成三个部分:
- 初始化(localInit)部分(Func)
- 执行体(localBody)部分(Func<Int32, ParallelLoopState, TLocal, TLocal)
- 结束体(localFinally)部分(Action)
下面的代码会让每个线程使用时和即将退出时分别执行初始化和结束体一次,而线程被重用时,初始化并不会重复执行,上次执行体返回的结果将作为重用时执行体的参数,因为这个特性,所以用来做大数据的累加就很方便了。
public static void ParallelForWithInit()
{
Parallel.For<string>(0, 10, () =>
{
Log($"init thread");
return $"t{Thread.CurrentThread.ManagedThreadId}";
},
(i, pls, str1) =>
{
Log($"body i {i} str1 {str1}");
Task.Delay(10).Wait();
return $"i {i}";
},
(str1) =>
{
Log($"finally {str1}");
});
}
下面是其部分执行结果,可以发现线程1被重用了。
如上所述,在迭代值为9的时候,使用的是已经初始化过的线程1。
因此其str1的参数为上次线程1执行体所返回的"i 0"。
init thread, task: 6, thread: 7
init thread, task: 8, thread: 9
init thread, task: 7, thread: 8
init thread, task: 9, thread: 10
init thread, task: 3, thread: 4
body i 2 str1 t4, task: 3, thread: 4
body i 6 str1 t8, task: 7, thread: 8
body i 8 str1 t10, task: 9, thread: 10
init thread, task: 4, thread: 5
body i 3 str1 t5, task: 4, thread: 5
body i 7 str1 t9, task: 8, thread: 9
init thread, task: 5, thread: 6
body i 4 str1 t6, task: 5, thread: 6
init thread, task: 2, thread: 3
init thread, task: 1, thread: 1
body i 5 str1 t7, task: 6, thread: 7
body i 1 str1 t3, task: 2, thread: 3
body i 0 str1 t1, task: 1, thread: 1
body i 9 str1 i 0, task: 1, thread: 1 //线程1被重用
finally i 3, task: 4, thread: 5
finally i 6, task: 7, thread: 8
finally i 5, task: 6, thread: 7
finally i 4, task: 5, thread: 6
finally i 2, task: 3, thread: 4
finally i 8, task: 9, thread: 10
finally i 7, task: 8, thread: 9
finally i 1, task: 2, thread: 3
finally i 9, task: 1, thread: 1
ForEach()
的用法和For()
类似,不同的是前者使用 可迭代对象(IEnumerable) 作为参数而非索引。
该类还提供一个Invoke()
方法,和前两者均不同,该方法使用 可变参委托(params Action[]
) 作为参数,如此一来就可以执行不同的任务了。
关于 .Net Framework \ Core 并行编程的更多细节可参阅: