Computer >> कंप्यूटर >  >> प्रोग्रामिंग >> Ruby

रेस की स्थिति को रोकने के लिए ActiveRecords #update_counters का उपयोग करना

रेल एक बड़ा ढांचा है जिसमें विशिष्ट परिस्थितियों के लिए बहुत सारे उपयोगी उपकरण अंतर्निहित हैं। इस श्रृंखला में, हम रेल के बड़े कोडबेस में छिपे कुछ कम ज्ञात टूल पर एक नज़र डाल रहे हैं।

श्रृंखला के इस लेख में, हम ActiveRecord के update_counters पर एक नज़र डालने जा रहे हैं तरीका। इस प्रक्रिया में, हम बहुप्रचारित कार्यक्रमों में "दौड़ की स्थिति" के सामान्य जाल को देखेंगे और यह विधि उन्हें कैसे रोक सकती है।

थ्रेड्स

प्रोग्रामिंग करते समय, हमारे पास समानांतर में कोड चलाने के कई तरीके हैं, जिसमें प्रक्रियाएं, थ्रेड्स, और, हाल ही में (रूबी में), फाइबर और रिएक्टर शामिल हैं। इस लेख में, हम केवल थ्रेड्स के बारे में चिंता करने जा रहे हैं, क्योंकि यह सबसे सामान्य रूप है जिसका सामना रेल डेवलपर्स करेंगे। उदाहरण के लिए, प्यूमा एक मल्टीथ्रेडेड सर्वर है, और साइडकीक एक मल्टीथ्रेडेड बैकग्राउंड जॉब प्रोसेसर है।

हम यहां धागे और धागे की सुरक्षा में गहरी डुबकी नहीं लगाएंगे। जानने वाली मुख्य बात यह है कि जब एक ही डेटा पर दो थ्रेड काम कर रहे हों, तो डेटा आसानी से सिंक से बाहर हो सकता है। इसे "दौड़ की स्थिति" के रूप में जाना जाता है।

रेस कंडीशन

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

एक उदाहरण

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

class UnsafeTransaction
  def self.run
    account = Account.find(1)
    account.update!(balance: 0)

    threads = []
    4.times do
      threads << Thread.new do
        balance = account.reload.balance
        account.update!(balance: balance + 100)

        balance = account.reload.balance
        account.update!(balance: balance - 100)
      end
    end

    threads.map(&:join)

    account.reload.balance
  end
end

हमारा UnsafeTransaction बहुत आसान है; हमारे पास केवल एक तरीका है जो Account . को खोजता है (एक स्टॉक-मानक रेल मॉडल जिसमें BigDecimal balance है विशेषता)। परीक्षण को फिर से चलाना आसान बनाने के लिए हम शेष राशि को शून्य पर रीसेट कर देते हैं।

आंतरिक पाश वह जगह है जहां चीजें थोड़ी अधिक दिलचस्प होती हैं। हम चार सूत्र बना रहे हैं जो खाते की वर्तमान शेष राशि को हथिया लेंगे, इसमें 100 जोड़ देंगे (जैसे, $ 100 जमा), और फिर तुरंत 100 घटाएं (उदाहरण के लिए, $ 100 की निकासी)। हम reload . का भी उपयोग कर रहे हैं दोनों बार अतिरिक्त . होने के लिए सुनिश्चित करें कि हमारे पास अप-टू-डेट बैलेंस है।

शेष पंक्तियाँ बस कुछ साफ कर रही हैं। Thread.join इसका मतलब है कि हम आगे बढ़ने से पहले सभी थ्रेड्स के समाप्त होने की प्रतीक्षा करेंगे, और फिर हम विधि के अंत में अंतिम शेष राशि वापस कर देंगे।

अगर हम इसे एक ही धागे से चलाते हैं (लूप को 1.times do . में बदलकर ), हम खुशी-खुशी इसे एक लाख बार चला सकते हैं और सुनिश्चित करें कि अंतिम खाता शेष हमेशा शून्य रहेगा। हालांकि, इसे दो (या अधिक) धागों में बदलें, और चीजें कम निश्चित हैं।

हमारे परीक्षण को एक बार कंसोल में चलाने से शायद हमें सही उत्तर मिल जाएगा:

UnsafeTransaction.run
=> 0.0

हालाँकि, क्या होगा यदि हम इसे बार-बार चलाते हैं। मान लें कि हमने इसे दस बार चलाया:

(1..10).map { UnsafeTransaction.run }.map(&:to_f)
=> [0.0, 300.0, 300.0, 100.0, 100.0, 100.0, 300.0, 300.0, 100.0, 300.0]

