【发布时间】:2011-03-31 17:35:43
【问题描述】:
这是我对上一篇关于内存管理问题的帖子的跟进。以下是我知道的问题。
1) 数据竞争(违反原子性和数据损坏)
2) 排序问题
3)误用锁导致死锁
4)heisenbugs
多线程还有其他问题吗?如何解决?
【问题讨论】:
标签: c++ c multithreading operating-system
这是我对上一篇关于内存管理问题的帖子的跟进。以下是我知道的问题。
1) 数据竞争(违反原子性和数据损坏)
2) 排序问题
3)误用锁导致死锁
4)heisenbugs
多线程还有其他问题吗?如何解决?
【问题讨论】:
标签: c++ c multithreading operating-system
Eric 列出的四个问题非常准确。但是调试这些问题很困难。
对于死锁,我一直偏爱“水平锁”。本质上,您为每种类型的锁指定了一个级别编号。然后要求线程获取单调锁。
要做水平锁,你可以这样声明一个结构:
typedef struct {
os_mutex actual_lock;
int level;
my_lock *prev_lock_in_thread;
} my_lock_struct;
static __tls my_lock_struct *last_lock_in_thread;
void my_lock_aquire(int level, *my_lock_struct lock) {
if (last_lock_in_thread != NULL) assert(last_lock_in_thread->level < level)
os_lock_acquire(lock->actual_lock)
lock->level = level
lock->prev_lock_in_thread = last_lock_in_thread
last_lock_in_thread = lock
}
级别锁最酷的地方在于死锁可能导致断言。再加上 FUNC 和 LINE 的一些额外魔法,您就可以确切地知道您的线程做了什么坏事。
对于数据竞争和缺乏同步,目前的情况相当糟糕。有一些静态工具可以尝试识别问题。但误报率很高。
我工作的公司 (http://www.corensic.com) 有一个名为 Jinx 的新产品,它积极寻找可能暴露竞争条件的情况。这是通过使用虚拟化技术来控制各个 CPU 上的线程交错并放大 CPU 之间的通信来实现的。
检查一下。您可能还有几天时间免费下载测试版。
Jinx 特别擅长发现无锁数据结构中的错误。它在寻找其他竞争条件方面也做得很好。很酷的是没有误报。如果您的代码测试接近竞争条件,Jinx 会帮助代码走上错误的道路。但是如果坏路径不存在,您将不会收到错误警告。
【讨论】:
-> 将优先级反转添加到该列表中。
正如另一个海报所逃避的那样,日志文件是很棒的东西。对于死锁,使用LogLock 而不是Lock 可以帮助确定实体何时停止工作。也就是说,一旦你知道你遇到了死锁,日志就会告诉你锁是在何时何地被实例化和释放的。这对于追踪这些事情非常有帮助。
我发现在使用 Actor 模型时遵循相同的消息->确认->确认收到的样式时的竞争条件似乎消失了。也就是说,YMMV。
【讨论】:
注意全局变量,即使
他们是const,尤其是在
C++。只有静态的 POD
初始化的“à la”C在这里很好。
只要是运行时构造函数
发挥作用,非常
小心。 AFAIR 初始化顺序
具有静态链接的变量在
不同的编译单元是
以未定义的顺序调用。可能是
初始化所有的 C++ 类
他们的成员正确并有一个
空函数体,可以
如今,但我曾经有过不好的
也有这方面的经验。
这也是为什么在
POSIX方面pthread_mutex_t多
比sem_t 更容易编程:它
有一个静态初始化器
PTHREAD_MUTEX_INITIALIZER.
保持关键部分尽可能短 可能,有两个原因:它可能 最后效率更高,但是 更重要的是更容易 维护和调试。
决不应该有临界区 比屏幕更长的时间,包括 需要的锁定和解锁 保护它,包括 有帮助的 cmets 和断言 读者了解什么是 正在发生。
开始实施关键部分 非常严格地可能与一个全球 锁定他们所有人,然后放松 之后的约束。
如果有很多记录可能会很困难 线程同时开始写 时间。如果每个线程都执行 合理的工作量 让他们各自写一个文件 拥有,这样他们就不会互锁 彼此。
但请注意,日志记录会更改行为 的代码。当出现错误时,这可能会很糟糕 消失,或在 bug 时有益 看来你不会 已经注意到了。
进行事后分析 你必须有这么一团糟 每行的准确时间戳 这样所有的文件都可以 合并并为您提供连贯的视图 执行。
【讨论】:
让你的线程尽可能简单。
尽量不要使用全局变量。全局常量(永远不会改变的实际常量)很好。当您确实需要使用全局或共享变量时,您需要使用某种类型的互斥锁/锁(信号量、监视器等)来保护它们。
确保您真正了解互斥锁的工作原理。有几种不同的实现可以以不同的方式工作。
尝试组织您的代码,使关键部分(您持有某种类型的锁的地方)尽可能快。请注意,某些功能可能会阻塞(睡眠或等待某事并阻止操作系统允许该线程继续运行一段时间)。不要在持有任何锁时使用它们(除非绝对必要或在调试期间,因为它有时会显示其他错误)。
尝试了解更多线程实际上为您做了什么。盲目地在一个问题上投入更多的线程往往会使事情变得更糟。不同的线程竞争 CPU 和锁。
避免死锁需要计划。尽量避免一次获得多个锁。如果这是不可避免的,请决定您将使用的顺序来获取和释放所有线程的锁。确保您了解死锁的真正含义。
调试多线程或分布式应用程序很困难。如果您可以在单线程环境中进行大部分调试(甚至可能只是强制其他线程休眠),那么您可以在进入多线程调试之前尝试消除以非线程为中心的错误。
始终考虑其他线程可能在做什么。在您的代码中对此进行注释。如果您正在以某种方式做某事,因为您知道当时没有其他线程应该访问某个资源,请写一个大注释说明。
您可能希望在其他函数中包装对互斥锁/解锁的调用,例如:
int my_lock_get(lock_type lock, const char * file, unsigned line, const char * msg) {
thread_id_type me = this_thread();
logf("%u\t%s (%u)\t%s:%u\t%s\t%s\n", time_now(), thread_name(me), me, "get", msg);
lock_get(lock);
logf("%u\t%s (%u)\t%s:%u\t%s\t%s\n", time_now(), thread_name(me), me, "in", msg);
}
还有一个类似的解锁版本。请注意,这里使用的函数和类型都是由任何一个 API 组成的,而不是过度基于任何一个 API。
使用类似的方法,如果出现错误,您可以返回并使用 perl 脚本或类似的东西在您的日志上运行查询,以检查哪里出了问题(例如,匹配锁定和解锁)。
请注意,您的打印或日志记录功能可能也需要对其进行锁定。许多图书馆已经内置了这个,但并非所有图书馆都这样做。这些锁不需要使用 lock_[get|release] 函数的打印版本,否则您将拥有无限递归。
【讨论】:
不幸的是,没有好的药丸可以帮助自动解决大多数/所有线程问题。即使是在单线程代码片段上运行良好的单元测试也可能永远无法检测到极其微妙的竞争条件。
将线程交互数据封装在对象中会有所帮助。对象的接口/范围越小,就越容易检测到审查中的错误(可能还有测试,但在测试用例中检测竞争条件可能会很痛苦)。通过保持一个可以使用的简单接口,使用该接口的客户端默认也是正确的。通过从许多较小的部分(实际上只有少数部分真正进行线程交互)构建一个更大的系统,您可以在很大程度上避免线程错误。
【讨论】:
如何解决【多线程问题】?
“调试”MT 应用程序的一个好方法是通过日志记录。具有广泛过滤选项的良好日志库使其更容易。当然,日志记录本身会影响时间,因此您仍然可以拥有“heisenbugs”,但与实际闯入调试器时相比,这种可能性要小得多。
为此做好准备和计划。从一开始就在您的应用程序中包含一个好的日志记录工具。
【讨论】:
最常见的四个问题是
1-死锁
2-活锁
3 场比赛条件
4-饥饿
【讨论】: