कैसे पता करें कि रूबी में धागा सुरक्षित नहीं है?


93

रेल 4 से शुरू , सब कुछ डिफ़ॉल्ट रूप से थ्रेडेड वातावरण में चलाना होगा। इसका मतलब यह है कि हमारे द्वारा लिखे गए सभी कोड और हमारे द्वारा उपयोग किए जाने वाले सभी रत्न आवश्यक हैंthreadsafe

इसलिए, इस पर मेरे कुछ सवाल हैं:

  1. रूबी / रेल में धागा-सुरक्षित क्या नहीं है? बनाम क्या रूबी / रेल में धागा सुरक्षित है?
  2. क्या रत्नों की एक सूची है जिसे थ्रेडसेफ़ या इसके विपरीत जाना जाता है?
  3. क्या कोड के सामान्य पैटर्न की सूची है जो थ्रेडसेफ़ उदाहरण नहीं हैं @result ||= some_method?
  4. क्या रूबी लैंग कोर में डेटा संरचनाएं Hashआदि जैसे थ्रेडसेफ़ हैं?
  5. एमआरआई पर, जहां एक GVL/GIL जिसका मतलब है कि केवल 1 रूबी धागा एक समय पर चल सकता है IO, सिवाय इसके कि क्या थ्रेडसेफ़ हमें प्रभावित करता है?

2
क्या आप सुनिश्चित हैं कि सभी कोड और सभी रत्न थ्रेडसेफ़ होंगे? रिलीज नोट्स का कहना है कि रेल खुद
थ्रेडसफ़े

बहु-थ्रेडेड परीक्षण सबसे खराब संभव थ्रेडसेफ़ जोखिम होगा। जब आपको अपने परीक्षण मामले के आसपास एक पर्यावरण चर के मूल्य को बदलना होगा, तो आप तुरंत थ्रेडसेफ़ नहीं हैं। आप उसके आसपास कैसे काम करेंगे? और हाँ, सभी रत्नों को थ्रेडसेफ़ होना चाहिए।
लुकास ओबरहुबेर

जवाबों:


110

कोर डेटा संरचनाओं में से कोई भी धागा सुरक्षित नहीं है। रूबी के साथ जहाजों के बारे में केवल एक ही मुझे पता है कि मानक पुस्तकालय ( require 'thread'; q = Queue.new) में कतार कार्यान्वयन है ।

MRI की GIL हमें थ्रेड सेफ्टी के मुद्दों से नहीं बचाती है। यह केवल यह सुनिश्चित करता है कि दो धागे एक ही समय में रूबी कोड नहीं चला सकते हैं , यानी एक ही समय में दो अलग-अलग सीपीयू पर। आपके कोड में किसी भी बिंदु पर थ्रेड्स को रोका जा सकता है और फिर से शुरू किया जा सकता है। यदि आप कोड लिखते हैं @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }जैसे कि एक साझा चर को कई थ्रेड से बदलना, बाद में साझा किए गए चर का मान नियतात्मक नहीं है। जीआईएल कमोबेश सिंगल कोर सिस्टम का अनुकरण है, यह सही समवर्ती कार्यक्रमों को लिखने के बुनियादी मुद्दों को नहीं बदलता है।

यहां तक ​​कि अगर MRI को Node.js की तरह सिंगल-थ्रेडेड किया गया था, तब भी आपको कॉन्सेप्ट के बारे में सोचना होगा। वृद्ध चर के साथ उदाहरण ठीक काम करेगा, लेकिन आप अभी भी दौड़ की स्थिति प्राप्त कर सकते हैं जहां चीजें गैर-नियतात्मक क्रम में होती हैं और एक कॉलबैक दूसरे का परिणाम होता है। सिंगल थ्रेडेड एसिंक्रोनस सिस्टम के बारे में तर्क करना आसान है, लेकिन वे समसामयिक मुद्दों से मुक्त नहीं हैं। एक से अधिक उपयोगकर्ताओं के साथ एक एप्लिकेशन के बारे में सोचें: यदि दो उपयोगकर्ता एक ही बार में एक स्टैक ओवरफ्लो पोस्ट पर एडिट हिट करते हैं, तो पोस्ट को संपादित करने में कुछ समय व्यतीत करें और फिर सेव को हिट करें, जिसके परिवर्तन को तीसरे उपयोगकर्ता द्वारा बाद में देखा जाएगा जब वे उसी पोस्ट को पढ़ें?

