Tips Terbaik: Mengidentifikasi Konsep Unik dan Jebakan dalam Lua

API7.ai

October 12, 2022

OpenResty (NGINX + Lua)

Pada artikel sebelumnya, kita telah mempelajari tentang fungsi-fungsi pustaka yang terkait dengan tabel di LuaJIT. Selain fungsi-fungsi umum tersebut, hari ini saya akan memperkenalkan beberapa konsep unik atau kurang umum di Lua serta jebakan-jebakan umum Lua dalam OpenResty.

Weak Table

Pertama, ada weak table, sebuah konsep unik di Lua yang terkait dengan pengumpulan sampah (garbage collection). Seperti bahasa tingkat tinggi lainnya, Lua memiliki pengumpulan sampah otomatis, Anda tidak perlu memikirkan implementasinya, dan tidak perlu melakukan GC secara eksplisit. Pengumpul sampah akan secara otomatis mengumpulkan ruang yang tidak lagi dirujuk.

Namun, penghitungan referensi sederhana tidak cukup, dan terkadang kita membutuhkan mekanisme yang lebih fleksibel. Misalnya, jika kita memasukkan objek Lua Foo (table atau fungsi) ke dalam tabel tb, ini akan membuat referensi ke objek tersebut Foo. Bahkan jika tidak ada referensi lain ke Foo, referensi ke Foo dalam tb akan selalu ada, sehingga GC tidak dapat mengumpulkan memori yang ditempati oleh Foo. Pada titik ini, kita hanya memiliki dua opsi.

  • Satu adalah melepaskan Foo secara manual.
  • Kedua adalah membuatnya tetap berada di memori.

Contohnya, kode berikut.

$ 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

Namun, saya yakin Anda tidak ingin memori terus ditempati oleh objek yang tidak digunakan, terutama karena LuaJIT memiliki batasan memori 2G. Waktu pelepasan manual tidak mudah dan menambah kompleksitas pada kode Anda.

Maka, inilah saatnya weak table berperan. Lihat namanya, weak table. Pertama, ini adalah tabel, dan kemudian semua elemen dalam tabel ini adalah referensi lemah. Konsep ini selalu abstrak, jadi mari kita mulai dengan melihat kode yang sedikit dimodifikasi.

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

Seperti yang Anda lihat, objek yang tidak digunakan akan dibebaskan. Yang paling penting dari kode ini adalah baris berikut.

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

Apakah ini terasa familiar? Bukankah itu operasi dari metatable? Ya, sebuah tabel menjadi weak table ketika memiliki field __mode dalam metatable-nya.

  • Jika nilai __mode adalah k, maka key dari tabel tersebut adalah referensi lemah.
  • Jika nilai __mode adalah v, maka value dari tabel tersebut adalah referensi lemah.
  • Tentu saja, Anda juga dapat mengaturnya ke kv, yang menunjukkan bahwa baik key maupun value dari tabel ini adalah referensi lemah.

Salah satu dari ketiga weak table ini akan memiliki seluruh objek key-value-nya dikumpulkan begitu key atau value-nya dikumpulkan.

Dalam contoh kode di atas, nilai __mode adalah v, tb adalah array, dan nilai array adalah tabel dan objek fungsi sehingga dapat di-recycle secara otomatis. Namun, jika Anda mengubah nilai __mode menjadi k, maka tidak akan dibebaskan, misalnya, lihat kode berikut.

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

Kami hanya mendemonstrasikan weak table di mana value adalah referensi lemah, yaitu weak table dari tipe array. Secara alami, Anda juga dapat membangun weak table dari tipe hash table dengan menggunakan objek sebagai key, misalnya seperti berikut.

$ 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 '

Setelah memanggil collectgarbage() secara manual untuk memaksa GC, semua elemen dalam tabel tb akan dibebaskan. Tentu saja, dalam kode aktual, kita tidak perlu memanggil collectgarbage() secara manual, karena akan berjalan secara otomatis di latar belakang, dan kita tidak perlu khawatir tentang itu.

Namun, karena kita telah menyebutkan fungsi collectgarbage(), saya akan membahas sedikit tentang itu. Fungsi ini dapat menerima beberapa opsi berbeda dan default-nya adalah collect, yang merupakan GC penuh. Opsi lain yang berguna adalah count, yang mengembalikan jumlah ruang memori yang ditempati oleh Lua. Statistik ini berguna untuk melihat apakah ada kebocoran memori dan mengingatkan kita untuk tidak mendekati batas atas 2G.

