2012-01-18 9 views
25

जितना अधिक मैं उलझन में पढ़ता हूं, मैं बन जाता हूं ... मैंने सी ++ में लागू औपचारिक रूप से सही एमपीएससी कतार खोजने के लिए यह छोटा सा सोचा होगा।क्या सी ++ के लिए एक से अधिक उत्पादक एकल उपभोक्ता लॉक-फ्री कतार मौजूद है?

जब भी मैं उस पर फिर से वार मिल जाए, आगे अनुसंधान ऐसी ए.बी.ए. या अन्य सूक्ष्म दौड़ की स्थिति के रूप में मुद्दे हैं सुझाव है कि लगता है।

कचरा संग्रह की आवश्यकता के बारे में कई बात। यह कुछ है जिसे मैं टालना चाहता हूं।

क्या वहां कोई स्वीकार्य सही ओपन सोर्स कार्यान्वयन है?

+0

मेरी सलाह विकल्प तलाशना है। प्रदर्शन के लिए, मुफ्त कतार लॉक उस महान नहीं हैं। जब भी आपके पास एक ही कैश लाइन पर कई धागे लिखते हैं तो कुछ भी तेज करना संभव नहीं है। प्रत्येक निर्माता के लिए अलग एसपीएससी कतार का उपयोग करना तेज़ तरीका है। नकारात्मकता यह है कि आप वस्तुओं का क्रम खो देते हैं। अगर आपको एक आदेशित कतार की आवश्यकता है और खुद को बहुत सी सिरदर्द बचाने की इच्छा है, तो बस एक स्पिनलॉक का उपयोग करें। व्यवहार में यह लॉकफ्री के रूप में लगभग उतना ही अच्छा है और उदाहरण के मुकाबले सैकड़ों गुना तेज है Win32 महत्वपूर्ण खंड। – Timo

+0

क्या आपने कतार के बजाए रिंग बफर का उपयोग करने पर विचार किया था? कुछ प्रदर्शन लाभ हैं - स्लॉट आवंटित स्लॉट के साथ निश्चित मेमोरी लोकेशन, बहुत आसान काउंटर किसी भी लॉकिंग के साथ पूंछ/सिर को इंगित करने के लिए इंगित करते हैं, अनुमानित कैश-फ्रेंडली मेमोरी एक्सेस पैटर्न इत्यादि। – bronekk

+0

@bronekk क्या आप कृपया विस्तृत कर सकते हैं? क्या आपके पास किसी भी मौका से एक कामकाजी उदाहरण है? –

उत्तर

10

आप विघटनकर्ता को जांचना चाह सकते हैं; यह सी में उपलब्ध है ++ यहाँ: http://lmax-exchange.github.io/disruptor/

तुम भी स्पष्टीकरण प्राप्त कर सकते हैं यह कैसे काम करता here on stackoverflow मूल रूप से यह नहीं लॉकिंग, एक निश्चित-आकार स्लॉट में धागे के बीच फीफो संदेशों पारित करने के लिए अनुकूलित के साथ परिपत्र बफर है। Lock-free Multi-producer Multi-consumer Queue on Ring Buffer @ NatSys Lab. Blog और
Yet another implementation of a lock-free circular array queue @ CodeProject

नोट::

यहाँ दो कार्यान्वयन करना, जिससे मैं उपयोगी पाया हैं नीचे कोड गलत है, मैं इसे केवल एक उदाहरण है कि कैसे मुश्किल ये बातें हो सकता है के रूप में छोड़ दें।

