【问题标题】:Portable thread class implemented in C++用 C++ 实现的可移植线程类
【发布时间】:2011-02-24 16:05:02
【问题描述】:

在编写需要在应用程序和中断例程/线程/回调例程之间共享文件范围变量的 C 程序时,众所周知,必须将变量声明为 volatile,否则编译器可能会做错优化。这是我的意思的一个例子:

int flag;

void some_interrupt (void)
{
  flag = 1;
}



int main()
{
  flag = 0;
  ...

  /* <-- interrupt occurs here */

  x = flag; /* BUG: the compiler doesn't realize that "flag" was changed 
                    and sets x to 0 even though flag==1 */

}

为了防止上述错误,“flag”应该被声明为 volatile。

我的问题是:在创建包含线程的类时,这如何应用于 C++?

我的课程看起来像这样:

class My_thread
{
  private:
    int flag;

    static void thread_func (void* some_arg) // thread callback function
    {
      My_thread* this_ptr= (My_thread*)some_arg;

    }
};

“some_arg”将包含一个指向该类实例的指针,因此“My_thread”的每个对象都有自己的线程。通过这个指针,它将访问成员变量。

这是否意味着“this_ptr”必须声明为指向易失性数据的指针? “标志”也必须是易变的吗?如果是这样,我是否必须使所有修改“标志”的成员函数变为 volatile?

我对特定操作系统或编译器的行为方式不感兴趣,我正在寻找一种通用的、完全可移植的解决方案。

编辑:这个问题与线程安全无关!

真正的代码会有信号量等。

为了澄清,我希望避免由于编译器不知道回调函数可能从程序本身之外的源调用而导致的错误,从而对是否使用某些变量做出错误的结论。我知道如何在 C 中执行此操作,如第一个示例所示,但在 C++ 中不知道。

【问题讨论】:

  • 也许是众所周知的,但是错了。 volatile 不会使操作成为原子操作,并且可能不会对多线程提供帮助。
  • 太真实了,volatile 在这里无济于事。如果是这样,它将是特定于编译器的。您是否考虑过使用 Boost 实现? boost.org/doc/libs/1_42_0/doc/html/thread.html
  • volatile 会在某种程度上解决问题,但它仍然是错误的解决方案。你需要一个内存屏障,而不是volatile。 (并且您可能还需要使用 CPU 的原子指令来设置/测试标志,具体取决于平台)
  • 您说您可能需要将this_ptr 声明为“指向易失性数据的指针”是什么意思?您想要指向My_thread 对象的指针,因此将其声明为My_thread*。使类成员volatile 不会使它们成为线程安全的。您需要使用事件和/或互斥锁来实现这一点。这个问题很有趣:stackoverflow.com/questions/2484980/…
  • 由于当前的 C 和 C++ 标准都没有标准线程库,因此您无法编写通用且完全可移植的线程代码。在 C++0x 被广泛使用之前,您将获得的最接近的是 Boost::Thread。

标签: c++ multithreading volatile


【解决方案1】:

嗯,这个编辑让世界变得与众不同。信号量引入了内存屏障。这些使volatile 变得多余。对信号量进行任何操作后,编译器总是会重新加载int flag。

Fred Larson 已经预测到了这一点。 volatile 在没有锁的情况下是不够的,在有锁的情况下是多余的。这使得它对于线程安全编程毫无用处。

【讨论】:

  • 我不太明白内存屏障与此有什么关系?它们的存在是为了防止由意外的执行顺序引起的错误,对吗?我担心的错误是编译器甚至没有意识到回调函数已经执行。
  • @Lundin:不,内存屏障还强制编译器和 CPU 存储任何脏缓存值(写屏障)并重新加载任何未修改的缓存值(读屏障)。结合起来,他们确保flag 被写入内存并从内存中读回。
  • 好的,我明白了。但是我怎么知道记忆障碍实际上会为我做这件事呢?没有关于如何实施它们的标准。
  • @Lundin:我的解释中的“内存障碍”是一个描述性术语——它们是高级多线程操作中的幕后机制。可以说,只要这些更高级别的操作有标准,您就不需要它们的标准。但另请参阅this previous question 了解更多信息
  • +1 表示内存屏障。我们需要让更多的人学习这个术语。
【解决方案2】:

从函数指针签名我猜你正在使用 posix 线程实现线程。我假设您想知道如何使用此 API 启动线程。首先考虑改用boost thread。如果不是一个选项,我通常会使用类似以下的方法来获得一些舒适的 Java 可读性。

class Runnable {
    public:
        virtual void run() = 0;
};

class Thread : public Runnable {
    public:
        Thread();
        Thread(Runnable *r);

        void start();
        void join();        

        pthread_t getPthread() const;

    private:

        static void *start_routine(void *object);

        Runnable *runner;
        pthread_t thread;
};

然后在 start_routine 函数中类似这样:

void* Thread::start_routine(void *object) {
    Runnable *o = (Runnable *)object;

    o->run();

    pthread_exit(NULL);
    return NULL;
}

现在对扩展 Runnable 或 Thread 类的类字段的访问不必是 volatile 的,因为它们是 thread-local。

也就是说,在线程之间共享数据比使用 volatile 数据成员更复杂,如果这是您所要求的...

【讨论】:

  • 这将如何帮助解决由于编译器不知道线程回调函数已被执行而导致的错误?不,我实际上使用的是 Windows CreateThreads,但我的问题是通用的。
  • 我想我的意思是只有 one 线程访问的数据成员不需要是 volatile 的,当一个数据成员被两个或多个线程访问时 volatile 将无济于事靠它自己。
【解决方案3】:

阅读 Andrei Alexandrescu 在 Dr. Dobbs 的这篇文章,它可能是相关的:

volatile - Multithreaded Programmer's Best Friend

来自文章的介绍:

volatile 关键字被设计为 防止编译器优化 可能会在 某些异步的存在 事件。例如,如果您声明一个 原始变量为 volatile,则 不允许编译器缓存它 在寄存器中——一种常见的优化 如果那样的话,那将是灾难性的 变量在多个之间共享 线程。所以一般规则是,如果 你有原始类型的变量 必须在多个之间共享 线程,声明这些变量 易挥发的。但你实际上可以做一个 这个关键字更多:你可以 用它来捕获不是 线程安全,你可以这样做 编译时间。这篇文章展示了如何 完成了;该解决方案涉及 简单的智能指针,也使 很容易序列化关键部分 代码。

【讨论】:

    【解决方案4】:

    这里给出了 Windows 和 Linux 的一些回退机制的实现。试试this example:

    typeReadFileCallback varCallback;
    

    我能够使用它来实现。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-12-03
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多