作者:铁狼爷们儿 | 来源:互联网 | 2023-08-26 20:51
这篇文章主要是从实用的角度讲解并行计算需要了解的一些基础知识以及需要注意的地方,包括并行循环的方法、如何终止、线程安全、常用类型等几方面。关于TPL中提供的并行方法,这里就不
这篇文章主要是从实用的角度讲解并行计算需要了解的一些基础知识以及需要注意的地方,包括并行循环的方法、如何终止、线程安全、常用类型等几方面。
关于TPL中提供的并行方法,这里就不再多说了,网上有很多例子,本系列文章第三个Topic主要讲的不是“如何跑”,而是要讲一下“如何停”。
曾经查过很多关于二者的资料,可能是我理解的原因,总觉得很少有对其解释正确的,所以我觉得还是有必要写出我的观点,请大家指正。
Section 1.并行循环的终止:Break与Stop
关于二者网上有很多解释,很多人都认为,在并行计算中:
1.Break的调用会导致当前任务和已分配任务的终止
2.Stop的调用会导致当前任务的终止
本人对以上两点持怀疑态度,经过试验证明,Break和Stop并不像以上说的那样。
这里先解释一下名词,“当前任务”指的是当前已经触发Break(Stop)条件的那个任务;“已分配任务”指的是与“当前任务”并行执行的任务;“未分配任务”指尚未开始的循环部分。
有以下测试代码,这些代码在不同核数量的机器上返回的记录数是不同的,这一点待会再说,先看代码:
class Program
{
static List<int> Data = new List<int>();
static ParallelOptions opt = new ParallelOptions();
static void Main(string[] args)
{
opt.MaxDegreeOfParallelism = Environment.ProcessorCount;
for (int i = 0; i < 10; i++)
{
Data.Add(i);
}
Console.WriteLine("CPU Degree:" + opt.MaxDegreeOfParallelism);
Console.WriteLine("GeneralFor Result:");
new Program().GeneralFor();
Console.WriteLine("ParallerStop Result:");
new Program().ParallerStop();
Console.WriteLine("ParallelBreak Result:");
new Program().ParallelBreak();
Console.Read();
}
//一个普通的For循环
private void GeneralFor()
{
for (int i = 0; i < Data.Count; i++)
{
if (Data[i] > 5)
break;
Console.WriteLine(Data[i]);
}
}
//并行计算的Stop
private void ParallerStop()
{
Parallel.For(0, Data.Count,opt, (i, LoopState) =>
{
if (Data[i] > 5)
LoopState.Stop();
Thread.Sleep(10);
Console.WriteLine(Data[i]);
});
}
//并行计算的Break
private void ParallelBreak()
{
Parallel.For(0, Data.Count, opt, (i, LoopState) =>
{
if (Data[i] > 5)
LoopState.Break();
Thread.Sleep(10);
Console.WriteLine(Data[i]);
});
}
}
下图分别是程序在2(32#)、4(64#)、48(64#)核CPU下运行的结果:
个人认为,不同CPU核数量对于使用Break与Stop终止循环形式的最终记录返回数量是有影响的,即这会影响TPL分配任务的方针,TPL在运行时才会“源源不断”的分配任务,开启的线程数也是递增形式的。(最大线程数应该有限制,具体是多少不确定,有的说是64也有的说是256,等待高人解答)
所以,对于Stop与Break,我的观点是,在并行任意任务中调用Stop方法,则会终止除当前任务外的所有并行任务(包括未分配的和已分配的),返回最终结果,而不是像传说的那样,终止了当前任务;Break只会停止继续分配新任务,并不影响当前任务的和已经分配的并行任务的执行,而且同样不会终止当前任务。
Stop更改ParallelLoopState 对象的 IsStop值为true;
Break更改ParallelLoopState 对象的 LowestBreakIteration 属性值等于 true 。
|
未分配的任务(未开始的任务)
|
已分配的任务(并行中的任务)
|
当前任务(触发条件的那个任务)
|
单行中的Break
|
停止分配
|
停止执行(相当于“当前任务”)
|
停止执行
|
Break
|
停止分配
|
继续执行
|
继续执行
|
Stop
|
停止分配
|
停止执行
|
继续执行
|
如上图中显示,调用Stop后,结果中只能可能有一个大于5的结果,因为此时未分配和已分配的任务都被终止了,而当前任务并没有停止,继续执行打印语句,打印出来的只是当前的那个大于5的任务;
而调用Break后,结果中大于5的结果数量不定,因为当当前任务因满足大于5的条件,而触发Break后,其他已分配的任务并不会停止,即使它们包含大于5的任务,同样也会打印出来,这就是上图中第三幅图中,Break任务出现两个大于5的结果的原因!
Stop和Break,它们的区别可以用下图表示:
图中实现表示运行的任务,虚线表示任务运行过程中被终止。
Section 2.线程安全
在并行计算中应当使用线程安全的类,例如有些时候我们需要不断迭代形成一个集合组织,这个集合可能是一个列表,在普通程序中List完全可以完成这个任务,但是如果在并行计算中使用List的add方法,就会出现一些错误,这些错误是随机的,也就是说有时候并不出现。
以下代码:
List<string> ls = new List< string >();
Parallel.For(0, 10000, (i) =>
{
ls.Add(i.ToString());
});
以上代码偶尔会出现错误,出现错误的概率随着循环次数和并行任务的增加而增大,类似代码需求可以使用ConcurrentBag来代替,针对并行计算提供的类库请参照MSDN:http://msdn.microsoft.com/zh-cn/library/dd287108.aspx
提示:并行计算并不是天上掉下的馅饼,它不是所有时候都比串行程序快,因为并行总要付出一些额外的代价,比如任务分配、任务同步、任务通讯等,到底什么时候才能放心的吃掉这个馅饼,需要仔细地设计算法,并且应该在多台典型的服务器环境中进行测试、对比,这是一个比较烦人的过程,但是最终得到的结果可能会改变你的设计思路。
具体可以参考我的文章:
DotNet并行计算的使用误区(一)
http://www.cnblogs.com/isline/archive/2011/04/20/2022228.html
DotNet并行计算的使用误区(二)
http://www.cnblogs.com/isline/archive/2011/04/21/2023137.html