शीर्षक: शीर्ष युक्तियाँ: Lua में अद्वितीय अवधारणाओं और खामियों की पहचान करना

API7.ai

October 12, 2022

OpenResty (NGINX + Lua)

पिछले लेख में, हमने LuaJIT में टेबल से संबंधित लाइब्रेरी फंक्शन्स के बारे में सीखा। इन सामान्य फंक्शन्स के अलावा, आज मैं आपको OpenResty में कुछ अद्वितीय या कम सामान्य Lua अवधारणाओं और Lua से संबंधित सामान्य गलतियों के बारे में बताऊंगा।

वीक टेबल

सबसे पहले, वीक टेबल है, जो Lua में एक अद्वितीय अवधारणा है, और यह गार्बेज कलेक्शन से संबंधित है। अन्य उच्च-स्तरीय भाषाओं की तरह, Lua में स्वचालित गार्बेज कलेक्शन होता है, आपको इसके कार्यान्वयन की चिंता करने की आवश्यकता नहीं है, और आपको स्पष्ट रूप से GC करने की आवश्यकता नहीं है। गार्बेज कलेक्टर स्वचालित रूप से उन स्थानों को कलेक्ट करेगा जिनका कोई संदर्भ नहीं है।

लेकिन सरल संदर्भ गिनती काफी नहीं है, और कभी-कभी हमें एक अधिक लचीली प्रणाली की आवश्यकता होती है। उदाहरण के लिए, यदि हम एक Lua ऑब्जेक्ट Foo (टेबल या फंक्शन) को टेबल tb में डालते हैं, तो यह उस ऑब्जेक्ट Foo के लिए एक संदर्भ बनाता है। भले ही Foo के लिए कोई अन्य संदर्भ न हो, tb में इसका संदर्भ हमेशा मौजूद रहेगा, इसलिए GC के लिए Foo द्वारा कब्जा की गई मेमोरी को वापस लेना संभव नहीं है। इस स्थिति में, हमारे पास केवल दो विकल्प हैं।

  • एक है Foo को मैन्युअल रूप से रिलीज़ करना।
  • दूसरा है इसे मेमोरी में स्थायी रूप से रखना।

उदाहरण के लिए, निम्नलिखित कोड।

$ resty -e 'local tb = {} tb[1] = {red} tb[2] = function() print("func") end print(#tb) -- 2 collectgarbage() print(#tb) -- 2 table.remove(tb, 1) print(#tb) -- 1

हालांकि, मुझे लगता है कि आप उन ऑब्जेक्ट्स द्वारा कब्जा की गई मेमोरी को नहीं रखना चाहेंगे जिनका आप उपयोग नहीं कर रहे हैं, खासकर क्योंकि LuaJIT में 2G मेमोरी सीमा है। मैन्युअल रूप से मुक्त करने का समय आसान नहीं है और यह आपके कोड को जटिल बनाता है।

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

$ resty -e 'local tb = {} tb[1] = {red} tb[2] = function() print("func") end setmetatable(tb, {__mode = "v"}) print(#tb) -- 2 collectgarbage() print(#tb) -- 0 '

जैसा कि आप देख सकते हैं, उपयोग नहीं किए जा रहे ऑब्जेक्ट्स को मुक्त कर दिया गया है। इनमें से सबसे महत्वपूर्ण निम्नलिखित कोड की पंक्ति है।

setmetatable(tb, {__mode = "v"})

क्या यह डेजा वू है? क्या यह मेटा टेबल का ऑपरेशन नहीं है? हां, एक टेबल वीक टेबल होती है जब इसकी मेटा टेबल में __mode फील्ड होता है।

  • यदि __mode का मान k है, तो टेबल की कुंजी एक वीक संदर्भ है।
  • यदि __mode का मान v है, तो टेबल का मान एक वीक संदर्भ है।
  • बेशक, आप इसे kv पर भी सेट कर सकते हैं, जो इंगित करता है कि इस टेबल की कुंजी और मान दोनों वीक संदर्भ हैं।

इन तीनों वीक टेबल्स में से किसी एक में, एक बार इसकी कुंजी या मान मुक्त हो जाने पर, इसके पूरे कुंजी-मान ऑब्जेक्ट को वापस ले लिया जाएगा।