Kode yang terkait dengan weak table lebih rumit untuk ditulis dalam praktik, kurang mudah dipahami, dan secara bersamaan, lebih banyak bug tersembunyi. Tidak perlu terburu-buru. Nanti, saya akan memperkenalkan proyek open source, menggunakan weak table yang menyebabkan masalah kebocoran memori.

Closure dan Upvalue

Beralih ke closure dan upvalue, seperti yang telah saya tekankan sebelumnya, semua nilai adalah warga negara kelas satu di Lua, termasuk fungsi. Ini berarti fungsi dapat disimpan dalam variabel, diteruskan sebagai argumen, dan dikembalikan sebagai nilai dari fungsi lain. Misalnya, kode contoh ini muncul dalam weak table di atas.

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

Ini adalah fungsi anonim yang disimpan sebagai nilai dari sebuah tabel.

Di Lua, definisi dari dua fungsi dalam kode berikut adalah setara. Namun, perhatikan bahwa yang terakhir menetapkan fungsi ke variabel, metode yang sering kita gunakan.

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

Selain itu, Lua mendukung penulisan fungsi di dalam fungsi lain, yaitu fungsi bersarang, seperti contoh kode berikut.

$ 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 '

Anda dapat melihat bahwa fungsi bar dapat membaca variabel lokal i di dalam fungsi foo dan memodifikasi nilainya, meskipun variabel tersebut tidak didefinisikan di dalam bar. Fitur ini disebut lexical scoping.

Fitur-fitur Lua ini adalah dasar untuk closure. Closure hanyalah fungsi yang mengakses variabel dalam lingkup leksikal dari fungsi lain.

Secara definisi, semua fungsi di Lua sebenarnya adalah closure, bahkan jika Anda tidak menyarangkannya. Ini karena kompilator Lua mengambil di luar skrip Lua dan membungkusnya dengan lapisan lain dari fungsi utama. Misalnya, beberapa baris kode sederhana berikut.

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

Setelah dikompilasi, akan terlihat seperti ini.

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

Dan fungsi fn menangkap dua variabel lokal dari fungsi utama, sehingga itu juga merupakan closure.

Tentu saja, kita tahu bahwa konsep closure ada di banyak bahasa, dan itu tidak unik untuk Lua, jadi Anda dapat membandingkan dan membandingkan untuk mendapatkan pemahaman yang lebih baik. Hanya ketika Anda memahami closure, Anda dapat memahami apa yang akan kita bahas tentang upvalue.

upvalue adalah konsep yang unik untuk Lua, yaitu variabel di luar lingkup leksikal yang ditangkap dalam closure. Mari kita lanjutkan dengan kode di atas.

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

Anda dapat melihat bahwa fungsi fn menangkap dua variabel lokal, foo dan bar, yang tidak berada dalam lingkup leksikal mereka sendiri dan bahwa kedua variabel ini sebenarnya adalah upvalue dari fungsi fn.

Jebakan Umum

Setelah memperkenalkan beberapa konsep di Lua, saya akan membahas jebakan-jebakan terkait Lua yang saya temui dalam pengembangan OpenResty.

Pada bagian sebelumnya, kami menyebutkan beberapa perbedaan antara Lua dan bahasa pengembangan lainnya, seperti indeks yang dimulai dari 1, variabel global default, dll. Dalam pengembangan kode aktual OpenResty, kita akan menemukan lebih banyak masalah terkait Lua dan LuaJIT, dan saya akan membahas beberapa yang lebih umum di bawah ini.

Berikut adalah pengingat bahwa bahkan jika Anda mengetahui semua jebakan, Anda pasti akan harus melewatinya sendiri untuk mendapatkan kesan. Perbedaannya, tentu saja, adalah Anda akan dapat keluar dari lubang dan menemukan inti masalah dengan cara yang jauh lebih baik.

Apakah indeks dimulai dari 0 atau 1?

Jebakan pertama adalah bahwa indeks Lua dimulai dari 1, seperti yang telah kami sebutkan berulang kali sebelumnya.

Tetapi saya harus mengatakan bahwa ini tidak sepenuhnya benar. Karena di LuaJIT, array yang dibuat dengan ffi.new diindeks dari 0 lagi:

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

Jadi, jika Anda ingin mengakses buf cdata dalam kode di atas, ingatlah bahwa indeks dimulai dari 0, bukan 1. Pastikan untuk memperhatikan tempat ini ketika Anda menggunakan FFI untuk berinteraksi dengan C.

Pencocokan Pola Reguler

