在C++11中另开一个线程提前生成随机数

6

为了进行一些c++的数值模拟,我需要生成许多指数分布的随机数(所有随机数都具有相同的预定分布)。目前,我的程序运行良好,但超过50%的CPU时间用于生成这些随机数。

我想做的是以不阻塞模拟主循环的方式生成这些随机数。更准确地说,我想要一个线程,其工作是始终保持一个随机数“提前准备好”,并在某人读取此随机数时立即生成新的随机数。

有人知道一个不错的方法来实现这个吗?

目前,我的顺序代码如下:

#include <stdio.h>
#include <iostream>
#include <random>

using namespace std;

// exponential random variable with parameter lambda
class EXPGenerator{
    exponential_distribution<> expo;
    mt19937 engine; //mersene twister
public:
    EXPGenerator(double lambda){
        expo = exponential_distribution<>(lambda);
        engine = mt19937(time(NULL));
    }

    double step(){  
        return expo(engine);
    }
};

int main(int argc, char *argv[])
{
    EXPGenerator expgen(2.0);
    for(int i=0; i<100000; i++) {
        double randv(expgen.step());
        std::cout << randv << endl;
        // do something complicated
    }
    return 0;
}

我使用 clang++ -O2 --std=c++11 --stdlib=libc++ test.cpp -o test 进行编译。

[编辑:上面添加了 -O2]


7
你为什么要基于一个未经过优化的版本来做决策?在采用复杂且难以维护的解决方案之前,请先测试一下是否真的存在问题。一旦启用优化,代码可能已经足够快了。 - jalf
上面的程序只是一个例子,实际上我想基于它提出问题。我已经尽可能地优化了真正的程序,但运行仍需要几个小时... 我们正在讨论模拟随机扩散过程,这需要进行数千次时间步骤和数千次模拟以获得良好的统计数据... - Nown
1
您正在进行未启用优化的编译。 - jalf
2
确实,如上所述命令行。但是真正的程序(太长了,无法在此处显示)是使用-O2编译的。我应该在原始问题中放置-O2,抱歉。 - Nown
好的,没问题。 :) 是的,如果在原始问题中展示了这一点,就可以避免一些混淆了。但只要你的测量基于优化构建,那就是最重要的 ;) - jalf
5个回答

6

你应该尝试的第一件事是启用优化。尝试在clang命令行中添加一个-O2选项。


是的,我已经尝试过-O2,虽然它确实可以将执行时间减少一些因素,但实际模拟仍需要几个小时,通过在另一个线程中生成随机数可能可以将其缩短一半。 - Nown
我应该在问题中提到,抱歉。 - Nown

6
使用带有边界的队列,让一个线程向该队列推入随机数,并在队列满时阻塞该线程。要获取随机数,从该队列中取出一个数字,并在队列为空时让消费者线程阻塞。
这种简单的设计可以让生产者在线程可用时生成随机数并将其放入队列中。
优化:使用包含随机数列表的队列。在这种情况下,生产者将生成一个完整的随机数列表。消费者将保持一个缓存(可能在EXPGenerator内部)并从队列中取出一个列表。一旦缓存为空,就会使用来自队列的新列表填充缓存。这将减少上下文切换的开销,应仅在测量显示这是有意义的情况下使用。
队列应该基本上是一些具有T为随机数的std::deque或std::vector(随机数列表)。使用互斥锁同步对该std:queue的访问,并使用两个条件变量。其中一个用于向插入更多随机数的队列发出信号,另一个用于表示队列中已经至少有一个元素。当队列为空时,让消费者等待第二个条件,而当队列已满时,让生产者等待第一个条件。

4

当你处理优化(正如其他人所建议的)时,你可以在另一个线程中创建一堆随机数并将它们存储在向量中,然后使用消息队列将其传输到主线程。在那里,你可以将其包装成你的 EXPGenerator


是的,那就是我想做的。不幸的是,我不知道如何实现它。我会看一下消息队列的工作原理。 - Nown
1
@Nown 队列基本上应该是一些 std::deque<T>,其中 T 是一个随机数,或者是 std::vector<double>(一组随机数)。使用互斥锁来同步访问该 std:queue,并使用两个条件变量。一个用于信号表明可以再次插入更多的随机数。另一个用于信号表明队列中已经至少有一个元素。当队列为空时,让消费者等待第二个条件,当队列满时,让生产者等待第一个条件。 - Torsten Robitzki

2

这里有一个优化可能性,我认为还没有人提到过。

我看不出消费者线程等待随机数时为什么要在生产者线程上阻塞。也就是说,如果随机数缓存用尽了,不要阻塞,而是在消费者线程本身上生成一个或多个随机数,然后再检查缓存。

不需要阻塞通信也使得使用轻量级、无锁数据结构来进行线程间通信变得更加容易。优秀的候选包括:

事实上,如果只有一个“帮手线程”,在恰好有一个生产者和一个消费者之间进行通信的特殊情况下,可以使用循环缓冲区而完全不需要使用任何原子内存操作。


1

首先创建您的随机生成线程。由于线程同步与生成一个随机数相比较昂贵,因此使用一个具有容量10k的向量(如Jan所建议的)加载随机数是一个好主意。线程的创建、终止和销毁也很麻烦,因此将“随机”线程循环到对“go”自动重置事件(请参见MSDN),初始化为true上等待——线程将在启动时生成一个随机数向量,然后每当“go”被信号化时生成一个随机数向量。

您需要一种机制来等待直到向量完全组装完成才能拥有它。您可以将其发布到生产者-消费者队列、Windows消息队列中(如Jan所建议的),但在这种情况下,直接从线程中获取向量可能更容易。您可以使用另一个初始化为false的“complete”自动重置事件并等待它,当随机线程完成时会发出信号。

一旦您拥有了一个向量,请发出“go”事件的信号以启动随机线程生成另一个向量,以便在以后需要时它可能已经完成。

您需要一个可以轻松转移所有权的向量实例。我可能会使用指针,在随机线程中使用new创建一个指针,在主线程中复制指针值并在完成后删除它。每当随机线程通过“go”时,它将新建另一个向量,重新设置自己的指针。如果有适合的智能指针类可用,则可以使用该类,可能是unique_ptr,因为它可以移动。

C++11中提供了互斥锁和条件变量,因此无需使用Windows构造。而且为什么要使用指针而不是std::vector?你可以通过交换它们轻松地在两个线程之间传输。 - Torsten Robitzki
@TorstenRobitzki - 好的,没问题,随便你:)。只要能避免复制构造函数就行了。另外,我误读了标签,因为我刚看完一个winapi的问题:( - Martin James

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接