Thread.Yield是一种标准方法,用于确定您的多线程应用程序C#中是否存在错误

时间:2016-03-25 13:44:26

标签: c# multithreading concurrency thread-safety

我开始阅读http://www.albahari.com/threading/

上发布的信息

提交人说:

  

Sleep(0)或Yield在生产代码中偶尔会用于高级性能调整。它也是一个很好的诊断工具,可以帮助发现线程安全问题:如果在代码中的任何地方插入Thread.Yield()会导致或破坏程序,那么几乎肯定会有错误。

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

  

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

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

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

3 个答案:

答案 0 :(得分:2)

这是一个很好的建议,但我通常使用Sleep(1)。

使并发错误难以修复的一个原因是它们很难重现 - 大多数问题都会在您运气不好时出现,操作系统会在最糟糕的时间暂停您的线程。

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

答案 1 :(得分:1)

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

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

  

Sleep(0)或Yield在生产代码中偶尔会用于高级性能调整。它也是一个很好的诊断工具,可以帮助发现线程安全问题:如果在代码中的任何地方插入Thread.Yield()会破坏程序,你几乎肯定会有错误。

这里的关键是线程将处理器的权利放弃到另一个线程。

不要认为这是一个标准,但是如你所说的那样,它肯定是有用的,可以说....

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

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

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

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

  3. 结果可能会因您执行的环境而异,示例您的开发框,其中包含 i5和8 GB的内存可能正常工作,但当您将程序上传到生产环境时, 16核和128 GB内存,您很可能会得到不同的结果

  4. 将所有东西扔到墙上,看看有什么粘贴

    这不是一个很好的答案,但说实话是我最终调试很多时间的方式,你可以尝试这样的技巧:

    编译为发布代码,优化可能会改变代码的执行顺序,导致Debug构建的说明与发布版本不同

    1. 多次运行多线程代码(取决于1,000到1,000,000次之间的强度)
    2. 只打几次
    3. 只打电话一次(可能看起来很愚蠢,但这次与我搞砸了几次)
    4. 长时间多次通话(每小时拨打一天电话)
    5. 这不是万无一失的,但认为这是一种很好的方法,可以确保您在向野外发布内容之前捕获尽可能多的问题。

答案 2 :(得分:0)

使用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