Jebakan kedua adalah masalah pencocokan pola reguler, dan ada dua set metode pencocokan string yang berjalan paralel di OpenResty: pustaka string Lua dan API ngx.re.* OpenResty.

Pencocokan pola reguler Lua memiliki format uniknya sendiri dan ditulis berbeda dengan PCRE. Berikut adalah contoh sederhana.

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

Kode ini mengekstrak bagian numerik dari string, dan Anda akan melihatnya sangat berbeda dengan ekspresi reguler yang kita kenal. Pustaka pencocokan reguler Lua mahal untuk dipelihara dan berkinerja rendah - JIT tidak dapat mengoptimalkannya, dan pola yang telah dikompilasi sekali tidak di-cache.

Jadi, ketika Anda menggunakan pustaka string bawaan Lua untuk find, match, dll., jangan ragu untuk menggunakan ngx.re OpenResty jika Anda membutuhkan sesuatu seperti reguler. Saat mencari string tetap, kami mempertimbangkan menggunakan mode biasa untuk memanggil pustaka string.

Berikut adalah saran: Di OpenResty, kami selalu memprioritaskan API OpenResty, kemudian API LuaJIT, dan menggunakan pustaka Lua dengan hati-hati.

Pengkodean JSON Tidak Membedakan Array dan Dict

Jebakan ketiga adalah bahwa pengkodean JSON tidak membedakan antara array dan dict; karena Lua hanya memiliki satu struktur data, table, ketika JSON mengkodekan tabel kosong, tidak ada cara untuk menentukan apakah itu array atau kamus.

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

Misalnya, kode di atas menghasilkan {}, yang menunjukkan bahwa pustaka cjson OpenResty mengkodekan tabel kosong sebagai kamus secara default. Tentu saja, kita dapat mengubah default global ini dengan menggunakan fungsi encode_empty_table_as_object.

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

Kali ini, tabel kosong dikodekan sebagai array [].

Namun, pengaturan global ini memiliki dampak yang signifikan, jadi bisakah kita menentukan aturan pengkodean untuk tabel tertentu? Jawabannya tentu saja ya, dan ada dua cara untuk melakukannya.

Cara pertama adalah menetapkan userdata cjson.empty_array ke tabel yang ditentukan sehingga akan diperlakukan sebagai array kosong ketika dikodekan dalam JSON.

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

Namun, terkadang kita tidak yakin apakah tabel yang ditentukan selalu kosong. Kami ingin mengkodekannya sebagai array ketika kosong, jadi kami menggunakan fungsi cjson.empty_array_mt, yang merupakan metode kedua kami.

Ini akan menandai tabel yang ditentukan dan mengkodekannya sebagai array ketika tabel kosong. Seperti yang Anda lihat dari nama cjson.empty_array_mt, ini diatur menggunakan metatable, seperti dalam operasi kode berikut.

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

Batasan Jumlah Variabel

Mari kita lihat jebakan keempat, batasan jumlah variabel. Lua memiliki batas atas pada jumlah variabel lokal dan jumlah upvalue dalam sebuah fungsi, seperti yang dapat Anda lihat dari kode sumber Lua.

/* @@ LUAI_MAXVARS adalah jumlah maksimum variabel lokal per fungsi @* (harus lebih kecil dari 250). */ #define LUAI_MAXVARS 200 /* @@ LUAI_MAXUPVALUES adalah jumlah maksimum upvalue per fungsi @* (harus lebih kecil dari 250). */ #define LUAI_MAXUPVALUES 60

Kedua ambang batas ini dikodekan secara keras ke 200 dan 60, masing-masing, dan meskipun Anda dapat memodifikasi kode sumber secara manual untuk menyesuaikan kedua nilai ini, mereka hanya dapat diatur hingga maksimum 250.

Secara umum, kita tidak melebihi ambang batas ini. Namun, saat menulis kode OpenResty, Anda harus berhati-hati untuk tidak terlalu banyak menggunakan variabel lokal dan upvalue, tetapi menggunakan do ... end sebanyak mungkin untuk mengurangi jumlah variabel lokal dan upvalue.

Misalnya, mari kita lihat kode pseudo berikut.

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

Jika hanya fungsi foo yang menggunakan re_find, maka kita dapat memodifikasinya sebagai berikut:

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

Ringkasan

Dari sudut pandang "bertanya lebih banyak", dari mana ambang batas 250 di Lua berasal? Ini adalah pertanyaan pemikiran kita hari ini. Anda dipersilakan untuk meninggalkan komentar Anda dan membagikan artikel ini dengan rekan dan teman Anda. Kami akan berkomunikasi dan meningkatkan bersama.