यदि आपको Google संस्करण की जटिलता पसंद नहीं है, तो यहां कुछ ऐसा ही है - यह बहुत आसान है, लेकिन मैं इसे पाठक के लिए एक अभ्यास के रूप में छोड़ देता हूं (यह बड़ी परियोजना का हिस्सा है, नहीं इस समय पोर्टेबल)। संपूर्ण विचार डेटा के लिए परिपत्र बफर को बनाए रखना और लिखने/पढ़ने और पढ़ने/पढ़ने के लिए स्लॉट की पहचान करने के लिए काउंटरों के एक छोटे समूह को बनाए रखना है। चूंकि प्रत्येक काउंटर अपनी कैश लाइन में होता है, और (सामान्यतः) प्रत्येक संदेश पर लाइव होने पर केवल परमाणु रूप से अपडेट किया जाता है, इसलिए उन्हें बिना किसी सिंक्रनाइज़ेशन के पढ़ा जा सकता है। post_done में लेखन धागे के बीच एक संभावित विवाद बिंदु है, यह फीफो गारंटी के लिए आवश्यक है। काउंटर (head_, wrtn_, rdng_, tail_) शुद्धता और फीफो सुनिश्चित करने के लिए चयन किया गया था, इसलिए छोड़ने फीफो भी काउंटर के परिवर्तन की आवश्यकता होगी (और उस शुद्धता sacrifying के बिना ऐसा करने के लिए मुश्किल हो सकता है)।एक उपभोक्ता के साथ परिदृश्यों के प्रदर्शन में थोड़ा सुधार करना संभव है, लेकिन मुझे परेशान नहीं होगा - यदि आपको एकाधिक पाठकों के साथ अन्य उपयोग मामलों को पाया जाता है तो आपको इसे पूर्ववत करना होगा।

total=1000000 samples, avg=0.24us 
    50%=0.214us, avg=0.093us 
    90%=0.23us, avg=0.151us 
    99%=0.322us, avg=0.159us 
    99.9%=15.566us, avg=0.173us 

इन परिणामों एकल मतदान उपभोक्ता के लिए कर रहे हैं, यानी कार्यकर्ता धागा:

मेरी मशीन विलंबता पर ऐसा दिखाई देता है (बाएं पर प्रतिशतक, सही पर इस प्रतिशतक भीतर मतलब है, इकाई माइक्रोसेकंड, RDTSC द्वारा मापा है) तंग लूप में व्हील.read() को कॉल करना और खाली नहीं होने पर जांचना (उदाहरण के लिए नीचे स्क्रॉल करें)। प्रतीक्षा करने वाले उपभोक्ताओं (बहुत कम CPU उपयोग) घटना पर प्रतीक्षा करेंगे (acquire... फ़ंक्शंस में से एक), यह संदर्भ स्विच के कारण औसत विलंबता के बारे में 1-2us जोड़ता है।

चूंकि पढ़ने पर बहुत कम विवाद है, इसलिए उपभोक्ता थ्रेड की संख्या के साथ उपभोक्ताओं को बहुत अच्छी तरह से स्केल किया जाता है, उदाहरण के लिए मेरी मशीन पर 3 धागे के लिए:

while (wheel.active()) 
    { 
    core::wheel::wheel<int>::slot<false> slot = wheel.read(); 
    if (!slot.empty()) 
    { 
     Data& d = slot.cast<Data>(); 
     // do work 
    } 
    // uncomment below for waiting consumer, saving CPU cycles 
    // else 
    // wheel.try_acquire(10); 
    } 

संपादित जोड़ा उपभोक्ता:

total=1500000 samples, avg=0.07us 
    50%=0us, avg=0us 
    90%=0.155us, avg=0.016us 
    99%=0.361us, avg=0.038us 
    99.9%=8.723us, avg=0.044us 

पैच होगा स्वागत :)

// Copyright (c) 2011-2012, Bronislaw (Bronek) Kozicki 
// 
// Distributed under the Boost Software License, Version 1.0. (See accompanying 
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 

#pragma once 

#include <core/api.hxx> 
#include <core/wheel/exception.hxx> 

#include <boost/noncopyable.hpp> 
#include <boost/type_traits.hpp> 
#include <boost/lexical_cast.hpp> 
#include <typeinfo> 

