【问题标题】:Unordered threads problem无序线程问题
【发布时间】:2010-08-12 07:53:16
【问题描述】:

我在here 中询问了有关锁定的问题,人们回答说我的锁定实现没有问题。但我发现了问题。这是相同的锁实现,我得到了奇怪的结果。我希望看到数字从 1 开始,但它从 5 开始。示例如下。

class Program
{
    static object locker = new object();
    static void Main(string[] args)
    {
        for (int j = 0; j < 100; j++)
        {
            (new Thread(new ParameterizedThreadStart(dostuff))).Start(j);
        }
        Console.ReadKey();
    }
    static void dostuff(dynamic input)
    {
        lock (locker)
        {
            Console.WriteLine(input);
        }
    }
}

【问题讨论】:

    标签: c# .net multithreading locking


    【解决方案1】:

    代码很好。但是你不能保证线程的执行顺序。当我运行我得到的代码时:

    0 1 3 5 2 4 6 10 9 11 7 12 8 等等

    如果您需要按指定顺序运行线程,您可以考虑改用ThreadPool.QueueUserWorkItem。

    class Program
    {
          static object locker = new object();
          static EventWaitHandle clearCount 
              =new EventWaitHandle(false, EventResetMode.ManualReset);
      static void Main(string[] args)
      {
        for (int j = 0; j < 100; j++)
        {
           ThreadPool.QueueUserWorkItem(dostuff, j);
        }
        clearCount.WaitOne();
      }
      static void dostuff(dynamic input)
      {
        lock (locker)
        {
          Console.WriteLine(input);
              if (input == 99) clearCount.Set();
         }
       }
    }
    

    【讨论】:

    • 为什么不保证订购的结果?如果期望有序列表,您确定不需要线程吗?
    • 因为当您启动一个新线程时,您并不真正知道该特定线程何时完成创建以及何时执行(由运行时处理)。此外,如果一个线程由于某种原因需要更长的时间执行,那么其他线程将在此之前结束,结果将是“随机的”。 ThreadPool 也是如此。但是使用 ThreadPool,您可以知道线程开始的顺序,但不一定是它们完成的顺序。
    • 好吧,但是我的方法不适合 WaitCallback 方法,所以有没有办法将 ThreadPool.QueueUserWorkItem 用于不同类型的委托?
    • @Freshblood:当最后一个线程完成执行时,您需要一些信息来告诉您。如果您事先知道您正在使用多少线程,您可以使用计数器和 (Interlocked.Increment),然后检查您是否已达到目标,然后向 EventHandle 发出信号。很像我已经写的代码。为什么不能使用等待句柄?
    • @ Mikael Svenson - 在这个例子中可以使用,但在我的特殊代码中是不可能的。我不应该等待任何线程。所有迭代等待来自用户的输入然后在另一个线程上处理它,所以如果我等待线程结束以开始下一个,我在那一刻无法监听用户输入。
    【解决方案2】:

    在你放置它的地方放置一个锁是没有意义的,因为你没有锁定更改多个线程共享的值的代码。您锁定的代码部分根本不会更改任何变量。

    数字乱序的原因是线程不能保证以任何特定顺序启动,除非您执行@Mikael Svenson 建议的操作。

    以共享变量为例,如果你使用这段代码:

    class Program
    {
        static object locker = new object();
        static int count=0;
    
        static void Main(string[] args)
        {
            for (int j = 0; j < 100; j++)
            {
                (new Thread(new ParameterizedThreadStart(dostuff))).Start(j);
            }
            Console.ReadKey();
        }
    
        static void dostuff(object Id)
        {
            lock (locker)
            {
                count++;
                Console.WriteLine("Thread {0}: Count is {1}", Id, count);
            }
        }
    }
    

    您可能会看到线程编号不是按顺序排列的,但计数是按顺序排列的。如果你去掉 lock 语句,计数也不会按顺序排列。

    【讨论】:

    • 如果你解除锁定,最终计数可能会小于 100。
    【解决方案3】:

    你有几个问题和错误的假设。

    • 不建议以这种方式创建 100 个线程。
    • 线程不会按照它们启动的顺序执行。
    • 将锁放在您拥有的地方将有效地序列化线程的执行,立即消除您希望通过使用线程获得的任何优势。

    使用的最佳方法是将您的问题划分为单独的独立块,这些块可以同时使用尽可能少的线程同步进行计算。这些分区应该在少量且相当静态的线程上执行。您可以使用 ThreadPool、Parallel 或 Task 类来执行此操作。

    我使用Parallel.For 方法包含了一个示例模式。为了使示例易于理解,假设您有一个要克隆的对象列表并将其放入单独的列表中。让我们假设克隆操作很昂贵,并且您希望并行克隆许多对象。这是您将如何做到的。注意lock 关键字的位置和限制使用。

    public static void Main()
    {
      List<ICloneable> original = GetCloneableObjects();
      List<ICloneable> copies = new List<ICloneable>();
      Parallel.For(0, 100,
        i =>
        {
          ICloneable cloneable = original[i];
          ICloneable copy = cloneable.Clone();
          lock (copies)
          {
            copies.Add(copy);
          }
        });
    }
    

    【讨论】:

    • 我决定再问一个问题。
    • @Freshblood,链接到 another 问题会更友好
    • @Brian Gideon,包含一个无法编译的示例代码有什么意义,并且非常不清楚如何运行或理解它?
    • @Fulproof:简洁是省略一些代码的要点。而且,是的,它确实需要读者的一些努力才能理解它。我真的很想把重点放在一个替代方案上,即 1) 不需要创建 100 个线程并防止锁定的序列化效果,这是 OP 原始问题中的问题。保持示例简短通常可以让人们一眼就看到模式的重要部分,而不会被细节分散注意力。
    猜你喜欢
    • 2017-05-05
    • 2012-11-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-15
    相关资源
    最近更新 更多