यदि यहां सिंटैक्स परिचित नहीं है, तो (1..10).map {} ब्लॉक में 10 बार कोड चलाएगा, प्रत्येक रन के परिणामों को एक सरणी में डाल दिया जाएगा। .map(&:to_f) अंत में संख्याओं को अधिक मानव-पठनीय बनाने के लिए है, क्योंकि BigDecimal मान सामान्य रूप से 0.1e3 जैसे घातीय संकेतन में मुद्रित किए जाएंगे। ।

याद रखें, हमारा कोड वर्तमान शेष राशि लेता है, 100 जोड़ता है, और फिर तुरंत 100 घटाता है, इसलिए अंतिम परिणाम चाहिए हमेशा 0.0 be रहें . ये 100.0 और 300.0 प्रविष्टियाँ, तब, इस बात का प्रमाण हैं कि हमारे पास एक दौड़ की स्थिति है।

एनोटेटेड उदाहरण

आइए यहां समस्या कोड पर ज़ूम इन करें और देखें कि क्या हो रहा है। हम परिवर्तनों को balance . में अलग कर देंगे और भी स्पष्टता के लिए।

threads << Thread.new do
  # Thread could be switching here
  balance = account.reload.balance
  # or here...
  balance += 100
  # or here...
  account.update!(balance: balance)
  # or here...

  balance = account.reload.balance
  # or here...
  balance -= 100
  # or here...
  account.update!(balance: balance)
  # or here...
end

जैसा कि हम टिप्पणियों में देखते हैं, इस कोड के दौरान लगभग किसी भी बिंदु पर धागे की अदला-बदली हो सकती है। यदि थ्रेड 1 शेष राशि को पढ़ता है, तो कंप्यूटर थ्रेड 2 को निष्पादित करना शुरू कर देता है, इसलिए यह बहुत संभव है कि update! को कॉल करने तक डेटा पुराना हो जाएगा। . दूसरा तरीका रखो, थ्रेड 1, थ्रेड 2, और डेटाबेस, सभी में डेटा है, लेकिन वे एक-दूसरे के साथ सिंक से बाहर हो रहे हैं।

यहां उदाहरण जानबूझकर तुच्छ है ताकि इसे काटना आसान हो। वास्तविक दुनिया में, हालांकि, दौड़ की स्थिति का निदान करना कठिन हो सकता है, खासकर क्योंकि उन्हें आमतौर पर मज़बूती से पुन:प्रस्तुत नहीं किया जा सकता है।

समाधान

दौड़ की स्थिति को रोकने के लिए कुछ विकल्प हैं, लेकिन उनमें से लगभग सभी एक ही विचार के इर्द-गिर्द घूमते हैं:यह सुनिश्चित करना कि किसी भी समय केवल एक इकाई डेटा बदल रही है।

विकल्प 1:म्यूटेक्स

सबसे आसान विकल्प एक "म्यूचुअल एक्सक्लूज़न लॉक" है, जिसे आमतौर पर म्यूटेक्स के रूप में जाना जाता है। आप म्यूटेक्स को केवल एक कुंजी के साथ लॉक के रूप में सोच सकते हैं। यदि एक थ्रेड कुंजी धारण कर रहा है, तो यह म्यूटेक्स में जो कुछ भी है उसे चला सकता है। अन्य सभी थ्रेड्स को तब तक प्रतीक्षा करनी होगी जब तक कि वे कुंजी को पकड़ न सकें।

हमारे उदाहरण कोड में म्यूटेक्स लागू करना इस प्रकार किया जा सकता है:

class MutexTransaction
  def self.run
    account = Account.find(1)
    account.update!(balance: 0)

    mutex = Mutex.new

    threads = []
    4.times do
      threads << Thread.new do
        mutex.lock
        balance = account.reload.balance
        account.update!(balance: balance + 100)
        mutex.unlock

        mutex.lock
        balance = account.reload.balance
        account.update!(balance: balance - 100)
        mutex.unlock
      end
    end

    threads.map(&:join)

    account.reload.balance
  end
end

यहां, हर बार जब हम Account . को पढ़ते और लिखते हैं , हम सबसे पहले mutex.lock . को कॉल करते हैं , और फिर एक बार जब हम कर लेते हैं, तो हम mutex.unlock . को कॉल करते हैं अन्य धागों को मोड़ने की अनुमति देने के लिए। हम बस mutex.lock को कॉल कर सकते हैं ब्लॉक की शुरुआत में और mutex.unlock अतं मै; हालांकि, इसका मतलब यह होगा कि धागे अब एक साथ नहीं चल रहे हैं, जो कुछ हद तक पहले स्थान पर धागे का उपयोग करने के कारण को अस्वीकार करता है। प्रदर्शन के लिए, कोड को mutex के अंदर रखना सबसे अच्छा है जितना संभव हो उतना छोटा, क्योंकि यह थ्रेड को जितना संभव हो सके समानांतर में अधिक से अधिक कोड निष्पादित करने की अनुमति देता है।