namespace core { namespace wheel 
{ 
    struct bad_size : core::exception 
    { 
    template<typename T> explicit bad_size(const T&, size_t m) 
     : core::exception(std::string("Slot capacity exceeded, sizeof(") 
        + typeid(T).name() 
        + ") = " 
        + boost::lexical_cast<std::string>(sizeof(T)) 
        + ", capacity = " 
        + boost::lexical_cast<std::string>(m) 
       ) 
    {} 
    };   

    // inspired by Disruptor 
    template <typename Header> 
    class wheel : boost::noncopyable 
    { 
    __declspec(align(64)) 
    struct slot_detail 
    { 
     // slot write: (memory barrier in wheel) > post_done > (memory barrier in wheel) 
     // slot read: (memory barrier in wheel) > read_done > (memory barrier in wheel) 

     // done writing or reading, must update wrtn_ or tail_ in wheel, as appropriate 
     template <bool Writing> 
     void done(wheel* w) 
     { 
     if (Writing) 
      w->post_done(sequence); 
     else 
      w->read_done(); 
     } 

     // cache line for sequence number and header 
     long long sequence; 
     Header header; 

     // there is no such thing as data type with variable size, but we need it to avoid thrashing 
     // cache - so we invent one. The memory is reserved in runtime and we simply go beyond last element. 
     // This is well into UB territory! Using template parameter for this is not good, since it 
     // results in this small implementation detail leaking to all possible user interfaces. 
     __declspec(align(8)) 
     char data[8]; 
    }; 

    // use this as a storage space for slot_detail, to guarantee 64 byte alignment 
    _declspec(align(64)) 
    struct slot_block { long long padding[8]; }; 

    public: 
    // wrap slot data to outside world 
    template <bool Writable> 
    class slot 
    { 
     template<typename> friend class wheel; 

     slot& operator=(const slot&); // moveable but non-assignable 

     // may only be constructed by wheel 
     slot(slot_detail* impl, wheel<Header>* w, size_t c) 
     : slot_(impl) , wheel_(w) , capacity_(c) 
     {} 

    public: 
     slot(slot&& s) 
     : slot_(s.slot_) , wheel_(s.wheel_) , capacity_(s.capacity_) 
     { 
     s.slot_ = NULL; 
     } 

     ~slot() 
     { 
     if (slot_) 
     { 
      slot_->done<Writable>(wheel_); 
     } 
     } 

     // slot accessors - use Header to store information on what type is actually stored in data 
     bool empty() const   { return !slot_; } 
     long long sequence() const { return slot_->sequence; } 
     Header& header()   { return slot_->header; } 
     char* data()    { return slot_->data; } 

     template <typename T> T& cast() 
     { 
     static_assert(boost::is_pod<T>::value, "Data type must be POD"); 
     if (sizeof(T) > capacity_) 
      throw bad_size(T(), capacity_); 
     if (empty()) 
      throw no_data(); 
     return *((T*) data()); 
     } 

    private: 
     slot_detail* slot_; 
     wheel<Header>* wheel_; 
     const size_t capacity_; 
    }; 

    private: 
    // dynamic size of slot, with extra capacity, expressed in 64 byte blocks 
    static size_t sizeof_slot(size_t s) 
    { 
     size_t m = sizeof(slot_detail); 
     // add capacity less 8 bytes already within sizeof(slot_detail) 
     m += max(8, s) - 8; 
     // round up to 64 bytes, i.e. alignment of slot_detail 
     size_t r = m & ~(unsigned int)63; 
     if (r < m) 
     r += 64; 
     r /= 64; 
     return r; 
    } 

    // calculate actual slot capacity back from number of 64 byte blocks 
    static size_t slot_capacity(size_t s) 
    { 
     return s*64 - sizeof(slot_detail) + 8; 
    } 

    // round up to power of 2 
    static size_t round_size(size_t s) 
    { 
     // enfore minimum size 
     if (s <= min_size) 
     return min_size; 

     // find rounded value 
     --s; 
     size_t r = 1; 
     while (s) 
     { 
     s >>= 1; 
     r <<= 1; 
     }; 
     return r; 
    } 

