针对 C++ 静态破坏/构造顺序问题的特定于平台的解决方法
Posted
技术标签:
【中文标题】针对 C++ 静态破坏/构造顺序问题的特定于平台的解决方法【英文标题】:Platform-Specific Workarounds to C++ Static Destruction / Construction Order Problems 【发布时间】:2012-02-03 02:19:27 【问题描述】:我正在使用 Visual Studio 2008 在 Windows XP Pro SP 3 下使用标准(非托管)C++ 进行开发。
我已经围绕 std::cout 创建了一个线程安全的包装器。这个包装器对象是一个插入式替换(即相同的名称),用于替换 #defined 到 cout 的宏。 很多代码使用它。它的行为可能与您预期的差不多:
在构造时,它会创建一个临界区。
在调用operator
在销毁时,它会销毁临界区。
这个包装器存在于静态存储中(它是全局的)。与所有此类对象一样,它在 main() 启动之前构造,并在 main() 退出后销毁。
在同样存在于静态存储中的另一个对象的析构函数中使用我的包装器是有问题的。由于此类对象的构造/销毁顺序是不确定的,我很可能会尝试锁定已被销毁的临界区。我看到的症状是我的程序阻止了锁定尝试(尽管我认为任何事情都可能发生)。
至于解决这个问题的方法......
我在析构函数中什么也做不了;具体来说,我会让关键部分继续存在。 C++ 标准保证 cout 在程序执行期间永远不会死掉,这将是使我的包装器行为相似的最佳尝试。当然,我的包装器在其空的析构函数运行后会“正式”死亡,但它可能(我讨厌这个词)会像它的析构函数运行之前一样有效。在我的平台上,情况似乎确实如此。但是,天哪,这丑陋的、不可携带的,而且以后容易损坏……
我将关键部分(但不是对 cout 的流引用)保存在 pimpl 中。所有通过 pimpl 访问临界区之前都会检查 pimpl 的非空性。碰巧我在析构函数中调用 delete 后忘记将 pimpl 设置为 0。如果我将其设置为 0(无论如何我都应该这样做),在它被破坏后调用我的包装器不会对关键部分做任何事情,但仍会将要打印的数据传递给 cout。在我的平台上,这似乎也有效。再一次,丑陋...
我可以告诉我的队友在 main() 退出后不要使用我的包装器。不幸的是,它的空气动力学性能与坦克差不多。
问题:
* 问题 1 * 对于案例1,如果我不破坏关键部分,操作系统中的关键部分就会发生资源泄漏。在我的程序完全退出后,这种泄漏是否会持续存在?如果没有,案例 1 变得更可行。
* 问题 2 * 对于案例 1 和 2,是否有人知道 在我的特定平台上在空析构函数运行后我是否确实可以安全地继续使用我的包装器?看来我可以,但我想看看是否有人知道我的平台在这种情况下的行为方式......
* 问题 3 * 我的建议显然不完美,但我没有看到真正正确的解决方案。有没有人知道这个问题的正确解决方案?
旁注:当然,如果我尝试在另一个也存在于静态存储中的对象的构造函数中使用包装器,则可能会出现相反的问题。在这种情况下,我可能会尝试锁定尚未创建的临界区。我想使用“第一次使用时构造”习语来解决这个问题,但这需要对使用我的包装器的所有代码进行语法更改。这将需要放弃使用
* 问题 4 * 正如我所说,我的包装器存在于静态存储中(它是全局的)并且它有一个 pimpl(荷尔蒙问题:))。我的印象是静态存储中变量的原始字节在加载时设置为 0(除非在代码中以不同方式初始化)。这意味着我的包装器的 pimpl 在构建包装器之前的值为 0。这是正确的吗?
谢谢你, 戴夫
【问题讨论】:
你不是第一个想到这个问题的。 iostream 管道中已经内置了一个低级锁。添加自己的没有意义。 我没有读完所有这些,但听起来this 可能对你有用。基本上,你可以搞乱静态初始化顺序。 当然,C++ 标准并不保证 cout 是线程安全的,但听起来您(Hans)的意思是说 Visual C++ 的 C++ 特定实现确实使 cout 线程安全.我理解正确吗? 【参考方案1】:首先,我会重新考虑你在做什么。您不能仅通过为每个操作添加锁定来创建线程安全接口。线程安全必须设计到接口中。正如您所建议的那样,减少替换的问题在于,它将使每个单独的操作线程安全(我认为它们已经是安全的),但这并不能避免不必要的交错。
考虑两个执行cout << "Hi" << endl;
的线程,锁定每个操作并不排除“HiHi\n\n”作为输出,并且操作器使事情变得非常复杂,其中一个线程可能会更改下一个值的格式打印,但另一个线程可能会触发下一次写入,在这种情况下,两种格式都会出错。
对于您提出的特定问题,您可以考虑使用标准库对 iostreams 采用的相同方法:
不要将对象创建为全局对象,而是创建一个辅助类型,对类型实例的数量执行引用计数。构造函数将检查对象是否是第一个要创建的类型并初始化线程安全包装器。最后一个要销毁的对象会破坏你的包装器。下一个难题是在标头中创建该类型的全局静态变量,该标头又包含 iostreams 标头。最后一个难题是您的用户应该包含您的标头而不是 iostream。
【讨论】:
以上是关于针对 C++ 静态破坏/构造顺序问题的特定于平台的解决方法的主要内容,如果未能解决你的问题,请参考以下文章
我的服务器设置正确的特定于语言环境的时区是不是重要? [复制]