使用匿名管道是否为跨线通信引入了内存屏障?

时间:2016-03-07 06:31:42

标签: c++ windows posix memory-model

例如,假设我使用new分配结构并将指针写入匿名管道的写入端。

如果我从相应的读取端读取指针,我保证在结构上看到“正确”的内容吗?

同样令人感兴趣的是unix& amp;上的socketpair()的结果。在Windows上通过tcp环回自连接具有相同的保证。

上下文是一个服务器设计,它使用select / epoll

集中事件调度

6 个答案:

答案 0 :(得分:4)

  

例如,假设我使用new分配结构并将指针写入匿名管道的写入端。

     

如果我从相应的读取端读取指针,我保证会看到'正确的'结构上的内容?

没有。无法保证写入CPU会刷新其缓存的写入并使其对可能执行读取的其他CPU可见。

  

同样令人感兴趣的是unix& amp;上的socketpair()的结果。在Windows上通过tcp环回自连接具有相同的保证。

没有

答案 1 :(得分:2)

实际上,调用write()(系统调用)将最终锁定内核中的一个或多个数据结构,这应该处理重新排序问题。例如,POSIX需要后续读取才能看到在调用之前写入的数据,这意味着锁定(或某种获取/释放)本身。

至于是否是电话正式规范的一部分,可能不是。

答案 2 :(得分:2)

指针只是一个内存地址,所以如果你在同一个进程上指针在接收线程上有效,并指向同一个结构。如果您使用不同的进程,最多会立即出现内存错误,更糟糕的是,您将读取(或写入)一个基本上未定义的行为的随机内存。

您会阅读正确的内容吗?既没有更好也没有比你的指针在两个线程共享的静态变量中更好:如果你想要一致性,你仍然必须做一些同步

静态内存(由线程共享),匿名管道,套接字对,tcp环回等之间的传输地址类型是否重要?否:所有这些通道都传输字节,所以如果你传递一个内存地址,你将获得你的内存地址。那么剩下的就是同步,因为在这里你只是共享一个内存地址。

如果您不使用任何其他同步,可能会发生任何事情(我是否已经谈到了未定义的行为?):

  • 读取线程可以通过编写一个提供陈旧数据的内容来访问内存
  • 如果您忘记将结构成员声明为volatile,则读取线程可以继续使用缓存值,此处再次获取过时数据
  • 读取线程可以读取部分写入的数据,意味着不连贯的数据

答案 3 :(得分:1)

到目前为止,有趣的问题只来自Cornstalks的一个正确答案。

在同一个(多线程)进程中,由于指针和数据遵循不同的路径到达目的地,因此无法保证。 隐式获取/释放保证不适用,因为struct数据不能通过缓存捎带指针,而且正式处理数据争用。