रूबी में, अधिकांश अन्य समवर्ती रनटाइम के रूप में, जो कुछ भी एक से अधिक ऑपरेशन है, वह थ्रेड सुरक्षित नहीं है। @n += 1यह थ्रेड सुरक्षित नहीं है, क्योंकि यह कई ऑपरेशन हैं। @n = 1थ्रेड सुरक्षित है क्योंकि यह एक ऑपरेशन है (यह हुड के तहत बहुत सारे ऑपरेशन हैं, और मैं शायद मुश्किल में पड़ जाऊंगा अगर मैंने यह वर्णन करने की कोशिश की कि यह "थ्रेड सेफ" क्यों है विस्तार से, लेकिन अंत में आपको असाइनमेंट से असंगत परिणाम नहीं मिलेगा )। @n ||= 1, नहीं है और कोई अन्य शॉर्टहैंड ऑपरेशन + असाइनमेंट नहीं है। एक गलती जो मैंने कई बार की है return unless @started; @started = true, वह लिख रहा है , जो बिल्कुल भी सुरक्षित नहीं है।

मुझे रूबी के लिए थ्रेड सेफ और नॉन-थ्रेड सेफ स्टेटमेंट्स की किसी भी आधिकारिक सूची का पता नहीं है, लेकिन अंगूठे का एक सरल नियम है: यदि कोई एक्सप्रेशन केवल एक (साइड-इफ़ेक्ट फ्री) ऑपरेशन करता है तो वह थ्रेड सुरक्षित है। उदाहरण के लिए: a + bठीक है, ठीक a = bभी है, और a.foo(b)ठीक है, अगर विधि fooसाइड-इफ़ेक्ट फ़्री है (चूँकि रूबी में कुछ भी एक विधि कॉल है, तो कई मामलों में असाइनमेंट भी, यह अन्य उदाहरणों के लिए भी जाता है)। इस संदर्भ में साइड-इफेक्ट्स का मतलब उन चीजों से है जो राज्य बदलती हैं। def foo(x); @x = x; endहै पक्ष प्रभाव मुक्त।

रूबी में थ्रेड सेफ कोड लिखने के बारे में सबसे कठिन चीजों में से एक यह है कि सरणी, हैश और स्ट्रिंग सहित सभी मुख्य डेटा संरचनाएं परस्पर भिन्न हैं। गलती से अपने राज्य के एक टुकड़े को लीक करना बहुत आसान है, और जब वह टुकड़ा उत्परिवर्तित होता है तो वास्तव में खराब हो सकता है। निम्नलिखित कोड पर विचार करें:

class Thing
  attr_reader :stuff

  def initialize(initial_stuff)
    @stuff = initial_stuff
    @state_lock = Mutex.new
  end

  def add(item)
    @state_lock.synchronize do
      @stuff << item
    end
  end
end

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

एक और क्लासिक रूबी उदाहरण यह है:

STANDARD_OPTIONS = {:color => 'red', :count => 10}

def find_stuff
  @some_service.load_things('stuff', STANDARD_OPTIONS)
end

find_stuffपहली बार उपयोग किए जाने पर ठीक काम करता है, लेकिन दूसरी बार कुछ और देता है। क्यों? load_thingsविधि यह यह करने के लिए पारित कर दिया विकल्पों हैश मालिक है, और करता है सोचने के लिए होता है color = options.delete(:color)। अब STANDARD_OPTIONSस्थिरांक का समान मान नहीं है। कॉन्स्टेंट केवल उसी चीज में स्थिर होते हैं जिसका वे संदर्भ लेते हैं, वे उन डेटा संरचनाओं की निरंतरता की गारंटी नहीं देते हैं जिन्हें वे संदर्भित करते हैं। जरा सोचिए अगर यह कोड समवर्ती रूप से चलाया जाता तो क्या होता।

