在两个线程之间传递数据时的时序不一致

时间:2014-01-30 04:56:55

标签: c++ linux pthreads queue spinlock

我有一段代码用于在将数据从生产者(线程1)传递给使用者(线程2)时测试各种容器(例如deque和循环缓冲区)。数据由具有一对时间戳的结构表示。在推入生产者之前获取第一个时间戳,在消费者弹出数据时获取第二个时间戳。 容器用pthread螺旋锁保护。

机器运行带有2.6.18内核的redhat 5.5(旧!),它是一个禁用超线程的4核系统。所有测试都使用gcc 4.7和-std = c ++ 11标志。

生产者获取锁定,为数据加时间戳并将其推入队列,解锁并在繁忙的循环中休眠2微秒(我发现在该系统上只有2微秒的唯一可靠方式)。

消费者锁定,弹出数据,为其加上时间戳并生成一些统计信息(运行平均延迟和标准差)。统计数据每5秒打印一次(M是平均值,M2是std dev)并重置。我使用gettimeofday()来获取时间戳,这意味着平均延迟数可以被认为是超过1微秒的延迟百分比。

大多数情况下输出如下:

    CNT=2500000 M=0.00935 M2=0.910238
    CNT=2500000 M=0.0204112 M2=1.57601
    CNT=2500000 M=0.0045016 M2=0.372065

但有时候(可能是20次中的1次试验)是这样的:

    CNT=2500000 M=0.523413 M2=4.83898
    CNT=2500000 M=0.558525 M2=4.98872
    CNT=2500000 M=0.581157 M2=5.05889

(注意平均数比第一种情况要糟糕得多,并且随着程序的运行它永远不会恢复。)

我很想知道为什么会这样。感谢。

#include <iostream>
#include <string.h>
#include <stdexcept>
#include <sys/time.h>
#include <deque>
#include <thread>
#include <cstdint>
#include <cmath>
#include <unistd.h>
#include <xmmintrin.h> // _mm_pause()

int64_t timestamp() {
    struct timeval tv;
    gettimeofday(&tv, 0);
    return 1000000L * tv.tv_sec + tv.tv_usec;
}

//running mean and a second moment
struct StatsM2 {
    StatsM2() {}
    double m = 0;
    double m2 = 0;
    long count = 0;
    inline void update(long x, long c) {
        count = c;
        double delta = x - m;
        m += delta / count;
        m2 += delta * (x - m);
    }
    inline void reset() {
        m = m2 = 0;
        count = 0;
    }
    inline double getM2() { // running second moment
        return (count > 1) ? m2 / (count - 1) : 0.;
    }
    inline double getDeviation() {
        return std::sqrt(getM2() );
    }
    inline double getM() { // running mean
        return m;
    }
};

// pause for usec microseconds using busy loop
int64_t busyloop_microsec_sleep(unsigned long usec) {
    int64_t t, tend;
    tend = t = timestamp();
    tend += usec;
    while (t < tend) {
        t = timestamp();
    }
    return t;
}

struct Data {
    Data() : time_produced(timestamp() ) {}
    int64_t time_produced;
    int64_t time_consumed;
};

int64_t sleep_interval = 2;
StatsM2 statsm2;
std::deque<Data> queue;
bool producer_running = true;
bool consumer_running = true;
pthread_spinlock_t spin;

void producer() {
    producer_running = true;
    while(producer_running) {
        pthread_spin_lock(&spin);
        queue.push_back(Data() );
        pthread_spin_unlock(&spin);
        busyloop_microsec_sleep(sleep_interval);
    }
}

void consumer() {
    int64_t count = 0;
    int64_t print_at = 1000000/sleep_interval * 5;
    Data data;
    consumer_running = true;
    while (consumer_running) {
        pthread_spin_lock(&spin);
        if (queue.empty() ) {
            pthread_spin_unlock(&spin);
            // _mm_pause();
            continue;
        }
        data = queue.front();
        queue.pop_front();
        pthread_spin_unlock(&spin);
        ++count;
        data.time_consumed = timestamp();
        statsm2.update(data.time_consumed - data.time_produced, count);
        if (count >= print_at) {
            std::cerr << "CNT=" << count << " M=" << statsm2.getM() << " M2=" << statsm2.getDeviation() << "\n";
            statsm2.reset();
            count = 0;
        }
    }
}

int main(void) {
    if (pthread_spin_init(&spin, PTHREAD_PROCESS_PRIVATE) < 0)
        exit(2);
    std::thread consumer_thread(consumer);
    std::thread producer_thread(producer);
    sleep(40);
    consumer_running = false;
    producer_running = false;
    consumer_thread.join();
    producer_thread.join();
    return 0;
}

2 个答案:

答案 0 :(得分:1)

编辑:
我相信下面的5是唯一可以解释1/2秒延迟的东西。当在同一个核心上时,每个核心都会运行很长时间,然后才会切换到另一个核心 列表中的其余部分太小而不会导致1/2秒的延迟 您可以使用pthread_setaffinity_np将线程固定到特定的核心。您可以尝试不同的组合,看看性能如何变化。

编辑#2:
你应该注意的更多事情:(谁说测试很简单......)
1.确保生产者开始生产时消费者已经在运行。在你的情况下不太重要,因为生产者并没有真正生产紧密的循环 2. 这是非常重要的 :你每次都按计数除以,这对你的统计数据来说不是正确的。这意味着每个统计窗口中的第一次测量比最后一次重量更多。要衡量中位数,您必须收集所有值。在不收集所有数字的情况下测量平均值和最小值/最大值,可以为您提供足够好的延迟图像。

真的,这并不奇怪 1.时间是在Data()中进行的,但是容器花费时间调用malloc。
你在运行64位还是32位?在32位gettimeofday是一个系统调用,而在64位,它是一个没有进入内核的VDSO ...你可能想要自己gettimeofday本身并记录方差。或者使用rdtsc注册自己 最好的方法是使用循环而不是微观,因为微观对于这种情况实际上太大了......只有微处理器才能在处理如此小规模的事物时非常倾斜 3.您是否保证不会在生产者和消费者之间获得优先权?我猜不是。但这不应该在一个专门用于测试的盒子上频繁发生...... 4.单个插座上是4芯还是2个?如果它是一个2插槽盒,你想让2个线程在同一个插槽上,或者你支付(至少)两倍的数据传输。
5.确保螺纹没有在同一个核心上运行 6.如果您传输的数据和其他数据(容器节点)正在与其他Data +节点共享缓存行(可能类型),则消费者在写入消耗的时间戳时会延迟生产者。这称为虚假共享。您可以通过填充/对齐到64字节并使用侵入式容器来消除此问题。

答案 1 :(得分:0)

gettimeofday不是分析计算开销的好方法。它是挂钟,您的计算机是多处理的。即使您认为自己没有运行任何其他东西,操作系统调度程序也总是有一些其他活动来保持系统运行。要分析您的流程开销,您必须至少提高要分析的流程的优先级。还可以使用高分辨率计时器或cpu tick来进行计时测量。