【问题标题】:Destruction of pthread mutexes and deinitialization order in C++C++ 中 pthread 互斥体的破坏和去初始化顺序
【发布时间】:2020-03-23 13:23:22
【问题描述】:

pthread 互斥锁似乎相当普遍,它们打算一直存在到程序的生命周期结束。通常这些是使用PTHREAD_MUTEX_INITIALIZER 创建的。

这是一个简短但完整的代码示例,展示了我所指的内容:

#include <pthread.h>

#include <iostream>

void log(char const * const message) {
    static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

    pthread_mutex_lock(&mutex);
    std::cout << message << std::endl;
    pthread_mutex_unlock(&mutex);
}

struct Object final {
    Object() { log("Object::Object()"); }
    ~Object() { log("Object::~Object()"); }
};

Object const object;

int main(int const argc, const char * argv[]) {
    log("main()");

    // Here the program would enter a main loop, with log() potentially being
    // called from multiple threads at various times.
}

输出:

Object::Object()
main()
Object::~Object()

为简洁起见,省略了锁的错误检查和 RAII 包装。

这个例子包括一个线程安全的(至少这是意图)日志记录功能,该功能旨在在程序的整个生命周期内都可用,包括在取消初始化具有静态存储持续时间的对象期间,可能(尽管在这种情况下不是)跨越多个翻译单元,这意味着去初始化顺序可能是不确定的。

问题是没有机会安全地销毁互斥锁​​,因为在程序生命周期的任何时候都可能需要它。 (实际上,我不打算让对象具有静态存储持续时间和重要的析构函数,但我仍然有兴趣解决这个问题。)

出现的第一个问题是使用PTHREAD_MUTEX_INITIALIZER 初始化的互斥锁是否需要使用pthread_mutex_destroy() 销毁。至少documentation 的某些版本包含以下措辞:

在默认互斥属性合适的情况下,宏 PTHREAD_MUTEX_INITIALIZER 可用于初始化互斥锁。这 效果应等同于通过调用动态初始化 参数 attr 指定为 NULL 的 pthread_mutex_init(),除了 不执行错误检查。

这表明如果 pthread_mutex_destroy() 预计会在使用 pthread_mutex_init() 初始化的互斥锁上调用,那么它也预计会在使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥锁上调用。

但是,在在线和 Stack Overflow 上搜索时,我发现是否需要这样做存在分歧。例如,here 有人引用了一本关于 Linux 开发的书:

没有必要在互斥体上调用 pthread_mutex_destroy() 使用 PTHREAD_MUTEX_INITIALIZER 静态初始化。

另一方面,在this thread 中,有人认为实际上需要显式销毁这样的互斥体。

我还看到它认为在这种情况下没有必要清理互斥锁,无论它们是如何初始化的,因为无论如何资源都会被回收。 (这可能与“首次使用时构造并故意泄漏内存”惯用语背后的逻辑相同,有时用于单例和其他具有静态存储持续时间的对象。)

我发现了许多涉及该主题的其他线程,关于是否/如何销毁互斥锁​​的意见不一。我还要提一下,我相信我已经看到了来自可靠来源的生产代码,这些代码使用 PTHREAD_MUTEX_INITIALIZER 初始化互斥锁并且从不破坏它们。

出于尽职调查的目的,我在这里详细介绍了一些细节,但我的问题(我认为)相当简单。拥有从初始化到程序生命周期结束时都可用的互斥锁会很有用。我怀疑不清理这样的互斥体不会引起任何问题,但这种方法让我很困扰。即使有人说使用 PTHREAD_MUTEX_INITIALIZER 初始化的互斥锁不需要清理,这似乎与文档和其他人的各种声明相反。

总之,是否有一种安全且合理的方法来管理旨在在程序生命周期结束之前可用的 pthread 互斥锁?这里有没有我在搜索中没有偶然发现的标准最佳实践?

【问题讨论】:

  • 哪个先发布?静态变量mutex或全局Object占用的内存。这可能接近于未定义的行为。这也是为什么我几乎禁止我的团队使用全局对象的原因。在main 返回之后运行任何重要的代码是在出错时进行调试的噩梦。
  • 我认为这里没有顺序或未定义的行为问题,因为 pthread_mutex_t 很容易被破坏。如果有问题,只是没有清理“互斥锁”。我同意全局对象(至少在大多数情况下),但由于各种原因,我仍然对创建一个可以在程序执行中的任何点安全调用的线程安全函数的问题感兴趣(本例中的 log() )。
  • 为什么不使用标准 C++ 线程和互斥锁,让它们的内部处理这些细节?
  • @shawn - 我相信用std::mutex 替换pthread_mutex_t 风险更大,因为后者没有简单的析构函数。如果日志功能中的互斥锁在Object 实例之前破坏,您将遇到~Object 尝试记录的问题。这就是为什么可能可以安全地执行 OP 显示的操作。我会在回答时尝试详细说明。
  • @scg - 虽然我上面的评论表明我不喜欢使用全局对象(正如您在帖子中所指出的那样),但我认为您所拥有的是安全且可以的。我现在没有时间写一个完整的答案,但我今晚会尝试一些更正式的东西。

标签: c++ pthreads mutex


【解决方案1】:

既然用PTHREAD_MUTEX_INITIALIZER初始化就相当于调用pthread_mutex_init,那么调用pthread_mutex_destroy就可以销毁这样的互斥锁了。

但是,不需要调用pthread_mutex_destroy;资源将在程序退出时由操作系统回收。由于它是一个带有微不足道的析构函数的对象,因此它不会在程序退出时作为静态清理的一部分被销毁,因此在程序结束之前可以安全使用。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-08-07
    • 1970-01-01
    • 1970-01-01
    • 2021-06-11
    • 1970-01-01
    • 2021-09-01
    • 2013-06-06
    • 2013-05-12
    相关资源
    最近更新 更多