उपरोक्त कोड उदाहरण में, __mode का मान v है, tb एक एरे है, और एरे का मान टेबल और फंक्शन ऑब्जेक्ट है ताकि इसे स्वचालित रूप से रीसायकल किया जा सके। हालांकि, यदि आप __mode का मान k में बदलते हैं, तो यह मुक्त नहीं होगा, उदाहरण के लिए, यदि आप निम्नलिखित कोड को देखते हैं।

$ resty -e 'local tb = {} tb[1] = {red} tb[2] = function() print("func") end setmetatable(tb, {__mode = "k"}) print(#tb) -- 2 collectgarbage() print(#tb) -- 2 '

हम केवल उन वीक टेबल्स का प्रदर्शन करते हैं जहां मान एक वीक संदर्भ है, यानी, एरे प्रकार की वीक टेबल्स। स्वाभाविक रूप से, आप एक ऑब्जेक्ट को कुंजी के रूप में उपयोग करके हैश टेबल प्रकार की वीक टेबल भी बना सकते हैं, उदाहरण के लिए, निम्नलिखित।

$ resty -e 'local tb = {} tb[{color = red}] = "red" local fc = function() print("func") end tb[fc] = "func" fc = nil setmetatable(tb, {__mode = "k"}) for k,v in pairs(tb) do print(v) end collectgarbage() print("----------") for k,v in pairs(tb) do print(v) end '

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

हालांकि, चूंकि हमने collectgarbage() फंक्शन का उल्लेख किया है, मैं इसके बारे में कुछ और शब्द कहूंगा। इस फंक्शन को कई अलग-अलग विकल्प पास किए जा सकते हैं और यह डिफ़ॉल्ट रूप से collect होता है, जो एक पूर्ण GC है। एक और उपयोगी विकल्प count है, जो Lua द्वारा कब्जा की गई मेमोरी स्पेस की मात्रा लौटाता है। यह सांख्यिकी आपको यह देखने में मदद करती है कि क्या मेमोरी लीक है और हमें 2G ऊपरी सीमा के करीब नहीं जाने की याद दिलाती है।

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

क्लोजर और अपवैल्यू

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

tb[2] = function() print("func") end

यह एक अनाम फंक्शन है जो एक टेबल के मान के रूप में संग्रहीत है।

Lua में, निम्नलिखित कोड में दो फंक्शन्स की परिभाषा समतुल्य है। हालांकि, ध्यान दें कि बाद वाला एक फंक्शन को एक वेरिएबल को असाइन करता है, एक विधि जिसका हम अक्सर उपयोग करते हैं।

local function foo() print("foo") end local foo = fuction() print("foo") end

इसके अलावा, Lua एक फंक्शन के अंदर एक अन्य फंक्शन लिखने का समर्थन करता है, यानी, नेस्टेड फंक्शन्स, जैसे कि निम्नलिखित उदाहरण कोड।

$ resty -e ' local function foo() local i = 1 local function bar() i = i + 1 print(i) end return bar end local fn = foo() print(fn()) -- 2 '

आप देख सकते हैं कि bar फंक्शन foo फंक्शन के अंदर स्थानीय वेरिएबल i को पढ़ सकता है और इसके मान को संशोधित कर सकता है, भले ही यह वेरिएबल bar के अंदर परिभाषित न हो। इस सुविधा को लेक्सिकल स्कोपिंग कहा जाता है।

Lua की ये सुविधाएं क्लोजर के लिए आधार हैं। एक क्लोजर केवल एक फंक्शन है जो किसी अन्य फंक्शन के लेक्सिकल स्कोप में एक वेरिएबल तक पहुंचता है।

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

local foo, bar local function fn() foo = 1 bar = 2 end

कंपाइलेशन के बाद, यह इस तरह दिखेगा।

function main(...) local foo, bar local function fn() foo = 1 bar = 2 end end

और फंक्शन fn मुख्य फंक्शन के दो स्थानीय वेरिएबल्स को कैप्चर करता है, इसलिए यह भी एक क्लोजर है।

बेशक, हम जानते हैं कि क्लोजर की अवधारणा कई भाषाओं में मौजूद है, और यह Lua के लिए अद्वितीय नहीं है, इसलिए आप तुलना और विरोधाभास करके बेहतर समझ सकते हैं। केवल जब आप क्लोजर को समझते हैं, तभी आप upvalue के बारे में हमारी बात को समझ सकते हैं।

