【问题标题】:C# Threading/Lock confusionC# 线程/锁混淆
【发布时间】:2009-08-14 23:50:32
【问题描述】:

我有以下代码:

var items = new List<string> {"1", "2", "3"}; // 200 items
foreach(var item in items) {
  ThreadPool.QueueUserWorkItem((DoWork), item);
}

private void DoWork(object obj)
{
  lock(this)
  {
    using(var sw = File.AppendText(@"C:\somepath.txt")
    {
      sw.WriteLine(obj);
    }
  }
}

由于线程,出于某种原因,我得到了写入文件的 200 个项目的随机数。 60 或 127 或有时只有 3。如果我删除 ThreadPool 并仅在原始 foreach 循环内写入,则所有 200 项都已成功写入。

不知道为什么会这样?

感谢您的帮助。

【问题讨论】:

    标签: c# multithreading locking


    【解决方案1】:

    MSDN documentation on ThreadPool 的以下注释说明了一切:

    托管线程池中的线程是后台线程。也就是说,它们的IsBackground 属性为真。 这意味着在所有前台线程都退出后,ThreadPool 线程将不会继续运行应用程序

    您的应用程序会在您的线程完成运行之前退出(到达Main 的末尾)。

    【讨论】:

    • 解决此问题的一种方法是使用 Interlocked.Increment 来跟踪调用 DoWork 的次数,并在达到幻数时设置自动重置事件。主线程可以等待该事件而不是关闭。
    • 难道没有办法告诉主线程等待所有线程池线程完成工作吗?
    • Steven 的评论解释了如何做到这一点。
    【解决方案2】:

    这是我所暗示的一个简单版本。它使用单个事件并且不轮询或旋转,并且它的编写是为了可重用以及同时允许多个工作集。 lambda 表达式可以被分解,如果这样更方便调试的话。

    class Program
    {
        static void Main(string[] args)
        {
            var items = new string[] { "1", "2", "3", "300" };
            using (var outfile = File.AppendText("file.txt"))
            {
                using (var ws = new WorkSet<string>(x =>
                        { lock (outfile) outfile.WriteLine(x); }))
                    foreach (var item in items)
                        ws.Process(item);
            }
        }
    
        public class WorkSet<T> : IDisposable
        {
            #region Interface
    
            public WorkSet(Action<T> action)
            { _action = action; }
    
            public void Process(T item)
            {
                Interlocked.Increment(ref _workItems);
                ThreadPool.QueueUserWorkItem(o =>
                        { try { _action((T)o); } finally { Done(); } }, item);
            }
    
            #endregion
            #region Advanced
            public bool Done()
            {
                if (Interlocked.Decrement(ref _workItems) != 0)
                    return false;
    
                _finished.Set();
                return true;
            }
    
            public ManualResetEvent Finished
            { get { return _finished; } }
    
            #endregion
            #region IDisposable
    
            public void Dispose()
            {
                Done();
                _finished.WaitOne();
            }
    
            #endregion
            #region Fields
    
            readonly Action<T> _action;
            readonly ManualResetEvent _finished = new ManualResetEvent(false);
            int _workItems = 1;
    
            #endregion
        }
    }
    

    【讨论】:

      【解决方案3】:

      又短又甜怎么样?

          static int wrkThreads = 0;
          static readonly EventWaitHandle exit = new ManualResetEvent(false);
          static readonly object syncLock = new object();
      
          static void Main( string[] items )
          {
              wrkThreads = items.Length;
      
              foreach ( var item in items )
                  ThreadPool.QueueUserWorkItem(( DoWork ), item);
      
              exit.WaitOne();
          }
      
          static void DoWork( object obj )
          {
              lock ( syncLock ) {
                  /* Do your file work here */
              }
              if ( Interlocked.Decrement(ref wrkThreads) == 0 )
                  exit.Set();
          }
      

      【讨论】:

      • 对,这是我发布的算法,只是剥离了所有可重用性。它还要求您提前知道会有多少工作项,而我提供的解决方案却没有。但是,它确实反驳了 Spencer 的建议,即该算法本身很长。
      • 就是这样。我发布它的原因是考虑到这个问题,OP 在多线程方面还不是很大,这个简短的版本解决了他的问题,同时清楚地展示了一种基本的线程同步方法。
      • 我理解你的动机,但我并不喜欢你使用通用对象而不是 FileStream 本身来锁定。
      • 我并不喜欢你使用本地范围的对象来锁定任何一个。只要能带来可预测的结果,这无关紧要。
      • 不是本地的; FileStream 通过闭包限定为匿名委托。由于内部 using 块直到所有线程都结束才结束,因此外部 using 块中的 FileStream 保证始终处于活动状态。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多