    slot_detail& at(long long sequence) 
    { 
     // find index from sequence number and return slot at found index of the wheel 
     return *((slot_detail*) &wheel_[(sequence & (size_ - 1)) * blocks_]); 
    } 

    public: 
    wheel(size_t capacity, size_t size) 
     : head_(0) , wrtn_(0) , rdng_(0) , tail_(0) , event_() 
     , blocks_(sizeof_slot(capacity)) , capacity_(slot_capacity(blocks_)) , size_(round_size(size)) 
    { 
     static_assert(boost::is_pod<Header>::value, "Header type must be POD"); 
     static_assert(sizeof(slot_block) == 64, "This was unexpected"); 

     wheel_ = new slot_block[size_ * blocks_]; 
     // all slots must be initialised to 0 
     memset(wheel_, 0, size_ * 64 * blocks_); 
     active_ = 1; 
    } 

    ~wheel() 
    { 
     stop(); 
     delete[] wheel_; 
    } 

    // all accessors needed 
    size_t capacity() const { return capacity_; } // capacity of a single slot 
    size_t size() const  { return size_; }  // number of slots available 
    size_t queue() const { return (size_t)head_ - (size_t)tail_; } 
    bool active() const  { return active_ == 1; } 

    // enough to call it just once, to fine tune slot capacity 
    template <typename T> 
    void check() const 
    { 
     static_assert(boost::is_pod<T>::value, "Data type must be POD"); 
     if (sizeof(T) > capacity_) 
     throw bad_size(T(), capacity_); 
    } 

    // stop the wheel - safe to execute many times 
    size_t stop() 
    { 
     InterlockedExchange(&active_, 0); 
     // must wait for current read to complete 
     while (rdng_ != tail_) 
     Sleep(10); 

     return size_t(head_ - tail_); 
    } 

    // return first available slot for write 
    slot<true> post() 
    { 
     if (!active_) 
     throw stopped(); 

     // the only memory barrier on head seq. number we need, if not overflowing 
     long long h = InterlockedIncrement64(&head_); 
     while(h - (long long) size_ > tail_) 
     { 
     if (InterlockedDecrement64(&head_) == h - 1) 
      throw overflowing(); 

     // protection against case of race condition when we are overflowing 
     // and two or more threads try to post and two or more messages are read, 
     // all at the same time. If this happens we must re-try, otherwise we 
     // could have skipped a sequence number - causing infinite wait in post_done 
     Sleep(0); 
     h = InterlockedIncrement64(&head_); 
     } 

     slot_detail& r = at(h); 
     r.sequence = h; 

     // wrap in writeable slot 
     return slot<true>(&r, this, capacity_); 
    } 

    // return first available slot for write, nothrow variant 
    slot<true> post(std::nothrow_t) 
    { 
     if (!active_) 
     return slot<true>(NULL, this, capacity_); 

     // the only memory barrier on head seq. number we need, if not overflowing 
     long long h = InterlockedIncrement64(&head_); 
     while(h - (long long) size_ > tail_) 
     { 
     if (InterlockedDecrement64(&head_) == h - 1) 
      return slot<true>(NULL, this, capacity_); 

     // must retry if race condition described above 
     Sleep(0); 
     h = InterlockedIncrement64(&head_); 
     } 

     slot_detail& r = at(h); 
     r.sequence = h; 

     // wrap in writeable slot 
     return slot<true>(&r, this, capacity_); 
    } 

    // read first available slot for read 
    slot<false> read() 
    { 
     slot_detail* r = NULL; 
     // compare rdng_ and wrtn_ early to avoid unnecessary memory barrier 
     if (active_ && rdng_ < wrtn_) 
     { 
     // the only memory barrier on reading seq. number we need 
     const long long h = InterlockedIncrement64(&rdng_); 
     // check if this slot has been written, step back if not 
     if (h > wrtn_) 
      InterlockedDecrement64(&rdng_); 
     else 
      r = &at(h); 
     } 

     // wrap in readable slot 
     return slot<false>(r , this, capacity_); 
    } 

