【问题标题】:Is Thread.Yield a standard way to determine if you have a bug in your multi threaded application C#Thread.Yield 是确定多线程应用程序 C# 中是否存在错误的标准方法吗
【发布时间】:2016-03-25 13:44:25
【问题描述】:

我开始阅读http://www.albahari.com/threading/发布的信息

作者表示:

Sleep(0) 或 Yield 有时在生产代码中用于高级性能调整。它也是帮助发现线程安全问题的出色诊断工具:如果在代码中的任何位置插入 Thread.Yield() 会导致或破坏程序,则几乎可以肯定存在错误。

根据MSDN on Thread.Yield()Thread.Yield()定义如下:

使调用线程将执行让给准备在当前处理器上运行的另一个线程。操作系统选择要让步的线程。

对我来说,这描述了半数软件开发中的竞争条件无法解决。

这是线程中的标准调试实践吗?

【问题讨论】:

  • 我从来没有听说过...
  • 我也没听说过。我不是线程新手,但我不是大师。
  • 这是个好主意。一个问题是您需要知道或猜测在哪里插入此调用。
  • 好像有人在宣传shotgun debugging...
  • Thread.Yield() 的强大之处在于它改变了时间。线程竞争错误很难诊断,因为它们非常依赖于时间。这些错误隐藏起来是因为在实践中程序执行太容易预测了。直到它不是,每周只发生一次。只是不够频繁地诊断错误。测试应用程序线程错误的工具会故意将随机延迟注入线程,以最大限度地提高触发故障模式的几率。显然,一种比自己使用 Thread.Yield() 更有效的方法。

标签: c# multithreading concurrency thread-safety


【解决方案1】:

这是个好建议,但我通常使用 Sleep(1) 代替。

使并发错误如此难以修复的原因之一是它们难以重现 - 大多数问题在您不走运并且操作系统在最糟糕的时间暂停您的线程时表现出来。

在调试这样的问题时,您通常需要测试一个假设,例如“可能当我的线程在这里暂停时发生,并且......”。此时您可以插入 yield 或 sleep,这将暂停您的线程并大大增加重现错误的可能性。

【讨论】:

  • Sleep(1) 意味着您的线程将始终停止执行并放弃操作系统分配的其余处理时间。 Yield 仅在同一核心上的另一个线程当前正在等待调度时切换执行,否则将继续执行您的线程。所以我想说,如果你真的想让你的线程在那里停止执行以重现一种情况,睡眠是可取的。
【解决方案2】:

使用Thread.Sleep()Thread.Yield() 不会解决您的错误,但它们可能会在某些情况下隐藏它们。虽然这似乎是一件好事 - 阻止错误弹出比让它们杀死你的程序更好 - 但现实是你并没有解决根本问题。

是的,消除多线程程序中的错误非常困难。这是您真正必须了解线程如何交互的地方,以及当线程同时在不同的 CPU 内核上运行时会发生什么,等等。如果没有这种理解,您可能永远不会在程序逻辑中找到导致问题的错误第一名。

在编写多线程程序时,您必须确保对共享数据的每个操作都是原子的。当您对共享值执行此操作时,即使是简单的增量操作也会成为问题,这就是我们使用Interlocked.Increment() 方法的原因。对于其他所有内容,都有锁等来帮助您管理线程交互。

检查您的线程与共享数据的每次交互,并确保在您使用数据时对数据进行锁定。例如,假设您正在为一组工作线程排队作业:

public class WorkerThread
{
    public static readonly Queue<Job> jobs = new Queue<Job>();

    public void ThreadFunc()
    {
        while (true)
        {
            if (jobs.Count > 0)
            {
                Job myJob = jobs.Dequeue()
                // do something with the job...
            }
            else
                Thread.Yield();
        }
    }
}

看起来很简单,在您检查作业然后去获取它之间只需要几个周期。与此同时,另一个线程突然出现并从你下面抓住了等待的工作。您可以通过几种方式解决这个问题,但最简单的可能是使用来自System.Collections.Concurrent 的队列的线程安全版本:

