使用线程局部存储将具有全局变量的单线程遗留代码转换为多线程代码

Posted

技术标签:

【中文标题】使用线程局部存储将具有全局变量的单线程遗留代码转换为多线程代码【英文标题】:Converting single-threaded legacy code with global variables to multithreaded code using thread-local storage 【发布时间】:2010-09-13 14:49:13 【问题描述】:

我有一个遗留 C/C++ 代码的代码库,其中包含许多访问全局静态变量的函数,因此不是线程安全的。我正在寻找有关如何转换此代码以使其线程安全的建议。我突然想到,一种方法是将静态变量转换为线程局部变量,或者将它们存储在线程局部存储中。这样做的好处是我不必重写大量使用函数向它们传递额外上下文的代码,只需重写线程不安全函数本身。但是在研究这一点时,我没有找到很多关于这是一个好主意还是坏主意的建议。我有一些具体的担忧是

访问基于 TLS 的数据会明显变慢吗? 我只是继续陷入使用全局变量的陷阱,因为“全局变量不好”,还是 TLS 抵消了 global-variables-are-bad 论点?

任何其他想法也将不胜感激。

【问题讨论】:

我应该提到这是跨平台代码:Windows、Linux 和 Mac OS X。 【参考方案1】:

首先,最好确定哪些全局变量以可变方式访问,哪些不是。可能有几种情况,变量一旦设置就没有实际修改,只是这些变量的初始化现在会给非确定性程序带来问题。这些变量,初始化顺序很重要,我会放在他们自己的类中,可以传播到需要它们的每个单独的线程。

在那之后,我相信您的问题只会更加复杂。我一直在你所在的地方和我的同情心。

【讨论】:

【参考方案2】:

使用 TLS 的问题在于您将线程关联性强加给客户端,他们必须意识到这一点并编写代码来相应地处理它。因此,如果他们有一个工作线程来完成繁重的工作,那么他们将不得不将对您的库的所有调用编组到该工作线程上,即使对于简单的 getter 和 setter 也是如此。或者,如果其他一些组件在线程池线程上调用它们的组件(例如),那么它们将不得不将该调用编组到它们的工作线程上。这不是世界上最糟糕的事情,但它可能非常不方便。我绝对更喜欢句柄/上下文模式(我维护了一个使用 TLS 的库,它有时处理线程关联性很麻烦)。

【讨论】:

【参考方案3】:

嗯,从那里开始很容易。您可以对线程本地进行条件编译。一些编译器,例如Visual Studio 有自己的 thread_local 存储,您可以直接放入。

【讨论】:

【参考方案4】:

就正确性而言,只要线程没有其他可能导致冲突的含义,我认为这没有任何原因。您需要确保您捕获了所有全局变量并转换了它们。

确定它是否明显变慢的最佳方法是分析代码的转换部分并查看。

话虽如此,我不知道这是最好的解决方案。全局变量就是这样,无论它们被称为什么,它们使跟踪程序状态和调试成为一场噩梦。我会认真地建议仔细查看代码,并一次将一段代码重构为不依赖全局变量的线程安全代码。你会更容易调试,未来的维护者(或你自己)会在几年后感谢你。

【讨论】:

【参考方案5】:

在不了解您计划转换的每个变量的语义的情况下,简单地从全局到 TLS 的转换很可能是一个巨大的非生产性浪费时间。其中一些可能需要保持全局并以线程安全的方式处理 - 其他可能非常安全地用作 TLS。还有更多可能像 TLS 一样是线程安全的,但随着线程数量的增加(例如,在以前的单客户端单线程套接字服务器中,每个客户端/线程一个套接字),会导致糟糕的不可扩展设计。

顺便说一句,您选择的平台/编译器是什么?

【讨论】:

【参考方案6】:

与其将所有变量存储在线程本地存储中,我认为将它们放入一个类中会更好;如果原始代码主要是 C,则可以将 C 代码包装在一个类中,然后该类可以将“全局”变量作为字段。在不同线程中运行的类的不同实例自然会看到这些变量的不同版本。

【讨论】:

以上是关于使用线程局部存储将具有全局变量的单线程遗留代码转换为多线程代码的主要内容,如果未能解决你的问题,请参考以下文章

线程局部存储的Win32实现

线程局部存储区

线程局部存储TLS

线程局部存储主要用来干啥的?

手写自己的ThreadLocal(线程局部变量)

线程本地存储及实现原理