हमने .lock . का उपयोग किया है और .unlock यहाँ स्पष्टता के लिए, लेकिन रूबी का Mutex क्लास एक अच्छा synchronize प्रदान करता है विधि जो एक ब्लॉक लेती है और हमारे लिए इसे संभालती है, इसलिए हम निम्नलिखित कार्य कर सकते थे:

mutex.synchronize do
  balance = ...
  ...
end

रूबी का म्यूटेक्स वह करता है जो हमें चाहिए, लेकिन जैसा कि आप शायद कल्पना कर सकते हैं, रेल अनुप्रयोगों में किसी विशेष डेटाबेस पंक्ति को लॉक करने की आवश्यकता होती है, और ActiveRecord ने हमें इस परिदृश्य के लिए कवर किया है।

विकल्प 2:ActiveRecord Locks

ActiveRecord कुछ अलग लॉकिंग तंत्र प्रदान करता है, और हम यहां उन सभी में एक गहरा गोता नहीं लगाएंगे। हमारे उद्देश्यों के लिए, हम केवल lock! . का उपयोग कर सकते हैं उस पंक्ति को लॉक करने के लिए जिसे हम अपडेट करना चाहते हैं:

class LockedTransaction
  def self.run
    account = Account.find(1)
    account.update!(balance: 0)

    threads = []
    4.times do
      threads << Thread.new do
        Account.transaction do
          account = account.reload
          account.lock!
          account.update!(balance: account.balance + 100)
        end

        Account.transaction do
          account = account.reload
          account.lock!
          account.update!(balance: account.balance - 100)
        end
      end
    end

    threads.map(&:join)

    account.reload.balance
  end
end

जबकि एक म्यूटेक्स किसी विशेष थ्रेड के लिए कोड के अनुभाग को "लॉक" करता है, lock! विशेष डेटाबेस पंक्ति को लॉक करता है। इसका मतलब यह है कि एक ही कोड कई खातों पर समानांतर में निष्पादित हो सकता है (उदाहरण के लिए, पृष्ठभूमि नौकरियों के एक समूह में)। केवल वही थ्रेड जिन्हें समान रिकॉर्ड तक पहुंचने की आवश्यकता होती है, उन्हें प्रतीक्षा करनी होगी। ActiveRecord एक आसान #with_lock भी प्रदान करता है। विधि जो आपको लेन-देन करने और एक बार में लॉक करने देती है, इसलिए ऊपर दिए गए अपडेट को थोड़ा और संक्षेप में इस प्रकार लिखा जा सकता है:

account = account.reload
account.with_lock do
  account.update!(account.balance + 100)
end
...

समाधान 3:परमाणु विधियां

निष्पादन के माध्यम से एक 'परमाणु' विधि (या कार्य) को बीच में नहीं रोका जा सकता है। उदाहरण के लिए, सामान्य += रूबी में ऑपरेशन नहीं है परमाणु, भले ही यह एक ही ऑपरेशन जैसा दिखता हो:

value += 10

# equivalent to:
value = value + 10

# Or even more verbose:
temp_value = value + 10
value = temp_value

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

कुछ भाषाओं में इस तरह की थ्रेड-सुरक्षा (उदाहरण के लिए, परमाणु इंटेगर और परमाणु फ्लोट) के लिए आदिम मूल्यों के परमाणु संस्करण होते हैं। इसका मतलब यह नहीं है कि रेल डेवलपर्स के रूप में हमारे पास कुछ "परमाणु" संचालन उपलब्ध नहीं हैं। एक बार उदाहरण है ActiveRecord का update_counters विधि।

यद्यपि यह काउंटर कैश को अद्यतित रखने के लिए अधिक अभिप्रेत है, फिर भी हमें अपने अनुप्रयोगों में इसका उपयोग करने से कोई रोक नहीं रहा है। काउंटर कैश के बारे में अधिक जानकारी के लिए, आप कैशिंग पर मेरा पिछला लेख देख सकते हैं)।

विधि का उपयोग करना अविश्वसनीय रूप से सरल है:

class CounterTransaction
  def self.run
    account = Account.find(1)
    account.update!(balance: 0)

    threads = []
    4.times do
      threads << Thread.new do
        Account.update_counters(account.id, balance: 100)

        Account.update_counters(account.id, balance: -100)
      end
    end

    threads.map(&:join)

    account.reload.balance
  end
end