public class WorkerThread
{
    public static readonly ConcurrentQueue<Job> jobs = new ConcurrentQueue<Job>();

    public void ThreadFunc()
    {
        Job myJob;
        while (true)
        {
            if (jobs.TryDequeue(out myJob))
            {
                // do something with the job...
            }
            else
                Thread.Yield();
        }
    }
}

如果您没有线程安全版本,您将不得不依靠锁定或其他机制来保护您对共享数据的访问。上述基于锁的解决方案可能如下所示:

public class WorkerThread
{
    private static object _jobs_lock = new object();
    private static readonly Queue<Job> _jobs = new Queue<Job>();

    public void ThreadFunc()
    {
        Job myJob;
        while (true)
        {
            if ((myJob = NextJob()) != null)
            {
                // do something with the job...
            }
            else
                Thread.Yield();
        }
    }

    public void AddJob(Job newJob)
    {
        lock(_jobs_lock)
            _jobs.Enqueue(newJob);
    }

    private Job NextJob()
    {
        lock (_jobs_lock)
        {
            if (_jobs.Count > 0)
                return _jobs.Dequeue();
        }
        return null;
    }
}

这两者中的任何一个都将确保在测试是否有作业和实际从队列中检索作业之间不会修改集合。确保尽可能快地释放锁,否则您将遇到锁争用问题,这可能更难解决。永远不要将锁留在原地超过绝对必要的时间来完成工作 - 在这种情况下,测试并从队列中检索一个项目。对您的所有共享资源执行此操作,您将不再有任何竞争条件。

当然还有很多其他线程问题,包括本质上是线程不安全的方法。使您的线程尽可能独立并锁定对共享资源的访问,您应该能够避免大多数讨厌的heisenbugs

【讨论】:

    【解决方案3】:

    Thread.Yield 将帮助您找到一些错误

    您实际上在这里回答了您自己的问题:

    Sleep(0) 或 Yield 有时在生产代码中用于高级性能调整。它也是帮助发现线程安全问题的出色诊断工具:如果在代码中的任何位置插入 Thread.Yield() 会造成或破坏程序,那么您几乎肯定会遇到错误。

    这里的关键是线程将处理器的权利放弃给不同的线程。

    不要认为它是一个标准,但它绝对是有用的,正如你在引用中提到的那样......

    多线程调试很困难,没有真正的标准方法

    调试多线程代码真的很难,有几个原因

    1. 用于观察程序的工具会修改程序的执行方式,这意味着您并没有真正调试将在生产中执行的内容。

    2. 添加让您暂时观察应用程序状态的方法也需要同步(即Console.WriteLine,这意味着与实际代码不同的代码)

    3. 结果可能会因您执行的环境而有很大差异,示例您的 开发盒具有 i5 和 8 gigs 的 RAM strong> 可能会正常工作,但是当您将程序上传到具有 16 核和 128 GB RAM生产环境时,您很可能会得到不同的结果

      李>

    把所有东西都扔到墙上,看看有什么能粘住

    这不是一个很好的答案,但老实说,这是我最终调试大量时间的方式,您可以尝试以下技术:

    编译为 Release 代码,优化可能会改变代码的执行顺序并导致 Debug 构建的指令与 Release 构建不同

    1. 多次运行多线程代码(取决于 1,000 到 1,000,000 次之间的强度)
    2. 只打了几次电话
    3. 只打一次电话(可能看起来很愚蠢,但这让我很困惑)
    4. 长时间多次通话(一天一小时)

    这不是万无一失的,但认为这是确保在将某些东西发布到野外之前尽可能多地捕获问题的好方法。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-04-19
      • 2017-04-06
      • 1970-01-01
      • 2018-03-06
      • 1970-01-01
      • 1970-01-01
      • 2017-03-02
      • 2016-11-17
      相关资源
      最近更新 更多