在我管理的新团队中,我们的大部分代码是平台,TCP套接字和http网络代码。所有C ++。其中大多数来自离开团队的其他开发人员。团队中目前的开发人员非常聪明,但从经验来看大多是初级的。
我们最大的问题:多线程并发错误。我们的大多数类库都通过使用某些线程池类而被编写为异步的。类库中的方法通常将长时间运行的任务从一个线程排队到线程池中,然后在另一个线程上调用该类的回调方法。结果,我们有很多涉及错误线程假设的边缘错误。这导致了一些细微的错误,这些错误不仅仅具有关键部分和锁以防止并发问题。
使这些问题更难解决的是,修复尝试通常是不正确的。我发现团队在尝试(或在遗留代码本身中)尝试犯的一些错误包括以下内容:
常见错误#1-通过仅对共享数据进行锁定来解决并发问题,但是忘记了当方法未按预期顺序调用时会发生什么情况。这是一个非常简单的示例:
void Foo::OnHttpRequestComplete(statuscode status)
{
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
因此,现在有了一个错误,其中在OnHttpNetworkRequestComplete发生时可以调用Shutdown。测试人员找到错误,捕获故障转储,并将错误分配给开发人员。他反过来修复了这样的错误。
void Foo::OnHttpRequestComplete(statuscode status)
{
AutoLock lock(m_cs);
m_pBar->DoSomethingImportant(status);
}
void Foo::Shutdown()
{
AutoLock lock(m_cs);
m_pBar->Cleanup();
delete m_pBar;
m_pBar=nullptr;
}
除非您意识到还有一个更微妙的边缘情况,否则上面的修补程序看起来不错。如果在调用OnHttpRequestComplete 之前调用Shutdown,会发生什么情况?我的团队拥有的真实示例更加复杂,并且在代码审查过程中很难发现边缘情况。
常见错误#2-通过盲目退出锁来解决死锁问题,等待另一个线程完成,然后重新进入锁-但是没有处理对象只是被另一个线程更新的情况!
常见错误#3-即使对象是引用计数,关闭序列也会“释放”它的指针。但是忘记等待仍在运行的线程释放它的实例。这样,组件将完全关闭,然后在不希望再有任何调用的状态下在对象上调用伪造或较晚的回调。
还有其他边缘情况,但最重要的是:
即使对于聪明人,多线程编程也很难。
当我发现这些错误时,我会花时间与每个开发人员讨论这些错误,以开发更合适的修复程序。但是我怀疑,由于“正确的”解决方案涉及到大量遗留代码,因此他们常常对如何解决每个问题感到困惑。
我们将很快发货,并且我确定我们正在应用的补丁将在即将发布的版本中保留。之后,我们将有一些时间来改善代码库并在需要时进行重构。我们将没有时间重新编写所有内容。而且大多数代码还不错。但是我希望重构代码,以便可以完全避免线程问题。
我正在考虑的一种方法是这种方法。对于每个重要的平台功能,请使用专用的单线程,将所有事件和网络回调整理到其中。与Windows中使用消息循环的COM公寓线程相似。长阻塞操作仍然可以分派到工作池线程,但是完成回调在组件的线程上调用。组件甚至可能共享同一线程。然后,可以在单个线程世界的假设下编写线程中运行的所有类库。
在走这条路之前,如果还有其他标准技术或设计模式来处理多线程问题,我也非常感兴趣。我必须强调-除了描述互斥量和信号量基础知识的书以外的内容。你怎么看?
我也对其他用于重构过程的方法感兴趣。包括以下任何一项:
有关线程周围设计模式的文献或论文。除了互斥量和信号量介绍之外,还有其他内容。我们也不需要大规模的并行性,只需设计对象模型以正确处理来自其他线程的异步事件的方法即可。
绘制各种组件的线程的方法,以便轻松地研究和发展解决方案。(也就是说,相当于一个UML,用于讨论跨对象和类的线程)
对您的开发团队进行有关多线程代码问题的教育。
你会怎么做?