簡體   English   中英

使用匿名管道是否會為線程間通信引入內存障礙?

[英]Does the use of an anonymous pipe introduce a memory barrier for interthread communication?

例如,假設我分配了一個帶有new的結構,並將指針寫入匿名管道的寫入端。

如果我從相應的讀取端讀取了指針,是否可以確保在結構上看到“正確”的內容?

同樣有趣的是,unix上的socketpair()和Windows上通過tcp環回進行自我連接的結果是否具有相同的保證。

上下文是一種服務器設計,可通過select / epoll集中進行事件分發

例如,假設我分配了一個帶有new的結構,並將指針寫入匿名管道的寫入端。

如果我從相應的讀取端讀取了指針,是否可以確保在結構上看到“正確”的內容?

不能。不能保證寫入CPU會將寫入內容從其緩存中清除掉,並使它對可能進行讀取的其他CPU可見。

同樣有趣的是,unix上的socketpair()和Windows上通過tcp環回進行自我連接的結果是否具有相同的保證。

沒有。

在實踐中,調用write() (這是系統調用)將最終鎖定內核中的一個或多個數據結構,這應解決重新排序問題。 例如,POSIX需要后續讀取才能看到在調用之前寫入的數據,這本身就意味着有鎖定(或某種獲取/釋放)。

至於這是否屬於電話正式說明的一部分,可能不是。

指針只是一個內存地址,因此, 如果您在同一進程上,則該指針將在接收線程上有效,並且指向同一結構。 如果您使用的是不同的進程,那么充其量您將立即獲得一個內存錯誤,更糟糕的是,您將讀取(或寫入)一個本質上是未定義行為的隨機內存。

您會閱讀正確的內容嗎? 無論指針是在兩個線程共享的靜態變量中,都沒有比這更好或更壞的了:如果要保持一致性,您仍然必須進行一些同步

靜態內存(由線程共享),匿名管道,套接字對,tcp環回等之間的傳輸地址類型是否重要? 否:所有這些通道都傳輸字節 ,因此,如果您傳遞一個內存地址,則將獲得您的內存地址。 然后剩下的就是同步,因為在這里您只是共享一個內存地址。

如果您不使用任何其他同步,則可能發生任何事情(我是否已經提到過未定義的行為?):

  • 讀取線程可以通過寫入一個過時的數據來在寫入之前訪問內存
  • 如果您忘記將struct成員聲明為volatile,則讀取線程可以繼續使用緩存的值,從而再次獲取陳舊的數據
  • 讀取線程可以讀取部分寫入的數據,這意味着數據不一致

到目前為止,有趣的問題是Cornstalks的一個正確答案。

在同一(多線程)進程中,由於指針和數據遵循不同的路徑到達目的地,因此無法保證。 隱式獲取/釋放保證不適用,因為結構數據無法通過緩存搭載在指針上,並且您正在正式處理數據爭用。

但是,查看指針和結構數據本身如何到達第二個線程(分別通過管道和內存高速緩存),則很有可能該機制不會造成任何危害。 將指針發送到對等線程需要3個系統調用(發送線程中的write() ,接收線程中的select()read() ),這(相對)昂貴,並且在接收時指針值可用時線程,結構數據可能早已到達。

請注意,這只是一個觀察,機制仍然不正確。

我相信,您的情況可能會簡化為以下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)

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

由於在消息大小小於PIPE_BUF (通常為4096個字節)時,保證write系統調用是原子的,因此多生產者線程不會弄亂彼此的對象地址(對於64位應用程序為8個字節)。

談話很便宜,這是Linux的演示代碼(為簡單起見,省略了防御性代碼和錯誤處理程序)。 只需復制並粘貼到pipe_ipc_demo.cc然后編譯並運行測試。

#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()被阻塞,這是OS提供的可靠的擁塞控制功能。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM