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