并行锁定.foreach会降低c#中的性能

时间:2014-03-19 15:11:59

标签: c# parallel-processing

我有一个示例程序,其中我试图复制我的实际应用程序scenerio。有一种方法只锁定一次rahter而不是每个循环实际降低并行循环的性能。如果我删除锁定性能为预期,但我在竞争条件下运行。我在GetTotal方法中有一些代码也进入竞争条件。在多线程试图修改共享变量的情况下,可以进行并行处理。是否有更好的改善长时间运行性能

private static void Main()
{
    var datetime = DateTime.Now;

    int j = 0;
    Parallel.ForEach(Enumerable.Range(0, 5), i =>
    {   
         lock (SomeLockObject)
         {
            Console.WriteLine(j++);
            GetTotal(j);                       
         }                          
    });

    Console.WriteLine(DateTime.Now.Second - datetime.Second);
    Console.ReadLine();
}

static long GetTotal(int j)
{
    long total = 0;
    for (int i = 1; i < 1000000000; i++)    // Adjust this loop according
    {                                       // to your computer's speed
        total += i + j;
     }
     return total;
 }

3 个答案:

答案 0 :(得分:1)

您可以查看C#的Concurrent库。它们允许更快的线程读/写共享数据。

这来自上面的Concurrent链接:

  

某些并发集合类型使用轻量级   同步机制,如SpinLock,SpinWait,SemaphoreSlim,   和CountdownEvent,它们是.NET Framework 4中的新增功能   同步类型通常在短时间内使用忙碌旋转   在他们将线程置于真正的等待状态之前。等待时间   预计非常短,旋转的计算量要小得多   比等待昂贵,这涉及昂贵的内核过渡。   对于使用旋转的集合类,这种效率意味着   多个线程可以以非常高的速率添加和删除项目。对于   有关旋转与阻塞的更多信息,请参阅SpinLock和   SpinWait。

这些类只能解决线程之间更快通信的问题。看起来你应该只在你绝对需要时才进行一些改进。

这是一个使用Interlocked类来锁定j的递增而不会减慢速度的示例。

        int j = 0;
        Parallel.ForEach(Enumerable.Range(0, 5), i =>
        {
                Console.WriteLine(Interlocked.Increment(j));
                GetTotal(j);
        });

答案 1 :(得分:1)

我不知道你在这里要展示的确切内容。增加j绝对是您想要保护的,但GetTotal方法是完全自包含的(即不引用共享状态),因此不需要使用锁保护它。如果你做一个小改动,我认为你会看到相当大的性能提升:

        int j = 0;
        Parallel.ForEach(Enumerable.Range(0, 5), i =>
        {

            lock (SomeLockObject)
            {
                Console.WriteLine(j++);
            }
            GetTotal();
        }

现在只有需要同步的代码才会被锁保护。

你的例子显然是做作的,所以我不能完全相信这会解决你所遇到的真正的问题。

答案 2 :(得分:0)

代码的主要问题是

        Parallel.ForEach(Enumerable.Range(0, 5), i =>{lock(SomeLockObject){/*some work*/}}

不允许并行执行任何操作。锁(SomeLockObject)只允许一个线程在完全击败Parallel.ForEach时进入锁。

尝试并行化任何算法时,首先要弄清楚哪些部分可以按数据隔离。在这种情况下,它只是j ++操作,因为j是唯一有趣的共享内存块。这意味着为了获得某种程度的并行性,您必须将锁定调整为

        Parallel.ForEach(Enumerable.Range(0, 5), i =>
        {
            int tempj;
            lock (SomeLockObject)
            {
                tempj = j++;
            }
            Console.WriteLine(tempj-1);
            GetTotal(tempj);
        });

注意:控制台是线程安全的,请参阅link

这会让你的速度提升很快,但通过完全跳过锁定可以更快地完成任务。

        Parallel.ForEach(Enumerable.Range(0, 5), i =>
        {
            int tempj = Interlocked.Increment(ref j);
            Console.WriteLine(tempj-1);
            GetTotal(tempj);
        });

j ++的问题在于它包含几个操作: 得到j,将j加1,存储j。下面是一个简短的代码块,展示了如何在两个步骤中完成j ++可以产生一些奇怪的结果。

int j = 0;   //intitialize j
int aj = j;  //thread a gets j, aj = 0 , j = 0
aj = aj + 1; //thread a increments aj = 1, j = 0
int bj = j;  //thread b gets j, aj = 1, bj = 0, j = 0
bj = bj + 1; //thread b increments bj = 1, aj = 1, j = 0
j = bj;      //thread b writes j = 1, aj = 1
j = aj;      //thread a writes j = 1

你可以看到j仍然只有1!

Interlocked通过提供原子操作解决了这个问题。 Interlocked.Increment发出一个命令,告诉处理器它应该递增j并返回它,好像它只是一个操作一样,防止了上述竞争条件。

临时变量的使用是必要的,因为其他线程也将使用j,我们希望每个j都调用GetTotal和Console.WriteLine。

最后注意:j ++首先返回j然后递增其值,因此tempj-1

int j = 0;
int pre = j++; //pre = 0, j = 1
j = 0; 
int after = ++j // after = 1, j = 1;