Node.js worker threads: concurrency va parallelism farqi

Assalamu Alaykum bugunThread ularning parallel va concurrentishlashi nimaligi hamda nodejsda worker threadlar bilan ishlashni ko'rib chiqamiz.

Birinchi bo'lib dasturlarimiz kompyuterda qanday ishlashini bilib olishimiz kerak .

Kompyuter processlar bilan qanday ishlaydi ?

Har bir processda o'zining statusi bo'ladi ular ready , sleep va running . ready status process CPU coreda ishga tushishga tayyorligini bildiradi , running status esa u hozir CPUda ishlayotganini bildiradi, sleep status esa bu blocking status bo'lib timer tufayli yoki tarmoqdan so'rov kelishini kutishi mumkin. Bundan tashqari uyerda bir dispatcher bo'lib u ready statusdagi processlar running statusga o'tkazadi va ularni CPUga olib o'tadi va bu yerda ma'lum muddat ishlaydi , agar shu vaqt ichida qilinishi kerak bo'lgan ishlar tugasa u tugatiladi mabodo tugatilmasa qayta ready statusga o'tib yana CPUga qaytib kelishni kutadi. Agarda running statusda blocking so'rov qilsa os unda hozir ishga tushuriladigan hech narsa yo'q bo'lganligi uchun uni sleep statusga o'tkaziladi, blockin so'rov tugagandan so'ng esa u qayta ready statusga o'tkazilib ready queue ga qo'shiladi. Process shu paytda instructsiyalarga bo'linadi , uni CPUdan ready statusga olib o'tganda u to'xtagan joyda pointer bo'ladi va keyingi safar qayerdan boshlashni biladi.

CPU running
CPU running

Concurrency vs parallelism

CPU tasklarni qanday ishga tushurishini bilib oldik endi concurensiya va parallelismni o'rganamiz. Concurrent ishlash degan bir vaqtda bir necha ishlarni amalga oshirish ammo istalgan bir vaqtda bir ishni bajarayotgan bo'ladi. Parallelism esa bir necha ishlarni birgalikda bajarishga aytiladi. Concurrencyda u hamma ishlarni bir vaqtda qilayotganga o'xshaydi ammo bu tez ishlagani uchun bizga shunday tuyuladi. Tepadagi rasmda bunga misol ko'rish mumkin , concurrency ishlashda context switching muammosi bor masalan task qayergacha bajarilganin saqlash va yana uni o'sha yerdan davom ettirish qo'shimcha vaqt oladi. Parallelismda har bir core alohida bir vazifa uchun masul bo'ladi va ular mustaqil ularni tugata oladi. Concurrency I/O operationlarni kutish uchun qulay chunki bu paytda boshqa ishni qilishi mumkin va bu bizga CPUdan ko'proq foydalanishga yordam beradi , parallel ishlash esa uni tezroq tugatishga yordam beradi. Hozirda concurency parallelism bilan birga ishlaydi ya'ni core concurrent vazifalarni qabul qiladi va har bir core shunday ishlaydi va bu parallel concurrencyga olib keladi.