然而,看看指针和结构数据本身如何到达第二个线程(分别通过管道和内存缓存),这个机制很可能不会造成任何伤害。 将指针发送到对等线程需要3个系统调用(发送线程中的write(),接收线程中的select()read(),这是(相对)昂贵的并且到指针的时候价值可用 在接收线程中,struct数据很可能早就到了。

请注意,这只是一个观察,机制仍然不正确。

答案 4 :(得分:0)

我相信,你的情况可能会减少到这个2线程模型:

int data = 0;
std::atomic<int*> atomicPtr{nullptr};
//...

void thread1()
{
    data = 42;
    atomicPtr.store(&integer, std::memory_order_release);
}

void thread2()
{
    int* ptr = nullptr;
    while(!ptr)
        ptr = atomicPtr.load(std::memory_order_consume);
    assert(*ptr == 42);
}

由于你有2个进程,你不能在它们之间使用一个原子变量,但是因为你列出了,你可以从消费部分省略atomicPtr.load(std::memory_order_consume),因为,AFAIK,所有的体系结构都是Windows运行时保证此负载正确无负载侧的任何障碍。事实上,我认为没有太多的架构,那些指令不会是NO-OP(我只听说过DEC Alpha)

答案 5 :(得分:0)

我同意Serge Ballesta的回答。在同一个过程中,通过匿名管道发送和接收对象地址是可行的。

由于当消息大小低于write(通常为4096字节)时,PIPE_BUF系统调用保证是原子的,因此多生产者线程不会相互混淆对象地址(64位应用程序为8个字节)。

谈话很便宜,这里是Linux的演示代码(为简单起见,省略了防御代码和错误处理程序)。只需复制&amp;粘贴到pipe_ipc_demo.cc然后编译&amp;进行测试。

#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <string>
#include <list>

template<class T> class MPSCQ { // pipe based Multi Producer Single Consumer Queue
public:
        MPSCQ();
        ~MPSCQ();
        int producerPush(const T* t); 
        T* consumerPoll(double timeout = 1.0);
private:
        void _consumeFd();
        int _selectFdConsumer(double timeout);
        T* _popFront();
private:
        int _fdProducer;
        int _fdConsumer;
        char* _consumerBuf;
        std::string* _partial;
        std::list<T*>* _list;
        static const int _PTR_SIZE;
        static const int _CONSUMER_BUF_SIZE;
};

template<class T> const int MPSCQ<T>::_PTR_SIZE = sizeof(void*);
template<class T> const int MPSCQ<T>::_CONSUMER_BUF_SIZE = 1024;

template<class T> MPSCQ<T>::MPSCQ() :
        _fdProducer(-1),
        _fdConsumer(-1) {
        _consumerBuf = new char[_CONSUMER_BUF_SIZE];
        _partial = new std::string;     // for holding partial pointer address
        _list = new std::list<T*>;      // unconsumed T* cache
        int fd_[2];
        int r = pipe(fd_);
        _fdConsumer = fd_[0];
        _fdProducer = fd_[1];
}


template<class T> MPSCQ<T>::~MPSCQ() { /* omitted */ }

template<class T> int MPSCQ<T>::producerPush(const T* t) {
        return t == NULL ? 0 : write(_fdProducer, &t, _PTR_SIZE);
}

template<class T> T* MPSCQ<T>::consumerPoll(double timeout) {
        T* t = _popFront();
        if (t != NULL) {
                return t;
        }
        if (_selectFdConsumer(timeout) <= 0) {  // timeout or error
                return NULL;
        }
        _consumeFd();
        return _popFront();
}

template<class T> void MPSCQ<T>::_consumeFd() {
        memcpy(_consumerBuf, _partial->data(), _partial->length());
        ssize_t r = read(_fdConsumer, _consumerBuf, _CONSUMER_BUF_SIZE - _partial->length());
        if (r <= 0) {   // EOF or error, error handler omitted
                return;
        }
        const char* p = _consumerBuf;
        int remaining_len_ = _partial->length() + r;
        T* t;
        while (remaining_len_ >= _PTR_SIZE) {
                memcpy(&t, p, _PTR_SIZE);
                _list->push_back(t);
                remaining_len_ -= _PTR_SIZE;
                p += _PTR_SIZE;
        }
        *_partial = std::string(p, remaining_len_);
}

template<class T> int MPSCQ<T>::_selectFdConsumer(double timeout) {
        int r;
        int nfds_ = _fdConsumer + 1;
        fd_set readfds_;
        struct timeval timeout_;
        int64_t usec_ = timeout * 1000000.0;
        while (true) {
                timeout_.tv_sec = usec_ / 1000000;
                timeout_.tv_usec = usec_ % 1000000;
                FD_ZERO(&readfds_);
                FD_SET(_fdConsumer, &readfds_);
                r = select(nfds_, &readfds_, NULL, NULL, &timeout_);
                if (r < 0 && errno == EINTR) {
                        continue;
                }
                return r;
        }
}

template<class T> T* MPSCQ<T>::_popFront() {
        if (!_list->empty()) {
                T* t = _list->front();
                _list->pop_front();
                return t;
        } else {
                return NULL;
        }
}

// = = = = = test code below = = = = =

#define _LOOP_CNT    5000000
#define _ONE_MILLION 1000000
#define _PRODUCER_THREAD_NUM 2

struct TestMsg {        // all public
        int _threadId;
        int _msgId;
        int64_t _val;
        TestMsg(int thread_id, int msg_id, int64_t val) :
                _threadId(thread_id),
                _msgId(msg_id),
                _val(val) { };
};

static MPSCQ<TestMsg> _QUEUE;
static int64_t _SUM = 0;

void* functor_producer(void* arg) {
        int my_thr_id_ = pthread_self();
        TestMsg* msg_;
        for (int i = 0; i <= _LOOP_CNT; ++ i) {
                if (i == _LOOP_CNT) {
                        msg_ = new TestMsg(my_thr_id_, i, -1);
                } else {
                        msg_ = new TestMsg(my_thr_id_, i, i + 1);
                }
                _QUEUE.producerPush(msg_);
        }
        return NULL;
}


void* functor_consumer(void* arg) {
        int msg_cnt_ = 0;
        int stop_cnt_ = 0;
        TestMsg* msg_;
        while (true) {
                if ((msg_ = _QUEUE.consumerPoll()) == NULL) {
                        continue;
                }
                int64_t val_ = msg_->_val;
                delete msg_;
                if (val_ <= 0) {
                        if ((++ stop_cnt_) >= _PRODUCER_THREAD_NUM) {
                                printf("All done, _SUM=%ld\n", _SUM);
                                break;
                        }
                } else {
                        _SUM += val_;
                        if ((++ msg_cnt_) % _ONE_MILLION == 0) {
                                printf("msg_cnt_=%d, _SUM=%ld\n", msg_cnt_, _SUM);
                        }
                }
        }
        return NULL;
}

int main(int argc, char* const* argv) {
        pthread_t consumer_;
        pthread_create(&consumer_, NULL, functor_consumer, NULL);
        pthread_t producers_[_PRODUCER_THREAD_NUM];
        for (int i = 0; i < _PRODUCER_THREAD_NUM; ++ i) {
                pthread_create(&producers_[i], NULL, functor_producer, NULL);
        }
        for (int i = 0; i < _PRODUCER_THREAD_NUM; ++ i) {
                pthread_join(producers_[i], NULL);
        }
        pthread_join(consumer_, NULL);
        return 0;
}

这是测试结果(2 * sum(1..5000000) == (1 + 5000000) * 5000000 == 25000005000000):

$ g++ -o pipe_ipc_demo pipe_ipc_demo.cc -lpthread
$ ./pipe_ipc_demo    ## output may vary except for the final _SUM
msg_cnt_=1000000, _SUM=251244261289
msg_cnt_=2000000, _SUM=1000708879236
msg_cnt_=3000000, _SUM=2250159002500
msg_cnt_=4000000, _SUM=4000785160225
msg_cnt_=5000000, _SUM=6251640644676
msg_cnt_=6000000, _SUM=9003167062500
msg_cnt_=7000000, _SUM=12252615629881
msg_cnt_=8000000, _SUM=16002380952516
msg_cnt_=9000000, _SUM=20252025092401
msg_cnt_=10000000, _SUM=25000005000000
All done, _SUM=25000005000000

此处显示的技术用于我们的生产应用程序。一种典型的用法是使用者线程充当日志写入器,而工作线程几乎可以异步地写入日志消息。是的, 几乎 表示有时写入线程可能会在管道已满时在write()中被阻止,这是操作系统提供的可靠拥塞控制功能。