साइकिल से न डरें। या C ++ 11 में एक और ग्रैंड सेंट्रल डिस्पैच (GCD)

IMHO (मेरे पास एक ओपिनियन हॉर्सरैडिश प्रतियोगिता है)


मेरे दृष्टिकोण से, एक प्रोग्रामर अपने पेशेवर स्तर को बेहतर बनाने के लिए एक सबसे उपयोगी चीज कर सकता है वह है बाइक लिखना। साइकिल चलाना एक बहुत ही रोमांचक प्रक्रिया है। कभी-कभी यह उस कार्य से अधिक वहन करता है जिसके लिए बाइक ने स्वयं शुरू किया था। साइकिल लिखते समय (साइकिल से, मेरा मतलब है कि मौजूदा का कार्यान्वयन), मौजूदा समाधान और तकनीकों की गहरी समझ होती है।


प्रेरणा


तीन साल से अधिक समय से, मेरी मुख्य कामकाजी भाषा ऑब्जेक्टिव-सी रही है, और जब मैंने पहली बार इस पर लिखना शुरू किया था, तो मैंने बहु- प्रचारक NSOperationQueue के साथ काम करने के लिए उच्च-स्तरीय API द्वारा सुखद आश्चर्यचकित किया था, और बाद में एमसीडी द्वारा, जो मेरी राय में निर्णायकता और थ्रेडिबिलिटी फॉर थ्रेडिबिलिटी की समझ है। संगामिति। और यहाँ Habré पर हाल के लेख हैं: C ++ के लिए C # और थ्रेड कंसीडर C ++ 11 से प्रतीक्षा / async का एनालॉग लिखने के लिए तकनीक । उन्होंने उन नए अच्छाइयों को देखने के लिए मजबूर किया जिन्हें C ++ मल्टीथ्रेडिंग के साथ काम करने के लिए प्रदान करता है। और उनमें से ज्यादातर (एक ही एसडी :: भविष्य) मेरे लिए कुछ इस तरह दिखते हैं:


अटकलें और विशलिस्ट


यहाँ एक विशिष्ट परिदृश्य है जिसमें मैं अपने अनुप्रयोगों में मल्टीथ्रेडिंग का उपयोग करता हूं:

आसानी से, इनमें से प्रत्येक ऑपरेशन के लिए अपनी बारी है।
और इससे भी अधिक सुविधाजनक जब यह सब एक ही स्थान पर एकत्र किया जाता है, और पाँच स्रोत फ़ाइलों में बिखरा हुआ नहीं है। कुछ इस तरह:
file_io_queue.async([=]{ file_data = get_data_from_file( file_name ); parser_queue.async([=]{ parsed_data = parse( file_data ); main_queue.async([=]{ update_ui_with_new_data( parsed_data ) ; }); }); }); 
यह कोड पूरी तरह से रैखिक, तुल्यकालिक कोड की तरह पढ़ता है। यह डेटा परिवर्तन कैसे होगा के तर्क का वर्णन करता है। मेरे लिए, बड़े और, यह मायने नहीं रखता कि फ़ाइल को किस थ्रेड में पढ़ा जाएगा, जिसमें - इसकी पार्सिंग। मुख्य बात इन ऑपरेशनों का अनुक्रम है। मैं पिछले कोड को 100500 फाइलों के लिए 100500 बार कॉल कर सकता हूं।

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


कोड


