我正在尝试理解我希望在大型c ++程序中使用的监视机制的性能影响。
我有一个容器,指向来自整个程序的双打指针,我有一个单独的线程,“监视”这些双打,并在更新时打印出最新的值。
如果其中一个双打快速改变两次(例如2-> 3-> 4),我不在乎错过3.如果其中一个改变然后改变回来(例如2-> 3) - > 2),我也不在乎是否错过了整个更新。我唯一关心的是最终的值最终被打印出来。这里主要关注的是,这对主程序的性能影响非常小,它主要以任意顺序读取和写入这些双精度程序。
下面我有一个我认为是一个有效的示例程序,但我不知道这可能会产生什么样的性能影响。我已经对玩具示例进行了一些基准测试,它似乎没有显着的伤害,但我担心我的玩具示例并没有像大型程序那样带来效果。最坏情况下的响应延迟在这里非常重要,所以我想在构建过多的系统之前确保这个机制是合理的。
我真正感兴趣的是一些教育。为什么从不同的线程中查看值对性能有任何影响?我进行了100毫秒的睡眠以缓解这种影响,但是我可能会忽略理论中关于为什么它在具有多个CPU的系统上的重要性。是否有有趣的缓存效果?
最后,我是否应该更改显示器的影响较小?
提前感谢您的帮助。
#include <iostream>
#include <thread>
#include <vector>
#include <string>
struct Record {
volatile double& target_;
double last_;
std::string name_;
};
void monitor(std::vector<Record> records) {
while (true) {
for (auto& record : records) {
double target = record.target_;
if (record.last_ != target) {
std::cout << record.name_ << ' ' << target << std::endl;
record.last_ = target;
}
}
// does this throttling help anything?
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
int main() {
std::vector<double> v(5, -1);
std::vector<Record> records;
for (size_t i = 0 ; i < v.size() ; ++i) {
records.push_back({v[i], v[i], "v[" + std::to_string(i) + "]"});
}
std::thread(monitor, records).detach();
for (size_t i = 0 ; i < v.size() ; ++i) {
// Can the presence of the monitor slow down this write? (or a read?)
v[i] = i;
std::this_thread::sleep_for(std::chrono::seconds(1));
}
}
澄清:
我循环使用所有内容而不是使用同步的原因是我有大量内核无所事事,我希望最大限度地减少主编写器线程所做的工作。
我使用double而不是std :: atomic的原因是我希望能够使用标准基元类型编写主程序,然后“注册”某些双精度进行监控(启动时),但之后只需正常使用它们在主程序中。看来我可能无法依赖原子写作。
答案 0 :(得分:2)
阅读我的整个帖子,我在#34;回答你的实际问题&#34;后面有重要的事情。
读取另一个处理器(CPU,核心或其他任何处理器)的缓存中的数据的直接影响是缓存内容必须传输到另一个CPU。对于不同的处理器架构,效率/低效率会有所不同。在最坏的情况下,数据必须写入实际的RAM,在其他情况下,它会进入某个中间缓存,或者在一些内部的处理器间通信通道上传输。总体效果可能是在&#34;发送&#34;上刷新缓存条目。处理器,但它往往更好。
写入同一个缓存行(last_ = x
)也会影响其他处理器,通过刷新缓存行,所以现在另一个处理器必须重新加载target_
值,即使它没有改变。再次,详细说明&#34;坏&#34;这取决于确切的处理器架构,有时还取决于系统设计(例如,速度和内存类型)。
然而,你的代码在一般意义上听起来并不完整:
读取/写入非原子数据的效果是不可预测的(并且至少是实现定义的,如果不是未定义的行为),并且取决于实际的处理器体系结构。我们无法保证您的示例中的读取target_
将以原子方式完成,因此您可以获得随机垃圾作为&#34;结果&#34;。这可能导致各种问题,包括应用程序崩溃,例如,实际数据的两半包含不一致的浮点数据,并导致处理器采取FP异常。
请注意,如果另一个线程正在更新for (auto& record : records) {
向量,records
不安全 - 它可能会重新分配整个向量内容,并且您的当前数据会被重新用于其他目的。
答案 1 :(得分:1)
从示例代码中,您应该注意以下几个性能/安全问题:
由于监视器线程永远不应该写入向量中的double值,因此使用const double&amp;引用而不是double&amp ;.除了存储双精度值的内存在代码之外被修改之外,这也不是你想要的'volatile'关键字。
迭代向量中的所有值以查找修改后的值是一种CPU密集型且效率非常低的方法。基本上你使用poll而不是push通知让监视器线程知道值何时发生变化。
使用条件变量在发生更改时唤醒监视器线程。以下是有关如何使用条件变量的教程 - http://baptiste-wicht.com/posts/2012/04/c11-concurrency-tutorial-advanced-locking-and-condition-variables.html
答案 2 :(得分:1)
好吧,这可能&#34;有效&#34;就像你认为的那样,但不会有你想要的性能,并且有很多方法可以更好地编写这种类型的功能。
一般来说,存在一些问题:
查看同步原语和互斥。如果您之前没有实现过生产者/消费者模式,那么学习它将非常有用。
尽管尝试以多线程方式对其进行优化很有诱惑力,但真正的答案是并行处理将是一个很好的解决方案。
编辑:这是一个可能对您有价值的玩具问题: 拥有一个10000的大小队列,当一个线程未满时,单个线程将随机双打插入队列。 有10个线程从队列中弹出并输出它是否是素数。