【问题标题】:How can I become thread safe?我怎样才能成为线程安全的?
【发布时间】:2018-03-20 10:02:13
【问题描述】:

我正在研究一个应用程序,我将在其中处理多个集成并需要它们在线程中运行。我需要线程“向母舰(又名主循环)报告”。 sn-p:

class App
{
    public delegate void StopHandler();
    public event StopHandler OnStop;

    private bool keepAlive = true;

    public App()
    {
        OnStop += (() => { keepAlive = false; });

        new Thread(() => CheckForStop()).Start();
        new Thread(() => Update()).Start();

        while (keepAlive) { }
    }

    private void CheckForStop()
    {
        while (keepAlive) if (Console.ReadKey().Key.Equals(ConsoleKey.Enter)) OnStop();
    }

    private void Update()
    {
        int counter = 0;

        while (keepAlive)
        {
            counter++;

            Console.WriteLine(string.Format("[{0}] Update #{1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), counter));

            Thread.Sleep(3000);
        }
    }
}

这里的问题是变量keepAlive。通过使用它不是线程安全的。我的问题是如何使它成为线程安全的。

如果Update 使用while(true) 而不是keepAlive 并且事件OnStop 中止线程,它会变得安全吗?

【问题讨论】:

  • bool 不是同步对象,AutoResetEvent 是。 .NET 有方便的线程包装器,可帮助您利用 CancellationToken。退后一步,想知道那个线程是如何有用的,在那个 while 循环中无休止地旋转,并且烧掉 100% 的核心,什么也没做。它还不如调用 Update()。

标签: c# multithreading thread-safety


【解决方案1】:

使用对象和lock it

class App
{
    public delegate void StopHandler();
    public event StopHandler OnStop;
    private object keepAliveLock = new object();
    private bool keepAlive = true;

....
    private void Update()
    {
        int counter = 0;

        while (true)
        {
            lock(keepAliveLock)
            {
                 if(!keepAlive) 
                      break;
            }
            counter++;

            Console.WriteLine(string.Format("[{0}] Update #{1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), counter));

            Thread.Sleep(3000);
        }
    }
}

请注意,每次对 keepAlive 的访问都需要被锁定(由 lock 语句包围)。处理死锁情况。

【讨论】:

  • 感谢您的意见。我对锁不太了解,但是通过将其锁定在一个线程中,我需要在另一个线程中测试锁(如果可用)?
  • 您可以点击lock it word后面的链接进行详细说明。锁很简单,你不需要检查额外的监视器或其他任何东西,你也可以。但是如果对象被锁定,锁会一直等到锁被释放。如果它没有被锁定,它将被锁锁定。由于这种行为,您需要处理死锁情况。
  • 注意:对变量的每次访问都使用锁可能会降低性能。我认为@MichaelRandall 提供了一个更好的解决方案来确保您的 bool 是线程安全的。然而。检查我的答案以获得更好的解决方案。
  • volatile 在写入时不会使 bool 线程安全。锁定是否会降低性能取决于发生锁定情况的数量。当然,从阅读代码的角度来看,您的解决方案也很好,而且更简洁。但对于小情况,我总是更喜欢锁定,因为开销较小。
【解决方案2】:

就您的问题和示例代码的具体措辞(关于keepAlive)而言,只需使用volatile(对于bool 的简单读取访问权限)。至于是否有其他方法可以改善您的代码,这是另一回事。

private volatile bool keepAlive = true;

对于简单类型bool之类的访问,那么volatile就足够了——这样可以确保线程不会缓存值。

bool 的读写是原子的,如C# Language Spec 中所示。

以下数据类型的读写是原子的:bool、char、 byte、sbyte、short、ushort、uint、int、float 和引用类型。

此外

CLI 规范的第 I 部分第 12.6.6 节指出:“符合标准的 CLI 应保证对正确对齐的内存的读写访问 不大于本机字大小的位置是原子的,当所有 对某个位置的写访问大小相同。

也值得一看volatile (C# Reference)

volatile 关键字表示一个字段可能被 同时执行的多个线程。是的字段 声明的 volatile 不受编译器优化的影响 假设由单个线程访问。这确保了最 字段中始终存在最新值。

【讨论】:

    【解决方案3】:

    我会亲自将其从等待 bool 变为 false 改为使用 ManualResetEvent。还使用 System.Timers.Timer 进行更新而不是循环:

    private ManualResetEvent WaitForExit;
    private ManualResetEvent WaitForStop;
    
    public App()
    {
        WaitForExit = new ManualResetEvent(false);
        WaitForStop = new ManualResetEvent(false);
    
        new Thread(() => CheckForStop()).Start();
        new Thread(() => Update()).Start();
    
        WaitForExit.WaitOne();
    }
    
    private void CheckForStop()
    {
        while (true) 
            if (Console.ReadKey().Key.Equals(ConsoleKey.Enter)) 
            {
                WaitForStop.Set();
                break;
            }
    }
    
    private void Update()
    {
        int counter = 0;
    
        Timer timer = new Timer(3000);
        timer.Elapsed += Timer_Elapsed;
        timer.AutoReset = true;
        timer.Start();
    
        WaitForStop.WaitOne();
    
        timer.Stop();
    
        WaitForExit.Set();
    }
    
    private int counter = 1;
    private void Timer_Elapsed(object sender, ElapsedEventArgs e)
    {
        Console.WriteLine(string.Format("[{0}] Update #{1}", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), counter++));
    }
    

    【讨论】:

      【解决方案4】:

      正如您自己所注意到的,您在线程之间共享的可变keepAlive 变量会导致您头疼。我的建议是删除它。更一般地说:

      停止在线程之间共享可变状态

      基本上所有多线程问题都来自共享可变状态的线程。

      在执行打印的对象上将keepAlive 设为私有实例变量。让那个类实例化它自己的线程,并让所有发送到对象的消息都放在一个 ConcurrentQueue 中:

      class Updater
      {
          // All messages sent to this object are stored in this concurrent queue
          private ConcurrentQueue<Action> _Actions = new ConcurrentQueue<Action>();
      
          private Task _Task;
          private bool _Running;
          private int _Counter = 0;
      
          // This is the constructor. It initializes the first element in the action queue,
          // and then starts the thread via the private Run method:
          public Updater()
          {
              _Running = true;
              _Actions.Enqueue(Print);
              Run();
          }
      
          private void Run()
          {
              _Task = Task.Factory.StartNew(() =>
              {
                  // The while loop below quits when the private _Running flag
                  // or if the queue of actions runs out.
                  Action action;
                  while (_Running && _Actions.TryDequeue(out action))
                  {
                      action();
                  }
              });
          }
      
          // Stop places an action on the event queue, so that when the Updater
          // gets to this action, the private flag is set.
          public void Stop()
          {
              _Actions.Enqueue(() => _Running = false);
          }
      
          // You can wait for the actor to exit gracefully...
          public void Wait()
          {
              if (_Running)
                  _Task.Wait();
          }
      
          // Here's where the printing will happen. Notice that this method
          // puts itself unto the queue after the Sleep method returns.
          private void Print()
          {
              _Counter++;
      
              Console.WriteLine(string.Format("[{0}] Update #{1}",
                  DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"), _Counter));
      
              Thread.Sleep(1000);
      
              // Enqueue a new Print action so that the thread doesn't terminate
              if (_Running)
                  _Actions.Enqueue(Print);
          }
      }
      

      现在我们只需要一种方法来停止线程:

      class Stopper
      {
          private readonly Updater _Updater;
          private Task _Task;
      
          public Stopper(Updater updater)
          {
              _Updater = updater;
              Run();
          }
      
          // Here's where we start yet another thread to listen to the console:
          private void Run()
          {
              // Start a new thread
              _Task = Task.Factory.StartNew(() =>
              {
                  while (true)
                  {
                      if (Console.ReadKey().Key.Equals(ConsoleKey.Enter))
                      {
                          _Updater.Stop();
                          return;
                      }
                  }
              });
          }
      
          // This is the only public method!
          // It waits for the user to press enter in the console.
          public void Wait()
          {
              _Task.Wait();
          }
      }
      

      将它们粘合在一起

      我们现在真正需要的是一个main 方法:

      class App
      {
          public static void Main(string[] args)
          {
              // Instantiate actors
              Updater updater = new Updater();
              Stopper stopper = new Stopper(updater);
      
              // Wait for the actors to expire
              updater.Wait();
              stopper.Wait();
      
              Console.WriteLine("Graceful exit");
          }
      }
      

      延伸阅读:

      上述为线程封装可变状态的方法称为Actor Model

      前提是,所有线程都被自己的类封装,只有那个类才能与线程交互。在上面的例子中,这是通过将Actions 放在一个并发队列中然后一个一个地执行来完成的。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-04-15
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-05-22
        • 2012-10-02
        • 1970-01-01
        相关资源
        最近更新 更多