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

本文关键字:性能 锁定 Foreach 并行 | 更新日期: 2023-09-27 17:51:01

我有一个示例程序,我试图复制我的实际应用程序场景。是否有一种方法只锁定一次,而不是每个循环,这实际上降低了并行循环的性能。如果我删除锁,性能是预期的,但我在竞争条件下运行。我的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;
 }

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

代码的主要问题是

        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);
        });

注意:控制台是线程安全的,见链接。

这将大大提高你的速度,但还有一种更快的方法,那就是完全跳过锁定。

        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通过提供原子操作解决了这个问题。联锁。Increment发出一个命令,告诉处理器它应该增加j,并将其作为一个操作返回,以防止上述竞争条件。

使用临时变量是必要的,因为其他线程也将使用j,我们需要GetTotal和Console。

每个j.

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

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

我不知道您到底想要演示什么。增加j肯定是您想要保护的东西,但是GetTotal方法是完全自包含的(即不引用共享状态),因此不需要用锁来保护它。我认为如果你做一个小小的改变,你会看到性能有很大的提高:

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

现在只有需要同步的代码受到锁的保护。

你的例子显然是人为的,所以我不能自信地说这将解决真正的问题。

你可以看看c#的并发库。它们允许更快的线程读/写共享数据。

来自上面的并发链接:

一些并发集合类型使用轻量级同步机制,如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);
        });