Note: 2.6 Ghz CPU degan u bir soniya ichida 2.6 million operatsiyani bajaraoladi degani (switchlarni o'chirib yoqish)

concurrency va parallel
concurrency va parallel

Process va thread nima farqi bor ? Davom ettirishdan oldin ,u maqolani o'qishni tavsiya beraman.

Thread

Threadni oddiy qilib qilinishi kerak bir nechta operatsiyalar to'plami deb qarasak bo'ladi. Threadda ham processdagi kabi 3 xil status mavjud va har bir processda kamida bitta thread bo'ladi. Biz processdan foydalangan holda o'zimiga kerakli ishni qilishimiz mumkin ekan thread bizga nimaga kerak.Thread yaratish process yaratishdan bir necha barobar tezroq (20 % emas 5–10 marta) bundan tashqari bir processdagi threadlar shared memoryga ega bo'ladi. Threadning ishlatishning oson usullaridan biri bu loglarni backgroundda yozib turadigan worker threaddan foydalanish.

Masalan tepadagi concurrency va parallelismdan misol olsak sizda 9 ta thread bor hamda 6 CPU corelik kompyuterda ishlaysiz. Ulardan 6 tasi shu har bir corega ishga tushish uchun boradi va qolgan 3 tasi ready statusda kutadi agarda birortasi blocking ish qilayotgan bo'lsa wait ga o'tib boshqa threadga o'rnini bo'shatib beradi.

Thread yaratish

Biz Nodejda worker_threads modulidan foydalangan holda threadlar yaratamiz.

const { Worker } = require("worker_threads")
console.log("Main thread")
new Worker("./worker.js")
console.log("Worker ishga tushdi")
worker threads
worker threads

Har bir threadning o'zining contexti (callstack , memory) va event loopi bo'ladi. Bu xuddi main threadga o'xshaydi ammo process.exit() faqatgina shu threadni o'chiradi.

Threadlarga ma'lumot uzatish(message passing)orqali muloqot qilish

Buni workerData orqali child threadlarga ma'lumot o'tkazish mumkin.

const { Worker } = require("worker_threads")
console.log("Main thread")
new Worker("./worker.js", {workerData: "Sending something from Main thread"})
const { workerData } = require("worker_threads")
console.log("Data came from parent:", workerData)
message passing
message passing

Biz workerData ga ma'lumot berganda threadga kelgan ma'lumot bu aynan biz bergan emas u V8 orqali klonlangan ma'lumot bo'ladi.U structured clone ya'ni V8 dagiv8.serializemetodini ishlatadi va u bu ma'lumot uchun yangi buffer yaratadi va yangi thread ma'lumotni shu yerdan oladi.

Bu usul Main threadni uning childlariga ma'lumot uzatishi edi.

Bundan tashqari threadlar bir biri bilan message channellar orqali ham komunikatsiya qila oladi.

Message channel orqali kommunikatsiya

MessageChannel objectini worker_threads modulidan olamiz. U ikki port (Nega port deb nomlangan bilmadim) yaratadi va ularni bir biriga bog'laydi . port1.postMessage("text") orqali ma'lumot uzatiladi , bu ham structuredclone qilingan bo'ladim boshqa port esa buni port2.on("message") orqali olishi mumkin.

const { Worker , MessageChannel} = require("worker_threads")
const channel = new MessageChannel()
const mainToWorker = channel.port1
const workerPort = channel.port2
mainToWorker.postMessage({text: "Message from main thread to worker "})
mainToWorker.on("message", (msg => {
  console.log(`Worker msg : ${msg.text}`)
}))
new Worker("./worker.js", {workerData:{port:workerPort} , transferList:[workerPort]})
const { workerData } = require("worker_threads")
const port = workerData.port
port.on("message", (msg => {
  console.log(`Data came from Main thread: ${msg.text}` )
}))
port.postMessage({text: `Hi from child thread`})
message channel
message channel

Tepadagi misol, orqali biz main thread va child thread o'rtasida aloqa o'rnatdik , xuddi shu ishni boshqa threadlarga ham qilish orqali biz hamma threadlarni main threadga bog'lashimiz mumkin.

Threadlar o'rtasidagi kommunikatsiya

Child threadlar orasidagi bog'lanish yaratish uchun biz yaratgan portlarni ikkalasini ham child threadlarga berib yuboramiz. Bundan tashqari ularni transferlistga qo'shishimiz ham kerak chunki bularga egalik qiladigan threadlar almashmoqda. Ya'ni 3ta narsa transferlistga qo'shilishi kerak port, array buffer va filehandle va bular bufferlanmagan holda to'g'ridan to'g'ri egalik yangi threadga o'tib ketadi va bular transferrable objectlar deyiladi.

const { Worker , workerData, MessageChannel} = require("worker_threads")
const channel = new MessageChannel()
const port1 = channel.port1
const port2 = channel.port2
const worker1 = new Worker("./worker.js", {workerData:{port:port1} , transferList:[port1]})
const worker2 = new Worker("./worker.js", {workerData:{port:port2}, transferList:[port2]})
const { workerData } = require("worker_threads")
const port = workerData.port
port.on("message", (msg => {
  console.log(`Data came from child thread: ${msg.text}` )
}))
port.postMessage({text: `Hi from child thread`})
2ta worker threads bilan
2ta worker threads bilan

main va 2 worker threads orasida bog'lanish

const { Worker , MessageChannel} = require("worker_threads")
const channel1 = new MessageChannel()
const mainToWorkerOne = channel1.port1
const workerOnePort = channel1.port2
const channel2 = new MessageChannel()
const mainToWorkerTwo = channel2.port1
const workerTwoPort = channel2.port2
mainToWorkerOne.postMessage({text: "Message from main thread to worker 1"})
mainToWorkerOne.on("message", (msg => {
  console.log(`Worker 1 msg : ${msg.text}`)
}))
mainToWorkerTwo.postMessage({text: "Message from main thread to worker 2"})
mainToWorkerTwo.on("message", (msg => {
  console.log(`Worker 1 msg : ${msg.text}`)
}))
const worker1 = new Worker("./worker.js", {workerData:{port:workerOnePort} , transferList:[workerOnePort]})
const worker2 = new Worker("./worker.js", {workerData:{port:workerTwoPort}, transferList:[workerTwoPort]})
const { workerData } = require("worker_threads")
const port = workerData.port
port.on("message", (msg => {
  console.log(`Data came from Main thread: ${msg.text}` )
}))
port.postMessage({text: `Hi from child thread`})

Nodejs syntactic sugar bilan

Odatda kommunikatsiya main va child thread o'rtasida bo'lganligi uchun Nodejs buni osonlashtirgan va biz har bir uchun channellar ochishimiz kerak emas (Ammo under the hood hammasi oldingiday ishlaydi bu faqat kod yozishni osonlashtirish uchun). Portni workerda olish uchun worker_threads moduldan parentPort objectini olish kerak.

const { Worker } = require("worker_threads")
const thread1 = new Worker("./new-worker.js")
thread1.postMessage("Easy way of talking from Main")
thread1.on("message", (msg)=> {
  console.log("Data from child: ", msg)
})
const { parentPort } = require("worker_threads")
parentPort.postMessage("Hi again from child")
parentPort.on("message", (msg)=> {
  console.log("Data from main : " ,msg)
})

Agarda child threadning child bilan maindan gaplashmoqchi bo'lsangiz yana channellardan foydalanishingiz kerak. Har bir request uchun thread yaratish yaxshi emas undan ko'ra workerpool yaratib undan foydalangan qulayroq.

isMainThread

isMainThread bu worker thread module tomonidan berilgan o'zgaruvchi bo'lib u thread main yoki childligini aniqlashga yordam beradi . Nodejsda main thread boshqa tillardagichalik emas yani funksiyalar share qilish yoki boshqa xususiyatlarga ega emas u shunchaki coordinatorhisoblanadi.

CPU intensive va I/O intensive

Worker threadslarni ishlatishimiz uchun CPU va I/O intensive nimaligini yaxshi bilib olishimiz kerak. CPU intensive degani bu vazifa ko'p vaqtini CPUda ishlayotganrunningholatda o'tkazishi kerak va uni tugashi ko'p vaqtni oladi (bu video yoki rasm processing bo'lishi mumkin).

I/O intensive esa input va output bir processdan boshqasiga message uzatish yoki qabul qilish bu holatda odatda ko'p vaqt kuriladi va message kelganda u process qilinishi mumkin. Bunga millionlab http so'rovlar jo'natish , file systemdan ma'lumotlar o'qish misol bo'ladi.

Hamma kodlarni shu yerdan topishinigz mumkin. Repo

Xulosa

Xulosa qilib aytadigan bo'lsak worker threadslar main threadni bloklashi va dastur javob qaytarmaydigan bo'lib qolishini oldini olishga yordam beradi va CPU intensive vazifalarni main threaddan tashqarida amalga oshira oladi hamda bular yangi process yaratishdan ancha yengilroq.

Note: Nodejs I/O operatsiyalar o'zida ancha yaxshi optimizatsiyalangan