    // waiting for new post, to be used by non-polling clients 
    void acquire() 
    { 
     event_.acquire(); 
    } 

    bool try_acquire() 
    { 
     return event_.try_acquire(); 
    } 

    bool try_acquire(unsigned long timeout) 
    { 
     return event_.try_acquire(timeout); 
    } 

    void release() 
    {} 

    private: 
    void post_done(long long sequence) 
    { 
     const long long t = sequence - 1; 

     // the only memory barrier on written seq. number we need 
     while(InterlockedCompareExchange64(&wrtn_, sequence, t) != t) 
     Sleep(0); 

     // this is outside of critical path for polling clients 
     event_.set(); 
    } 

    void read_done() 
    { 
     // the only memory barrier on tail seq. number we need 
     InterlockedIncrement64(&tail_); 
    } 

    // each in its own cache line 
    // head_ - wrtn_ = no. of messages being written at this moment 
    // rdng_ - tail_ = no. of messages being read at the moment 
    // head_ - tail_ = no. of messages to read (including those being written and read) 
    // wrtn_ - rdng_ = no. of messages to read (excluding those being written or read) 
    __declspec(align(64)) volatile long long head_; // currently writing or written 
    __declspec(align(64)) volatile long long wrtn_; // written 
    __declspec(align(64)) volatile long long rdng_; // currently reading or read 
    __declspec(align(64)) volatile long long tail_; // read 
    __declspec(align(64)) volatile long active_; // flag switched to 0 when stopped 

    __declspec(align(64)) 
    api::event event_;   // set when new message is posted 
    const size_t blocks_;  // number of 64-byte blocks in a single slot_detail 
    const size_t capacity_;  // capacity of data() section per single slot. Initialisation depends on blocks_ 
    const size_t size_;   // number of slots available, always power of 2 
    slot_block* wheel_; 
    }; 
}} 

यहाँ मतदान उपभोक्ता कार्यकर्ता धागे की तरह लग सकता है है उदाहरण

+0

कृपया आप बता सकते हैं कि शीर्षलेख और डेटा क्या हैं/अंतर क्या हैं? अगर मैं प्रत्येक स्लॉट में 3 * 64 बिट शब्दों को स्टोर करना चाहता हूं (जो कि पेलोड की पूरी बात है) मैं इसका उपयोग कैसे करूं? –

0

मैं ऐसी कोई बात अनुमान लगा रहा हूँ मौजूद है - और अगर यह होता है, यह या तो पोर्टेबल नहीं है या खुला स्रोत नहीं है।

संकल्पनात्मक रूप से, आप दो पॉइंटर्स को एक साथ नियंत्रित करने की कोशिश कर रहे हैं: tail पॉइंटर और tail->next सूचक। यह आमतौर पर केवल लॉक-मुक्त प्राइमेटिव के साथ नहीं किया जा सकता है।

+0

आपका अनुमान गलत है। निर्माता को सिर्फ पूंछ को स्थानांतरित करने की आवश्यकता है। जो आप वर्णन कर रहे हैं वह एक घुसपैठ कतार है। उस स्थिति में, आप पूंछ-> अगली और फिर परमाणु रूप से पूंछ को स्थानांतरित कर सकते हैं। यदि आप सफल नहीं हुए, तो फिर से लूप करें। – edwinc

3