upvalue एक ऐसी अवधारणा है जो Lua के लिए अद्वितीय है, जो क्लोजर में कैप्चर की गई लेक्सिकल स्कोप के बाहर की वेरिएबल है। आइए उपरोक्त कोड के साथ जारी रखें।

local foo, bar local function fn() foo = 1 bar = 2 end

आप देख सकते हैं कि फंक्शन fn दो स्थानीय वेरिएबल्स, foo और bar को कैप्चर करता है, जो उनके अपने लेक्सिकल स्कोप में नहीं हैं, और ये दो वेरिएबल्स वास्तव में फंक्शन fn के upvalue हैं।

सामान्य गलतियाँ

Lua में कुछ अवधारणाओं का परिचय देने के बाद, मैं OpenResty विकास में Lua से संबंधित गलतियों के बारे में बात करूंगा।

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

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

क्या इंडेक्स 0 या 1 से शुरू होता है?

पहली गलती यह है कि Lua का इंडेक्स 1 से शुरू होता है, जैसा कि हमने पहले बार-बार उल्लेख किया है।

लेकिन मुझे कहना होगा कि यह पूरी सच्चाई नहीं है। क्योंकि LuaJIT में, ffi.new के साथ बनाए गए एरे 0 से इंडेक्स होते हैं:

local buf = ffi_new("char[?]", 128)

इसलिए, यदि आप उपरोक्त कोड में buf cdata तक पहुंचना चाहते हैं, तो कृपया याद रखें कि इंडेक्स 0 से शुरू होता है, 1 से नहीं। जब आप FFI का उपयोग करके C के साथ इंटरैक्ट करते हैं, तो इस स्थान पर विशेष ध्यान दें।

रेगुलर पैटर्न मैच

दूसरी गलती रेगुलर पैटर्न मैचिंग समस्या है, और OpenResty में दो सेट स्ट्रिंग मैचिंग विधियां समानांतर में हैं: Lua की स्ट्रिंग लाइब्रेरी और OpenResty की ngx.re.* API।

Lua की रेगुलर पैटर्न मैचिंग इसकी अद्वितीय प्रारूप है और यह PCRE से अलग तरीके से लिखी जाती है। यहां एक सरल उदाहरण है।

resty -e 'print(string.match("foo 123 bar", "%d%d%d"))'123

यह कोड स्ट्रिंग से संख्यात्मक भाग निकालता है, और आप देखेंगे कि यह हमारे परिचित रेगुलर एक्सप्रेशन से पूरी तरह अलग है। Lua की रेगुलर मैचिंग लाइब्रेरी को बनाए रखना महंगा है और कम प्रदर्शन करती है - JIT इसे अनुकूलित नहीं कर सकता है, और एक बार कंपाइल किए गए पैटर्न को कैश नहीं किया जाता है।

इसलिए, जब आप Lua की अंतर्निहित स्ट्रिंग लाइब्रेरी का उपयोग करके find, match, आदि करते हैं, तो यदि आपको कुछ रेगुलर की तरह चाहिए, तो OpenResty के ngx.re का उपयोग करने में संकोच न करें। जब एक निश्चित स्ट्रिंग की तलाश करते हैं, तो हम स्ट्रिंग लाइब्रेरी को कॉल करने के लिए सादे मोड का उपयोग करने पर विचार करते हैं।

यहां एक सुझाव है: OpenResty में, हम हमेशा OpenResty के API को प्राथमिकता देते हैं, फिर LuaJIT के API, और Lua लाइब्रेरी का उपयोग सावधानी से करते हैं।

JSON एन्कोडिंग एरे और डिक्ट के बीच अंतर नहीं करता है

तीसरी गलती यह है कि JSON एन्कोडिंग एरे और डिक्ट के बीच अंतर नहीं करता है; चूंकि Lua में केवल एक डेटा संरचना है, टेबल, जब JSON एक खाली टेबल को एन्कोड करता है, तो यह निर्धारित करने का कोई तरीका नहीं है कि यह एक एरे है या एक डिक्शनरी।

resty -e 'local cjson = require "cjson" local t = {} print(cjson.encode(t)) '

उदाहरण के लिए, उपरोक्त कोड {} आउटपुट करता है, जो दिखाता है कि OpenResty की cjson लाइब्रेरी डिफ़ॉल्ट रूप से एक खाली टेबल को डिक्शनरी के रूप में एन्कोड करती है। बेशक, हम encode_empty_table_as_object फंक्शन का उपयोग करके इस ग्लोबल डिफ़ॉल्ट को बदल सकते हैं।

