Concurrencyda Shared-State
Message passing(Xabarni uzatish) - bu concurrencyni boshqarishning yaxshi usuli, ammo bu yagona emas. Yana bir usul bir nechta(multiple) threadlar bir xil umumiy ma'lumotlarga(shared data) kirishlari mumkin. Go tilidagi texnik hujjatlardagi shiorning ushbu qismini yana bir bor ko'rib chiqing: "xotirani almashish(sharing memory) orqali muloqot(comminicate) qilmang."
Xotirani almashish(sharing memory) orqali muloqot(comminication) qanday ko'rinishga ega bo'lar edi? Bundan tashqari, nima uchun message-passing enthusiastlar memory sharingdan foydalanmaslik haqida ogohlantiradilar?
Qaysidir ma'noda, har qanday dasturlash tilidagi kanallar bitta ownershiplik huquqiga o'xshaydi, chunki qiymatni kanalga o'tkazganingizdan so'ng, siz boshqa qiymatdan foydalanmasligingiz kerak. Shared memory concurrencyda bir nechta ownershiplik huquqiga o'xshaydi: concurrencyda bir nechta threadlar bir xil xotira joyiga(memory location) kirishi mumkin. 15-bobda ko'rganingizdek, smart pointerlar bir nechta ownershiplik qilish imkoniyatini yaratdi, bir nechta(multiple) ownershiplik murakkablikni oshirishi mumkin, chunki bu turli ownerlarni boshqarish kerak. Rust type tizimi va ownershiplik qoidalari ushbu boshqaruvni to'g'ri bajarishga katta yordam beradi. Misol uchun, shared memory uchun eng keng tarqalgan concurrency primitivlaridan biri bo'lgan mutexlarni ko'rib chiqaylik.
Bir vaqtning o'zida bitta threaddan ma'lumotlarga kirishga ruxsat berish uchun mutexlardan foydalanish
Mutex bu mutual exclusion ning qisqartmasi boʻlib, mutex istalgan vaqtda baʼzi maʼlumotlarga faqat bitta threadga kirish imkonini beradi. Mutexdagi ma'lumotlarga kirish uchun thread birinchi navbatda mutexning *lock(qulf)*ni olishni so'rab kirishni xohlashini bildirishi kerak. Lock(qulf) - bu mutexning bir qismi bo'lgan ma'lumotlar tuzilmasi bo'lib, u hozirda ma'lumotlarga kimning eksklyuziv kirish huquqiga ega ekanligini kuzatib boradi. Shuning uchun, mutex qulflash tizimi(locking system) orqali o'zida mavjud bo'lgan ma'lumotlarni himoya qilish(guarding) sifatida tavsiflanadi.
Mutexlardan foydalanish qiyinligi bilan mashhur, chunki siz ikkita qoidani eslab qolishingiz kerak:
- Ma'lumotlardan foydalanishdan oldin siz qulfni olishga harakat qilishingiz kerak.
- Mutex himoya qiladigan ma'lumotlar bilan ishlashni tugatgandan so'ng, boshqa threadlar qulfni(lock) olishi uchun ma'lumotlarni qulfdan chiqarishingiz(unlock) kerak.
Mutexni tushunish uchun bitta mikrofon bilan konferensiyada guruh muhokamasining haqiqiy hayotiy misolini tasavvur qiling. Panel ishtirokchisi gapirishdan oldin mikrofondan foydalanishni xohlashini so'rashi yoki signal berishi kerak. Mikrofonni olishganda, ular xohlagancha gaplashishi mumkin va keyin mikrofonni gapirishni so'ragan keyingi ishtirokchiga beradi. Agar panel ishtirokchisi mikrofon bilan ishlashni tugatgandan so'ng uni o'chirishni unutib qo'ysa, boshqa hech kim gapira olmaydi. Agar umumiy mikrofonni boshqarish noto'g'ri bo'lsa, panel rejalashtirilganidek ishlamaydi!
Mutexlarni boshqarish juda qiyin bo'lishi mumkin, shuning uchun ko'p odamlar kanallarga(channel) ishtiyoq bilan qarashadi. Biroq, Rust type tizimi va ownershiplik qoidalari tufayli siz qulflash(locking) va qulfni noto'g'ri ochishingiz(unlocking) mumkin emas.
Mutex<T>
API
Mutexdan qanday foydalanishga misol sifatida, keling, 16-12 ro'yxatda ko'rsatilganidek, bitta threadli kontekstda mutexdan foydalanishdan boshlaylik:
Fayl nomi: src/main.rs
use std::sync::Mutex; fn main() { let m = Mutex::new(5); { let mut raqam = m.lock().unwrap(); *raqam = 6; } println!("m = {:?}", m); }
Ro'yxat 16-12: Mutex<T>
API-ni soddaligi uchun single-threadli kontekstda oʻrganish
Ko'pgina turlarda(type) bo'lgani kabi, biz bog'langan new
funksiyasidan foydalangan holda Mutex<T>
ni yaratamiz.
Mutex ichidagi ma'lumotlarga kirish uchun biz qulfni olish uchun lock
metodidan foydalanamiz. Bu chaqiruv joriy threadni bloklaydi, shuning uchun u bizni qulflash navbati kelmaguncha hech qanday ishni bajara olmaydi.
Qulfni ushlab turgan boshqa thread panic qo'zg'atsa, lock
chaqiruvi muvaffaqiyatsiz bo'ladi. Bunday holda, hech kim qulfni qo'lga kirita olmaydi, shuning uchun biz unwrap
ni tanladik va agar shunday vaziyatda bo'lsak, bu threadni panic qo'yishni tanladik.
Qulfni qo'lga kiritganimizdan so'ng, biz bu holatda num
deb nomlangan return qiymatini ichidagi ma'lumotlarga o'zgaruvchan reference sifatida ko'rib chiqishimiz mumkin. Tur(type) tizimi m
dagi qiymatni ishlatishdan oldin qulfni olishimizni ta'minlaydi. m
turi i32
emas, Mutex<i32>
, shuning uchun biz i32
qiymatidan foydalanish uchun lock
ni chaqirishimiz kerak. Biz unuta olmaymiz; aks holda turdagi tizim bizga ichki i32
ga kirishga ruxsat bermaydi.
Taxmin qilgan bolishingiz mumkinki Mutexlock
qo'ng'irog'i MutexGuard deb nomlangan ochish
qo'ng'irog'i bilan oralgan LockResult-ga o'ralgan aqlli ko'rsatgichni qaytaradi . MutexGuard
ko'rsatkichi esa bizning ichki ma'lumotlarimizga ishora
qilish uchun Deref
ni amalga oshiradi( Derefdan foydalanadi). Aqlli ko'rsatgichda Drop
ilovasi ham mavjud bo'lib, MutexGuard qo'llanilish doirasidan
tashqariga chiqqanda avtomatik ravishda qulfni chiqaradi va bu esa ichki doiraning oxirida sodir bo'ladi. Natijada, biz qulfni(lock) bo'shatishni unutib
qo'ymaymiz va asosiysi mutexni boshqa threadlar tomonidan ishlatilishini bloklaymiz, chunki qulfni(lock) chiqarish avtomatik ravishda sodir bo'ladi.
Qulfni tashlaganimizdan so'ng, biz mutex qiymatini print qilishimiz(chop etishimiz ) va ichki i32
ni 6 ga o'zgartira olganimizni ko'rishimiz mumkin.
Bitta Muteks<T>
ni Bir nechta mavzular o'rtasida ulashish(almashtirish):
Keling, Mutex<T>
-dan foydalanib, bir nechta oqimlar o'rtasida qiymatni share qilishga(qiymatni almashtirishga) harakat qilaylik. Biz 10 ta threadni
aylantiramiz va ularning har biri hisoblagich qiymatini 1 ga oshiradi, shuning uchun hisoblagich 0 dan 10 gacha boradi. 16-13 ro'yxatdagi keyingi misolda
kompilyator xatosi (compiler error)bo'ladi va biz bu xatoni o'rganish uchun ishlatamiz. Mutex<T>
-dan foydalanish va Rust uni to'g'ri ishlatishimizga
qanday yordam berishi haqida ko'proq o'rganamiz.
Fayl nomi: src/main.rs
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Mutex::new(0);
let mut handles = vec![];
for _ in 0..10 {
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Ro'yxat 16-13: Mutex<T>
tomonidan qo'riqlanadigan hisoblagichni har biri o'nta threaddan amalga oshirishi.
Mutex<T>
ni ichida i32
ni ushlab turish uchun hisoblagich o'zgaruvchisini yaratamiz, xuddi 16-12 ro'yxatdagi kabi(listing 16-12). Keyingi amal esa,
biz raqamlar oralig'ida takrorlash orqali 10 ta thread yaratamiz. Biz thread::spawn
dan foydalanamiz va barcha threadlarga bir xil yopilishni beramiz:
hisoblagichni threadga o'tkazish uchun ishlatiladigan qulflash usulini amalga oshirish orqali(chaqirish orqali) orqali Mutex<T>
da blokirovkaga ega
bo'ladi va keyin mutexdagi qiymatga 1 qo'shiladi. Thread o'zining yopilishini tugatgandan so'ng, num
doirasi tashqariga chiqadi va boshqa thread uni
olishi uchun qulfni bo'shatadi.
Asosiy threadda biz barcha birlashma tutqichlarini yig'amiz. Keyin, 16-2 ro'yxatdagidek,barcha threadlar tugashiga ishonch hosil qilish uchun har bir
tutqichga join
chaqiramiz. O'sha paytda asosiy thread qulfni oladi va ushbu dasturning natijasini print(chop etadi).
Bu misol tuzilmasligiga ishora qilingan. Endi nima uchunligini o'ylab ko'raylik!
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0382]: use of moved value: `counter`
--> src/main.rs:9:36
|
5 | let counter = Mutex::new(0);
| ------- move occurs because `counter` has type `Mutex<i32>`, which does not implement the `Copy` trait
...
9 | let handle = thread::spawn(move || {
| ^^^^^^^ value moved into closure here, in previous iteration of loop
10 | let mut num = counter.lock().unwrap();
| ------- use occurs due to use in closure
For more information about this error, try `rustc --explain E0382`.
error: could not compile `shared-state` due to previous error
Xato xabari counter
(hisoblagich) qiymati tsiklning oldingi iteratsiyasida ko'chirilganligini bildiradi. Rust bizga qulflash counter
(hisoblagichining)
egaligini bir nechta mavzularga o'tkaza olmasligimizni aytadi. Keling, 15-bobda muhokama qilgan bir nechta egalik usuli bilan kompilyator xatosini
tuzataylik.
Bir nechta mavzular bilan bir nechta egalik
15-bobda mos yozuvlar hisoblangan qiymatni yaratish uchun aqlli ko'rsatkich RcMutex<T>
-ni Rc <T>
-ga 16-14-listingda o'rab olamiz va egalikni threadga ko'chirishdan oldin Rc<T>
-ni
klonlaymiz(nusxasini yaratmoq, cloning).
Fayl nomi: src/main.rs
use std::rc::Rc;
use std::sync::Mutex;
use std::thread;
fn main() {
let counter = Rc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Rc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
}
Listing 16-14: Rc<T>
ni ishlatib, bir nechta iplar (threads) Mutex<T>
ga egalik qilishiga imkon berishga urinish.
Yana bir bor, biz kompilyatsiya qilamiz va... turli xatolarni olamiz! Kompilyator bizga ko'p narsani o'rgatmoqda.
$ cargo run
Compiling shared-state v0.1.0 (file:///projects/shared-state)
error[E0277]: `Rc<Mutex<i32>>` cannot be sent between threads safely
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ------------- ^------
| | |
| ______________________|_____________within this `[closure@src/main.rs:11:36: 11:43]`
| | |
| | required by a bound introduced by this call
12 | | let mut num = counter.lock().unwrap();
13 | |
14 | | *num += 1;
15 | | });
| |_________^ `Rc<Mutex<i32>>` cannot be sent between threads safely
|
= help: within `[closure@src/main.rs:11:36: 11:43]`, the trait `Send` is not implemented for `Rc<Mutex<i32>>`
note: required because it's used within this closure
--> src/main.rs:11:36
|
11 | let handle = thread::spawn(move || {
| ^^^^^^^
note: required by a bound in `spawn`
--> /rustc/d5a82bbd26e1ad8b7401f6a718a9c57c96905483/library/std/src/thread/mod.rs:704:8
|
= note: required by this bound in `spawn`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `shared-state` due to previous error
Afsus, bu xato xabari juda uzun va yoqimsizroq(rasmiyligi uchun) ekan! Bu yerda diqqat qilish kerak bo'lgan muhim qism:
Rc<Mutex<i32>> ` threadlar o'rtasida xavfsiz yuborilishi mumkin emas
. Kompilyator bizga buning sababini ham aytib beradi: Send
xususiyati
Rc<Mutex<i32>>
uchun amalga oshirilmagan. Keyingi bo'limda Send
(Yuborish) haqida gaplashamiz: bu biz threadlar bilan ishlatadigan turlarni bir
vaqtda vaziyatlarda foydalanishga mo'ljallanganligini ta'minlaydigan xususiyatlardan biridir.
Afsuski, Rc<T>
ni threadlar bo'ylab almashish xavfsiz emas(yuqorida ham aytilishicha mumkin ham emas). Rc<T>
mos yozuvlar sonini boshqarganda, u clone
(klonlash) uchun har bir qo'ng'iroq uchun hisobni qo'shadi va har bir clone(klon) tushirilganda hisobdan ayiradi. Ammo hisobdagi o'zgarishlarni boshqa
oqim bilan to'xtatib qo'ymasligiga ishonch hosil qilish uchun u parallellik ibtidoiylaridan(parallallik ibtidoiysi bu concurrency yani raqobatga tegishli
mavzu) foydalanmaydi. Bu noto'g'ri hisob-kitoblarga olib kelishi mumkin - nozik xatolar, o'z navbatida, xotiraning oqishi yoki biz bilan ishlash
tugashidan oldin qiymatning tushib ketishiga olib kelishi mumkin. Bizga aynan Rc<T>
ga o'xshash tur kerak bo'ladi, ammo u mos yozuvlar soniga
o'zgartirish kiritadi.
Arc<T>
bilan atomik havolalarni hisoblash
Yaxshiyamki, Arc<T>
Rc<T>
kabi bir xil vaziyatlarda foydalanish uchun xavfsiz tur. A atomik degan ma'noni anglatadi, ya'ni bu atomik havola orqali
hisoblangan tur. Atomlar parallellik ibtidoiyning(concurrency:konkurentlik) qo'shimcha turi bo'lib,bu yerda batafsik ko'rib chiqolmaymiz: batafsil
ma'lumot uchun std::sync::atomic
uchun standart kutubxona hujjatlariga(dokumentatsiyasiga) qarang. Shu nuqtada, atomlar ibtidoiy turlar kabi ishlashini
bilishingiz kerak, lekin ularni threadlar bo'ylab almashish xavfsizdir.
Keyin nima uchun barcha ibtidoiy turlar atom emasligi va nega standart kutubxona turlari sukut bo'yicha Arc<T>
dan foydalanish uchun amalga
oshirilmaganligi haqida hayron bo'lishingiz mumkin. Buning sababi shundaki, thread xavfsizligi faqat sizga kerak bo'lganda to'lamoqchi bo'lgan ishlash
jazosi bilan birga keladi(PERFORMANCE PENALTY-IJRO,BAJARISH UCHUN JAZO) .Agar siz faqat bitta oqim ichidagi qiymatlar ustida amllarni bajarayotgan
bo'lsangiz yani atomik kafolatlarni bajarish shart bo'lmasa, kodingiz tezroq ishlashi mumkin.
Keling, misolimizga qaytaylik: Arc<T>
va Rc<T>
bir xil APIga ega, shuning uchun biz dasturimizni use
(foydalanish) qatorini, new
(yangi) chaqiruvni
va clone
(klonlash) uchun qo'ng'iroqni o'zgartirish orqali tuzatamiz. 16-15 ro'yxatdagi kod nihoyat togri boladi:
Fayl nomi: src/main.rs
use std::sync::{Arc, Mutex}; use std::thread; fn main() { let counter = Arc::new(Mutex::new(0)); let mut handles = vec![]; for _ in 0..10 { let counter = Arc::clone(&counter); let handle = thread::spawn(move || { let mut num = counter.lock().unwrap(); *num += 1; }); handles.push(handle); } for handle in handles { handle.join().unwrap(); } println!("Result: {}", *counter.lock().unwrap()); }
Ro'yxat 16-15: Mutex<T>
-ni o'rash uchun Arc<T>
dan foydalanish, bir nechta mavzular bo'ylab egalik huquqini baham ko'rish
uchun
Ushbu kod quyidagilarni print qiladi:
Result: 10
Biz uddaladik! Biz 0 dan 10 gacha hisobladik, bu katta ishdek korinmasligi mumkin, ammo bu bizga Mutex<T>
va thread xavfsizligi haqida ko‘p narsalarni
o‘rgatadi. Hisoblagich faqat ko'paytirishdan koproq ish qila olishini orgatdi. Ushbu strategiyadan foydalanib, siz hisobni mustaqil qismlarga
bo'lishingiz, bu qismlarni threadlar bo'ylab ajratishingiz va keyin Mutex<T>
dan foydalanib, har bir thread yakuniy natijani o'z qismi bilan yangilashi
mumkin.
E'tibor bering, agar siz oddiy raqamli amallarni bajarayotgan bo'lsangiz, standart kutubxonaning std::sync::atomic
modulida taqdim etilgan Mutex<T>
turlaridan oddiyroq turlar mavjud. Ushbu turlar ibtidoiy turlarga xavfsiz, parallel, atomik kirishni ta'minlaydi va ushbu misol uchun Mutex<T>
ning
ibtidoiy turi bilan foydalanishni tanladik, shuning uchun Mutex<T>
qanday ishlashiga e'tibor qaratishimiz mumkin.
RefCell<T>
/Rc<T>
va Mutex<T>
/Arc<T>
o'rtasidagi o'xshashliklar
Hisoblagich(counter) o'zgarmasligini payqagan bo'lishingiz mumkin, lekin biz uning ichidagi qiymatga o'zgaruvchan havolani olishimiz mumkin; bu Mutex<T> Cell
oilasi kabi ichki o'zgaruvchanlikni qollab quvvatlaydi. Xuddi shu tarzda biz Rc<T>
ichidagi tarkibni o'zgartirishga ruxsat berish uchun 15-bobda
RefCell<T>
dan foydalanganmiz, Arc<T>
ichidagi tarkibni mutatsiya qilish uchun Mutex<T>
dan foydalanamiz.
Yana bir muhim ma' lumot, Mutex<T>
dan foydalanganda Rust sizni barcha turdagi mantiqiy xatolardan himoya qila olmaydi. 15-bobda Rc<T>
dan
foydalanish oziga xos yozuvlar sikllarini yaratish xavfi bilan kelganligini eslang, bu erda ikkita Rc<T>
qiymati bir-biriga tegishli bo'lib, xotira
susayishi, tanqisligiga olib keladi. Xuddi shunday, Mutex<T>
ham boshi berk deadlocks(ko'chalarni) yaratish xavfi bilan birga keladi. Bular amal ikkita
resursni bloklashi kerak bo'lganda sodir bo'ladi va ikkita thread har biri locks(qulflardan) birini qo'lga kiritib. Agar siz ziddiyatlarga qiziqsangiz,
tanqislik deadlocks(ko'chasiga) ega Rust dasturini yaratishga harakat qiling; keyin har qanday tilda mutekslar uchun ziddiyatni yengilashtirish, yechim
topish strategiyalarini o'rganing va Rustda ularni amalga oshirishga kirishing. Mutex<T>
va MutexGuard
uchun standart kutubxona API hujjatlari
foydali ma'lumotlarni taqdim etadi.
Biz ushbu bobni Send
(Yuborish) va Sync
(Sinxronlashtirish) xususiyatlari va ularni maxsus turlar bilan qanday ishlatishimiz haqida gapirib,
yakunlaymiz.