कोई म्यूटेक्स नहीं, कोई ताले नहीं, रूबी की केवल दो पंक्तियां; update_counters रिकॉर्ड आईडी को पहले तर्क के रूप में लेता है, और फिर हम इसे बताते हैं कि कौन सा कॉलम बदलना है (balance: ) और इसे कितना बदलना है (100 .) या -100 ) इसका कारण यह है कि रीड-अपडेट-राइट चक्र अब एक SQL कॉल में डेटाबेस में होता है। इसका मतलब है कि हमारा रूबी थ्रेड ऑपरेशन को बाधित नहीं कर सकता है; भले ही वह सो जाए, इससे कोई फर्क नहीं पड़ेगा क्योंकि डेटाबेस वास्तविक गणना कर रहा है।

वास्तविक एसक्यूएल का उत्पादन इस तरह से होता है (कम से कम मेरी मशीन पर पोस्टग्रेज के लिए):

Account Update All (1.7ms)  UPDATE "accounts" SET "balance" = COALESCE("balance", 0) + $1 WHERE "accounts"."id" = $2  [["balance", "100.0"], ["id", 1]]

यह तरीका भी बहुत बेहतर प्रदर्शन करता है, जो आश्चर्यजनक नहीं है, क्योंकि गणना पूरी तरह से डेटाबेस में होती है; हमें कभी भी reload करने की आवश्यकता नहीं है नवीनतम मूल्य प्राप्त करने का रिकॉर्ड। हालाँकि, यह गति एक कीमत पर आती है। क्योंकि हम इसे कच्चे SQL में कर रहे हैं, हम रेल मॉडल को दरकिनार कर रहे हैं, जिसका अर्थ है कि कोई भी सत्यापन या कॉलबैक निष्पादित नहीं किया जाएगा (अर्थात्, अन्य बातों के अलावा, updated_at में कोई बदलाव नहीं किया जाएगा। टाइमस्टैम्प)।

निष्कर्ष

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

सामान्य रूबी कोड के लिए, mutex एक अच्छा विकल्प है और संभवत:"थ्रेड सेफ्टी" शब्द सुनते समय अधिकांश डेवलपर्स सबसे पहले सोचते हैं।

रेल के साथ, अधिक संभावना नहीं है, डेटा ActiveRecord से आ रहे हैं। इन मामलों में, lock! (या with_lock ) उपयोग करने के लिए सीधा है और म्यूटेक्स की तुलना में अधिक थ्रूपुट की अनुमति देता है, क्योंकि यह केवल डेटाबेस में प्रासंगिक पंक्तियों को लॉक करता है।

मैं यहां ईमानदार रहूंगा, मुझे यकीन नहीं है कि मैं update_counters तक पहुंच पाऊंगा वास्तविक दुनिया में बहुत कुछ। यह असामान्य है कि अन्य डेवलपर्स इससे परिचित नहीं हो सकते हैं कि यह कैसे व्यवहार करता है, और यह कोड के इरादे को विशेष रूप से स्पष्ट नहीं करता है। यदि थ्रेड-सुरक्षा चिंताओं का सामना करना पड़ता है, तो ActiveRecord के ताले (या तो lock! या with_lock ) दोनों अधिक सामान्य हैं और अधिक स्पष्ट रूप से कोडर के इरादे को संप्रेषित करते हैं।

हालांकि, यदि आपके पास बहुत सारे सरल 'जोड़ें या घटाएं' कार्य हैं, और आपको कच्ची पेडल-टू-द-मेटल गति की आवश्यकता है, update_counters आपकी पिछली जेब में एक उपयोगी उपकरण हो सकता है।


  1. रेल के साथ हॉटवायर का उपयोग करना

    यदि आप बिना किसी जावास्क्रिप्ट कोड को लिखे पेज परिवर्तन और फॉर्म सबमिशन को तेज करने और जटिल पेजों को घटकों में विभाजित करने का तरीका ढूंढ रहे हैं, तो यह पोस्ट आपको हॉटवायर के साथ रेल को अगले स्तर तक ले जाने में मदद करेगी। यह लेख आपको सर्वर-साइड रेंडरिंग के लिए टूल का उपयोग करना सिखाएगा। हॉटवायर क्या

  1. रूबी में लैम्ब्डा का उपयोग करना

    ब्लॉक रूबी का इतना महत्वपूर्ण हिस्सा हैं, उनके बिना भाषा की कल्पना करना मुश्किल है। लेकिन लैम्ब्डा? लैम्ब्डा को कौन प्यार करता है? आप एक का उपयोग किए बिना वर्षों तक जा सकते हैं। वे लगभग पुराने जमाने के अवशेष की तरह लगते हैं। ...लेकिन यह बिल्कुल सच नहीं है। एक बार जब आप उनकी थोड़ी जांच कर लेते हैं त

  1. रेल के साथ कोणीय का उपयोग करना 5

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