resty -e 'local cjson = require "cjson" cjson.encode_empty_table_as_object(false) local t = {} print(cjson.encode(t)) '

इस बार, खाली टेबल को एरे [] के रूप में एन्कोड किया जाता है।

हालांकि, इस ग्लोबल सेटिंग का एक महत्वपूर्ण प्रभाव होता है, इसलिए क्या हम किसी विशिष्ट टेबल के लिए एन्कोडिंग नियम निर्दिष्ट कर सकते हैं? उत्तर स्वाभाविक रूप से हां है, और इसे करने के दो तरीके हैं।

पहला तरीका है userdata cjson.empty_array को निर्दिष्ट टेबल को असाइन करना ताकि इसे JSON में एन्कोड करते समय एक खाली एरे के रूप में माना जाए।

$ resty -e 'local cjson = require "cjson" local t = cjson.empty_array print(cjson.encode(t)) '

हालांकि, कभी-कभी हम यह सुनिश्चित नहीं करते हैं कि निर्दिष्ट टेबल हमेशा खाली हो। हम चाहते हैं कि जब यह खाली हो, तो इसे एरे के रूप में एन्कोड किया जाए, इसलिए हम cjson.empty_array_mt फंक्शन का उपयोग करते हैं, जो हमारा दूसरा तरीका है।

यह निर्दिष्ट टेबल को चिह्नित करेगा और जब टेबल खाली होगी, तो इसे एरे के रूप में एन्कोड करेगा। जैसा कि आप cjson.empty_array_mt नाम से देख सकते हैं, यह metatable का उपयोग करके सेट किया गया है, जैसा कि निम्नलिखित कोड ऑपरेशन में है।

$ resty -e 'local cjson = require "cjson" local t = {} setmetatable(t, cjson.empty_array_mt) print(cjson.encode(t)) t = {123} print(cjson.encode(t)) '

वेरिएबल्स की संख्या पर सीमा

चौथी गलती को देखते हैं, वेरिएबल्स की संख्या पर सीमा। Lua में एक फंक्शन में स्थानीय वेरिएबल्स और upvalue की संख्या पर एक ऊपरी सीमा है, जैसा कि आप Lua स्रोत कोड से देख सकते हैं।

/* @@ LUAI_MAXVARS is the maximum number of local variables per function @* (must be smaller than 250). */ #define LUAI_MAXVARS 200 /* @@ LUAI_MAXUPVALUES is the maximum number of upvalues per function @* (must be smaller than 250). */ #define LUAI_MAXUPVALUES 60

ये दो थ्रेसहोल्ड्स क्रमशः 200 और 60 पर हार्डकोडेड हैं, और हालांकि आप इन दो मानों को समायोजित करने के लिए स्रोत कोड को मैन्युअल रूप से संशोधित कर सकते हैं, लेकिन इन्हें अधिकतम 250 पर सेट किया जा सकता है।

आम तौर पर, हम इस थ्रेसहोल्ड को पार नहीं करते हैं। फिर भी, OpenResty कोड लिखते समय, आपको स्थानीय वेरिएबल्स और upvalue का अत्यधिक उपयोग नहीं करने के लिए सावधान रहना चाहिए, बल्कि do ... end का उपयोग करके स्थानीय वेरिएबल्स और upvalue की संख्या को कम करने का प्रयास करना चाहिए।

उदाहरण के लिए, निम्नलिखित स्यूडो कोड को देखें।

local re_find = ngx.re.find function foo() ... end function bar() ... end function fn() ... end

यदि केवल फंक्शन foo re_find का उपयोग करता है, तो हम इसे निम्नलिखित रूप में संशोधित कर सकते हैं:

do local re_find = ngx.re.find function foo() ... end end function bar() ... end function fn() ... end

सारांश

"अधिक प्रश्न पूछने" के दृष्टिकोण से, Lua में 250 की सीमा कहां से आती है? यह आज के लिए हमारा सोचने का प्रश्न है। आप अपनी टिप्पणियों को छोड़ने और इस लेख को अपने सहयोगियों और दोस्तों के साथ साझा करने के लिए स्वागत करते हैं। हम एक साथ संवाद और सुधार करेंगे।