यदि आप साझा किए गए उत्परिवर्तित स्थिति से बचते हैं (उदाहरण के लिए कई थ्रेड द्वारा एक्सेस की गई वस्तुओं में उदाहरण चर, हैश जैसी डेटा संरचनाएं और एकाधिक थ्रेड द्वारा एक्सेस किए गए थ्रेड्स) थ्रेड सुरक्षा इतना कठिन नहीं है। अपने एप्लिकेशन के उन हिस्सों को कम से कम करने की कोशिश करें जो समवर्ती रूप से एक्सेस किए जाते हैं, और वहां अपने प्रयासों को केंद्रित करें। IIRC, एक रेल अनुप्रयोग में, हर अनुरोध के लिए एक नया नियंत्रक ऑब्जेक्ट बनाया जाता है, इसलिए यह केवल एक ही धागे का उपयोग करने वाला होता है, और वही उस नियंत्रक से आपके द्वारा बनाए गए किसी भी मॉडल ऑब्जेक्ट के लिए जाता है। हालांकि, रेल वैश्विक चर (वैश्विक चर User.find(...)का उपयोग करता है) के उपयोग को भी प्रोत्साहित करती हैUser, आप इसे केवल एक वर्ग के रूप में सोच सकते हैं, और यह एक वर्ग है, लेकिन यह वैश्विक चर के लिए एक नाम स्थान भी है), इनमें से कुछ सुरक्षित हैं क्योंकि वे केवल पढ़े जाते हैं, लेकिन कभी-कभी आप इन वैश्विक चर में चीजों को सहेजते हैं क्योंकि यह सुविधाजनक है। जब आप ऐसी किसी भी चीज़ का उपयोग करें, जो विश्व स्तर पर सुलभ हो।

यह संभव है कि थ्रेडेड वातावरण में रेल को थोड़ी देर के लिए चलाया जाए, इसलिए बिना रेल विशेषज्ञ के मैं अभी भी इतना ही कहूंगा कि जब आप रेल की बात करेंगे तो आपको थ्रेड सेफ्टी की चिंता नहीं करनी चाहिए। आप अभी भी रेल एप्लिकेशन बना सकते हैं जो ऊपर बताई गई कुछ चीजों को करने से सुरक्षित नहीं हैं। जब यह आता है कि अन्य रत्न मान लेते हैं कि वे थ्रेड सेफ नहीं हैं जब तक कि वे यह नहीं कहते कि वे हैं, और यदि वे कहते हैं कि वे मान रहे हैं कि वे नहीं हैं, और अपने कोड के माध्यम से देखते हैं (लेकिन सिर्फ इसलिए कि आप देखते हैं कि वे चीजें जैसे हैं@n ||= 1 इसका मतलब यह नहीं है कि वे थ्रेड सुरक्षित नहीं हैं, यह सही संदर्भ में करने के लिए एक पूरी तरह से वैध चीज है - आपको इसके बजाय वैश्विक चर में परिवर्तनशील राज्य जैसी चीजों की तलाश करनी चाहिए, यह अपने तरीके से पारित होने योग्य वस्तुओं को कैसे संभालती है, और विशेष रूप से यह कैसे हैंडल विकल्प हैश)।

अंत में, थ्रेड असुरक्षित होना एक सकर्मक गुण है। कुछ भी जो थ्रेड सुरक्षित नहीं है का उपयोग करता है खुद थ्रेड सुरक्षित नहीं है।


बहुत बढ़िया जवाब। यह देखते हुए कि एक सामान्य रेल ऐप मल्टी-प्रोसेस है (जैसे आपने वर्णित है, एक ही ऐप को एक्सेस करने वाले कई अलग-अलग उपयोगकर्ता), मैं सोच रहा हूं कि कंसीडर मॉडल को थ्रेड्स का मामूली जोखिम क्या है ... दूसरे शब्दों में, कितना अधिक "खतरनाक" यदि आप पहले से ही प्रक्रियाओं के माध्यम से कुछ संगामिति के साथ काम कर रहे हैं तो यह थ्रेडेड मोड में चलना है?
जिंजलिमे