सबसे उपयुक्त कार्यान्वयन एक कतार के वांछित गुणों पर निर्भर करता है। क्या यह असंबद्ध होना चाहिए या बाध्य होना चाहिए ठीक है? क्या यह linearizable होना चाहिए, या कम सख्त आवश्यकताएं ठीक होंगी? आपको कितनी मजबूत फीफो गारंटी है? क्या आप उपभोक्ता द्वारा सूची को वापस करने की लागत का भुगतान करने के इच्छुक हैं (वहां एक बहुत ही सरल कार्यान्वयन मौजूद है जहां उपभोक्ता एक एकल-लिंक्ड सूची की पूंछ पकड़ लेता है, इस प्रकार उत्पादकों द्वारा इस समय तक सभी वस्तुओं को एक बार प्राप्त किया जाता है)? क्या यह गारंटी देनी चाहिए कि कोई धागा कभी अवरुद्ध नहीं होता है, या कुछ थ्रेड अवरुद्ध करने के छोटे अवसर ठीक हैं? और आदि

कुछ उपयोगी लिंक्स:
Is multiple-producer, single-consumer possible in a lockfree setting?
http://www.1024cores.net/home/lock-free-algorithms/queues
http://www.1024cores.net/home/lock-free-algorithms/queues/intrusive-mpsc-node-based-queue
https://groups.google.com/group/comp.programming.threads/browse_frm/thread/33f79c75146582f3

आशा है कि मदद करता है।

+1

मैंने दिमित्री व्याकोव के कार्यान्वयन के आधार पर एक संस्करण बनाया: https://github.com/samanbarghi/MPSCQ/ –

0

नीचे तकनीक है मैंने अपने सहकारी मल्टी-टास्किंग/मल्टी-थ्रेडिंग लाइब्रेरी (मैसे) http://bytemaster.github.com/mace/ के लिए उपयोग किया। कतार खाली होने के अलावा इसे लॉक-फ्री होने का लाभ है।

struct task { 
    boost::function<void()> func; 
    task* next; 
}; 


boost::mutex      task_ready_mutex; 
boost::condition_variable  task_ready; 
boost::atomic<task*>    task_in_queue; 

// this can be called from any thread 
void thread::post_task(task* t) { 
    // atomically post the task to the queue. 
    task* stale_head = task_in_queue.load(boost::memory_order_relaxed); 
    do { t->next = stale_head; 
    } while(!task_in_queue.compare_exchange_weak(stale_head, t, boost::memory_order_release)); 

    // Because only one thread can post the 'first task', only that thread will attempt 
    // to aquire the lock and therefore there should be no contention on this lock except 
    // when *this thread is about to block on a wait condition. 
    if(!stale_head) { 
     boost::unique_lock<boost::mutex> lock(task_ready_mutex); 
     task_ready.notify_one(); 
    } 
} 

// this is the consumer thread. 
void process_tasks() { 
    while(!done) { 
    // this will atomically pop everything that has been posted so far. 
    pending = task_in_queue.exchange(0,boost::memory_order_consume); 
    // pending is a linked list in 'reverse post order', so process them 
    // from tail to head if you want to maintain order. 

    if(!pending) { // lock scope 
     boost::unique_lock<boost::mutex> lock(task_ready_mutex);     
     // check one last time while holding the lock before blocking. 
     if(!task_in_queue) task_ready.wait(lock); 
    } 
} 
+0

मेरा मानना ​​है कि यह लंबित होना चाहिए = task_in_queue.exchange (0, boost :: memory_order_acquire); चूंकि आईएसओ सी ++ 11 मानक में कहा गया है 29.3.2 "एक परमाणु ऑपरेशन ए जो परमाणु ऑब्जेक्ट पर रिलीज ऑपरेशन करता है एम एक परमाणु ऑपरेशन बी के साथ सिंक्रनाइज़ करता है जो एम पर अधिग्रहण ऑपरेशन करता है और किसी भी तरफ से अपना मूल्य लेता है रिलीज अनुक्रम में प्रभाव ए के नेतृत्व में " – ipapadop

+0

मेमोरी आवंटन/पुनर्विचार के कारण "कतार" खाली नहीं होने पर मैं इसे लॉक मुक्त नहीं कहूंगा –

संबंधित मुद्दे

 संबंधित मुद्दे