आइए शुरू:
बारी
 namespace dispatch{ typedef std::function<void ()> function; struct queue { typedef long priority; //  .      const queue::priority queue_priority; static std::shared_ptr<queue> main_queue() ; //    virtual void async(function) const; //        queue(queue::priority priority) : queue_priority(priority) {}; }; } 

Async विधि कार्यान्वयन
थ्रेड पूल पर कॉल को पुनर्निर्देशित करता है:
  void queue::async(dispatch::function task) const { thread_pool::shared_pool()->push_task_with_priority(task, this->queue_priority); }; 

सारा काम हमारे यहां ही होगा
थ्रेड पूल:
  struct queue_impl{ const queue::priority priority; std::queue<function> tasks; bool is_running; queue_impl(queue::priority priority): priority(priority){}; }; struct thread_pool{ thread_pool(); static std::shared_ptr<thread_pool>& shared_pool(); // thread_pool virtual ~thread_pool(); bool stop; typedef std::shared_ptr<queue_impl> queue_ptr; void push_task_with_priority(const function&, queue::priority);//       bool get_free_queue(queue_ptr*) const; //  ,        void start_task_in_queue(const queue_ptr&); //     void stop_task_in_queue(const queue_ptr&); //   std::mutex mutex; //     std::map<queue::priority, queue_ptr> queues; //      std::mutex main_thread_mutex; std::queue<dispatch::function> main_queue; std::condition_variable condition; std::vector<std::thread> threads; //   ,      dispatch::function main_loop_need_update; void add_worker(); //     }; 

क्रम में तरीकों पर विचार करें। हमें सर्वोच्च प्राथमिकता के साथ एक मुफ्त कतार खोजने की आवश्यकता है:
उच्चतम प्राथमिकता के साथ मुक्त कतार खोजें:
  bool thread_pool::get_free_queue(queue_ptr* out_queue) const { //           auto finded = std::find_if(queues.rbegin(), queues.rend(), [](const std::pair<queue::priority, queue_ptr>& iter){ return ! iter.second->is_running; //      }); bool is_free_queue_exist = (finded != queues.rend()); if (is_free_queue_exist) *out_queue = finded->second; return is_free_queue_exist; } 

कार्य को कतार में जोड़ें
  void thread_pool::push_task_with_priority(const function& task, queue::priority priority){ { std::unique_lock<std::mutex> lock(mutex); //   //    .    -   auto queue = queues[priority]; if (!queue){ queue = std::make_shared<dispatch::queue_impl>(priority); queues[priority] = queue; } queue->tasks.push(task); //  ,    unsigned max_number_of_threads = std::max<unsigned>(std::thread::hardware_concurrency(), 2); unsigned number_of_threads_required = round(log(queues.size()) + 1); while (threads.size() < std::min<unsigned>(max_number_of_threads, number_of_threads_required)){ add_worker(); } } condition.notify_one(); //  ,       } 

कार्य को पूर्ण के रूप में चिह्नित करें
  void thread_pool::stop_task_in_queue(const queue_ptr& queue){ { std::unique_lock<std::mutex> lock(mutex); //    .    -      queue->is_running = false; if ( queue->tasks.size() ==0 ){ queues.erase(queues.find(queue->queue_priority)); } } condition.notify_one(); //  ,      } 

और, वास्तव में, धारा ही:
  void thread_pool::add_worker(){ threads.push_back(std::thread([=]{ dispatch::function task; thread_pool::queue_ptr queue; while(true){ { std::unique_lock<std::mutex> lock(mutex); //    while(!stop && !get_free_queue(&queue)) //     condition.wait(lock); //    if(stop) //     ,   return; task = queue->tasks.front(); //     queue->tasks.pop(); start_task_in_queue(queue); //     } task(); //   stop_task_in_queue(queue); //     } })); } 


मुख्य थ्रेड और रन लूप




C ++ में मुख्य थ्रेड जैसी कोई चीज नहीं है। लेकिन लगभग सभी यूआई एप्लिकेशन इस अवधारणा पर बनाए गए हैं। यूआई हम केवल मुख्य धागे से बदल सकते हैं। तो, हमें या तो स्वयं रन लूप को व्यवस्थित करने की आवश्यकता है, या खुद को मौजूदा में ही जाग्रत करना है।

सबसे पहले, "मुख्य सूत्र" के लिए एक अलग कतार बनाएँ:
मुख्य कतार
  struct main_queue : queue{ virtual void async(dispatch::function task) const override; main_queue(): queue(0) {}; }; std::shared_ptr<queue> queue::main_queue(){ return std::static_pointer_cast<dispatch::queue>(std::make_shared<dispatch::main_queue>()); } 

और async विधि में, हम कार्यों को जोड़ देंगे
अलग लाइन
  void main_queue::async(dispatch::function task) const { auto pool = thread_pool::shared_pool(); std::unique_lock<std::mutex> lock(pool->main_thread_mutex); pool->main_queue.push(task); if (pool->main_loop_need_update != nullptr) pool->main_loop_need_update(); } 

ठीक है, हमें एक फ़ंक्शन की आवश्यकता है जिसे मुख्य धागे से बुलाया जाएगा:
कोड
  void process_main_loop() { auto pool = thread_pool::shared_pool(); std::unique_lock<std::mutex> lock(pool->main_thread_mutex); while (!pool->main_queue.empty()) { auto task = pool->main_queue.front(); pool->main_queue.pop(); task(); } } 



अब केवल दो प्रश्न: "कैसे?" और "क्यों?"


सबसे पहले, "क्यों?": C ++ का उपयोग अक्सर क्रॉस-प्लेटफ़ॉर्म सॉफ़्टवेयर लिखने के लिए किया जाता है। पोर्टेबिलिटी के लिए, कई सुविधाजनक चीजों को छोड़ देना चाहिए। जीसीडी एक बहुत ही सुविधाजनक पुस्तकालय है जो अतुल्यकालिक कतारों का प्रबंधन करने के लिए एक सरल, सहज और सुविधाजनक तरीका प्रदान करता है।
प्रश्न "कैसे?" कोई निश्चित उत्तर नहीं है। आप अलग-अलग तरीकों से रैनअप को छेड़ सकते हैं। कई सिस्टम इसके लिए एक एपीआई प्रदान करते हैं। उदाहरण के लिए, iOS में "PerformSelectorOnMainThread:" है। हमें केवल कॉलबैक को प्रेषण के माध्यम से सेट करना होगा :: set_main_loop_process_callback:
 -(void)dispatchMainThread{ dispatch::process_main_loop(); } - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{ dispatch::set_main_loop_process_callback([=]{ [self performSelectorOnMainThread:@selector(dispatchMainThread) withObject:nil waitUntilDone:NO]; }); return YES; } 

यदि हम स्वयं अपने स्वयं के अनुष्ठान का आयोजन करते हैं, तो हम ऐसा कुछ कर सकते हैं:
  void main_loop(dispatch::function main_loop_function); void main_loop(dispatch::function main_loop_function){ auto main_queue = queue::main_queue(); while (!thread_pool::shared_pool()->stop) { main_queue->async(main_loop_function); process_main_loop(); } } 



और अब, वास्तव में, इसकी कल्पना के लिए:


6 कतार बनाएँ और प्रत्येक में 6 कार्य रटना:
  auto main_thread_id = std::this_thread::get_id(); for (unsigned task = 0; task < 6; ++task) for (unsigned priority = 0; priority < 6; ++priority){ dispatch::queue(priority).async([=]{ assert(std::this_thread::get_id() != main_thread_id); std::string task_string = std::to_string(task); std::string palceholder(1+priority*5, ' '); dispatch::queue::main_queue()->async([=]{ assert(std::this_thread::get_id() == main_thread_id); std::cout << palceholder << task_string << std::endl; }); }); } 

हम इस तस्वीर के बारे में
 0 1 0 0 2 1 1 3 2 2 4 3 3 5 4 4 0 5 5 1 0 0 2 1 1 3 2 2 4 3 3 5 4 4 5 5 

"कॉलम" एक कतार है। जितना अधिक दाईं ओर, कतार की प्राथमिकता उतनी ही अधिक होगी। लाइन मुख्य धारा कॉलबैक है।

खैर, iOS के लिए कोड:
  for (int i = 0; i < 20; ++i){ dispatch::queue(dispatch::QUEUE_PRIORITY::DEFAULT).async([=]{ NSAssert(![NSThread isMainThread], nil); std::string first_string = std::to_string(i); dispatch::queue::main_queue()->async([=]{ NSAssert([NSThread isMainThread], nil); std::string second_string = std::to_string(i+1); std::cout << first_string << " -> " << second_string << std::endl; [self.tableView reloadData]; //  -  UI. ,        }); }); } 


निष्कर्ष


कोई निष्कर्ष नहीं निकलेगा। इस बाइक को पूरी तरह से C ++ 11 में मल्टीथ्रेडिंग के परीक्षण के उद्देश्य से लिखा गया था। कोड सिर्फ बहुत अच्छी सी ++ कोड की 200 लाइनों से अधिक है, जिसे गीथब पर प्रस्तुत किया गया है । यह क्लैंग ++ 3.3, जी ++ - 4.7 / जी ++ - 4.8 और 2012 विज़ुअल स्टूडियो कंपाइलर पर परीक्षण किया गया था। यही है, मुख्य संकलक पहले से ही सी ++ 11 का पर्याप्त समर्थन करते हैं।

Py.Sy. अपनी बाइक लिखने के लिए कॉल करते हुए, मैं उनसे सैन्य परियोजनाओं पर उपयोग करने का आग्रह नहीं करता। हालांकि, दूसरी ओर, साइकिल कैसे कुछ गंभीर में बदल सकती है?

खैर, और साइकिल के एक जोड़े, जो मैंने नहीं सोचा था कि लेख में कहां से किनारा करना है


Source: https://habr.com/ru/post/In186200/


All Articles