【问题标题】:volatile for signal handler and multi-threadingvolatile 用于信号处理程序和多线程
【发布时间】:2017-02-11 23:25:27
【问题描述】:

据说信号处理程序需要volatile,例如,

volatile int flag = 1; // volatile is needed here?

void run() {
    while(flag) { /* do someting... */ }
}

void signal_handler(int sig) {
    flag = 0;
}

int main() {
    signal(SIGINT, sig_handler);
    run();
    // ...
}

据说 volatile 通常不用于多线程。但是在多线程中与上面类似的情况如何:

int flag = 1; // is volatile needed here?

void thread_function() {
    while(flag) { /* do someting... */ }
}

int main() {
    // pthread_create() to create thread_function()...
    sleep(10); // let thread_function run for 10 seconds
    flag = 0;
    // ...
}

在这两种情况下都应该使用 volatile 关键字吗?编译器对这两种情况的处理方式是否相同?

【问题讨论】:

  • volatile [通常] 不足以用于线程间操作(例如锁定、环形队列索引等)。但是,您在这里的用法是用于“运行”标志。只有 1 位作者 (main)。而且,在这两种情况下,它都应该是volatile。在第二种情况下,假设线程函数是while (flag) do_stuff();main 清除flag 是一种告诉线程退出循环、进行清理和退出的“温和”方式。您可以将第一个和第二个结合起来,并让信号处理程序为线程清除 flag。线程需要pthread_mutex_t 等。等等。
  • 我会使用std::atomic_flag(在每个系统上始终是无锁的)或std::atomic_signal_fencevolatile 仅强制编译器从指定的内存位置读取和写入,而不是优化值。内存位置也可以只是缓存。 volatile` 不会阻止编译器重新排序您的代码。

标签: c++ c multithreading volatile


【解决方案1】:

您可以从信号处理程序中修改的唯一非本地值是volatile sig_atomic_t 类型和原子类型的值。特别是,不允许写信给你的volatile int,如果你的信号处理程序运行,你有未定义的行为。

【讨论】:

  • 有用的,如果稍微有点切线,添加:虽然在信号处理程序的上下文中不允许您非常多,但您可以 write() 到文件描述符。这通常是将“事件”消息传递到主程序逻辑的最有用的方法。 Linux 甚至为这种使用模式提供了一个方便的signalfd API(即,与其编写显式转发事件的信号处理程序,不如注册一个signalfd 以从中读取事件)。
  • 你可以修改sig_atomic_t以外的原子类型吗?我以为这是唯一的一个;否则,为什么有必要呢?
  • 原子类型对于信号处理程序必须是无锁的,但如果基本类型是标准整数类型,它们通常是无锁的。
  • @rici,不幸的是,除了名称之外,sig_atomic_t 不是原子类型。它早于 C11 和线程和原子的引入,并且必须由所有 C 平台提供,而线程和原子是可选的。
  • GNU 说:“在实践中,你可以假设 int 是原子的。你也可以假设指针类型是原子的;这非常方便。这两个假设在所有机器上都是正确的GNU C 库支持我们所知道的所有 POSIX 系统。” gnu.org/software/libc/manual/html_node/Atomic-Types.html 我认为这意味着在这样的系统上的信号处理程序中读/写volatile intvolatile char* 是可以的。 (但根据规范,它仍然是未定义的行为,可能会导致其他系统出现问题。)
【解决方案2】:

[intro.execution] 第 6 段中的 c++ 标准告诉:

当抽象机的处理因接收到信号而中断时,两者都不是的对象的值
— volatile std::sig_atomic_t nor 类型
— 无锁原子对象 (29.4)
在信号处理程序执行期间未指定,并且处理程序修改的任何不在这两个类别中的任何一个对象的值都将变为未定义。

因此,是的,对于信号处理程序,您必须使用volatile std::sig_atomic_t

【讨论】:

    【解决方案3】:

    volatile 用于确保从其实际位置(在我们的例子中为内存)而不是从 CPU 寄存器中读取变量的内容。

    换句话说,每当“外部”事件可能会改变变量的值时,您应该考虑使用volatile(“外部” - 如在相关代码块之外)。

    在您的两个示例中,您都使用变量作为标志来表示行为发生变化。在这两个示例中,此标志由正在查看标志的循环“外部”的事件控制。因此,这两个示例都需要使用 volatile 关键字。

    应该注意volatile 确实 提供线程安全,原因有很多。为确保对象是线程安全的,读/写操作必须是受保护的或原子的。

    【讨论】:

    • ...当读/写操作受到保护或原子操作时,您不再需要volatile
    • @M.M - 虽然这是真的,但将 volatile 与受保护的操作一起使用仍然是一种好习惯。尽管 C11 原子变量总是从内存而不是 CPU 中读取(使用 atomic_load 时),但在锁内更改的变量“暗示”为易失性......但是当数据在外部读取时优化可以做一些有趣的事情锁,例如在 OP 示例中。
    • C 的volatile 与内存无关。您正在考虑 Java。 C 语言的volatile 禁止编译器优化任何访问,并禁止对具有可见副作用的其他操作进行重新排序。
    • @PaulJ.Lucas 有an explanation here,但一般来说,抽象机的规则需要对 volatile 对象进行内存读/写。来自标准:“具有 volatile 限定类型的对象可能会以实现未知的方式进行修改或具有其他未知的副作用。因此,任何引用此类对象的表达式 应严格按照抽象机的规则进行评估" ...所以我认为它回答了关于多线程代码中标志的问题。
    • @Myst 链接到的页面和您所写的内容中都没有明确说明所有 volatile 访问必须从 main 读取或写入记忆。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-01-24
    • 2011-11-18
    • 2021-12-21
    • 1970-01-01
    • 1970-01-01
    • 2014-08-29
    • 1970-01-01
    相关资源
    最近更新 更多