【问题标题】:Locking in parallel.foreach degrades the performance in c#锁定 parallel.foreach 会降低 c# 中的性能
【发布时间】:2014-03-19 15:11:58
【问题描述】:

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

【问题讨论】:

  • 这个问题中的任何内容都没有让我觉得不正确。锁定将强制竞争相同资源或试图相互协同工作的线程之间进行协调。这是您正在进行的权衡,听起来好像不需要竞争条件,因此需要锁定或其他一些编排机制。让某些东西并行运行可能并不总是让它更快地完成任务。并发总是需要审查信息流、共享资源的使用、所需的输出等。并非所有东西都很好地适合这个模型。
  • 基本上:(a) 并非所有算法都可以轻松并行化,(b) 编程尽可能不锁定。是的,除了简单的演示之外,多线程编程并不完全简单。
  • 我同意 Houldsworth 所说的。您锁定了每个单独的循环,因此实际上您没有获得任何性能。相反,您正在失去性能,因为现在您有六个线程获取和释放锁只是为了增加一个值。
  • 您将货物分成 5 辆卡车,但它们都在同一车道上行驶。
  • 你还没有问过问题。是的,锁定并行块的整个主体是一个坏主意。是的,就目前而言,如果没有某种形式的同步,您的程序将无法按您希望的方式运行。现在你的问题是什么?

标签: c# parallel-processing


【解决方案1】:

我不知道你想在这里展示什么。递增j 绝对是您想要保护的东西,但是GetTotal 方法是完全自包含的(即不引用共享状态),因此不需要用锁来保护它。我认为,如果您进行一些小改动,您会看到相当大的性能提升:

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

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

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

你的例子显然是人为的,所以我不能肯定地说这会解决你遇到的真正的问题。

【讨论】:

    【解决方案2】:

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

    这是来自上面的并发链接:

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

    这些类只会解决线程之间更快通信的问题。看起来您应该只在绝对需要时才锁定。

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

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

    【讨论】:

    • 每当我尝试跨多个并发线程处理集合时,我都会使用ConcurrentBag&lt;T&gt;。每次都像魅力一样工作。
    【解决方案3】:

    代码的主要问题是

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

    不允许并行执行任何操作。 lock(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,我们希望 GetTotal 和 Console.WriteLine 与每个 j 一起调用。

    最后说明:j++ 首先返回 j,然后增加它的值,因此 tempj-1

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-07-08
      • 2016-02-25
      • 1970-01-01
      • 2015-07-01
      • 2018-12-12
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多