2
@ एक टन धन्यवाद। वह निरंतर सामान एक बड़ा बम है। यह सुरक्षित प्रक्रिया भी नहीं है। यदि निरंतर एक अनुरोध में परिवर्तित हो जाता है, तो यह बाद के अनुरोधों को एकल धागे में भी परिवर्तित स्थिर देखने का कारण होगा। रूबी स्थिरांक अजीब हैं
रगड़

5
STANDARD_OPTIONS = {...}.freezeउथले म्यूटेशन को बढ़ाने के लिए करें
glebm

वास्तव में महान जवाब
Cheyne

3
"यदि आप कोड @n = 0; 3.times { Thread.start { 100.times { @n += 1 } } }[...] लिखते हैं , तो बाद में साझा चर का मूल्य नियतात्मक नहीं है।" - क्या आप जानते हैं कि क्या यह रूबी के संस्करणों के बीच भिन्न है? उदाहरण के लिए, 1.8 पर आपके कोड को चलाने से अलग-अलग मान @n@n
मिलते हैं

10

थियो के जवाब के अलावा, मैं विशेष रूप से रेल में देखने के लिए कुछ समस्या क्षेत्रों को जोड़ दूंगा, अगर आप config.threadsafe पर स्विच कर रहे हैं!

  • कक्षा चर :

    @@i_exist_across_threads

  • ENV :

    ENV['DONT_CHANGE_ME']

  • धागे :

    Thread.start


9

रेल 4 से शुरू, सब कुछ डिफ़ॉल्ट रूप से थ्रेडेड वातावरण में चलाना होगा

यह 100% सही नहीं है। थ्रेड-सुरक्षित रेल बस डिफ़ॉल्ट रूप से चालू है। यदि आप यात्री (समुदाय) या यूनिकॉर्न जैसे मल्टी-प्रोसेस ऐप सर्वर पर तैनात हैं, तो कोई अंतर नहीं होगा। यह परिवर्तन केवल आपकी चिंता करता है, यदि आप प्यूमा या पैसेंजर एंटरप्राइज> 4.0 जैसे बहु-थ्रेडेड वातावरण पर तैनात हैं

यदि आप मल्टी-थ्रेडेड ऐप सर्वर पर तैनाती करना चाहते हैं तो अतीत में आपको config.threadsafe को चालू करना था , जो अब डिफ़ॉल्ट है, क्योंकि सभी में इसका कोई प्रभाव नहीं था या एकल प्रक्रिया में चल रहे रेल एप्लिकेशन पर भी लागू होता है ( प्रूफलिंक )।

लेकिन अगर आप मल्टी-थ्रेड परिनियोजन के सभी रेल 4 स्ट्रीमिंग लाभ और अन्य वास्तविक समय सामान चाहते हैं तो शायद आपको यह लेख दिलचस्प लगेगा । @Theo दुखी के रूप में, एक रेल एप्लिकेशन के लिए, आपको वास्तव में अनुरोध के दौरान स्थैतिक स्थिति को परिवर्तित करना होगा। हालांकि यह एक सरल अभ्यास है, लेकिन दुर्भाग्य से आप इस बारे में निश्चित नहीं हो सकते हैं कि आपको हर रत्न मिलेगा। जहाँ तक मुझे याद है JRuby प्रोजेक्ट के चार्ल्स ओलिवर नट्टर के पास इस पॉडकास्ट में इसके बारे में कुछ सुझाव थे ।

और यदि आप एक शुद्ध समवर्ती रूबी प्रोग्रामिंग लिखना चाहते हैं, जहां आपको कुछ डेटा संरचनाओं की आवश्यकता होगी, जो एक से अधिक थ्रेड द्वारा एक्सेस की जाती हैं, तो शायद आपको थ्रेड_सैफ़ रत्न उपयोगी लगेगा

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.