چند رشته برای زبان آموزان بی حوصله زنگ.

به عنوان یک توسعه دهنده فول استک، همیشه سعی می کنم دانش جدیدی به دست بیاورم. من
چند سال پیش در مورد زبان برنامه نویسی Rust شنیده بودم، اما دست برای آزمایش نمونه های واقعی دراز نکرد. من چند روز پیش مدتی را در Rust گذراندم و می خواهم تجربیاتی را با شما به اشتراک بگذارم.
ابتدا، میخواهم توجه داشته باشم که این مقاله عمدتاً برای توسعهدهندگان NodeJS/Javascript که قبلاً در مورد Rust چیزی نشنیدهاند یا برای کسانی که قدمهای اول را امتحان میکنند مناسب است.
دوم، من فقط یک موضوع را توضیح می دهم که به آن پرداختم، و هدفم ارائه مجموعه نهایی دانش به شما نیست. با وجود این، امیدوارم مثال من شما را در مورد یادگیری Rust درگیر کند.
من فکر می کنم، در این مرحله، شما یک سوال از من بپرسید. به عنوان یک توسعه دهنده وب، چرا باید Rust را یاد بگیرم؟ من این سوال را پیش بینی کردم. راستش این مقاله ادامه منطقی مقاله قبلی من است. لطفا Node & Rust: Friendship Forever را بخوانید. راه NAPI-rs..
وقت آن است که چند کلمه در مورد Rust به شما بگویم.
ویکی پدیا در مورد Rust موارد زیر را به ما می گوید.
Rust یک زبان برنامه نویسی چند پارادایم، سطح بالا و همه منظوره است. Rust بر عملکرد، ایمنی نوع و همزمانی تأکید دارد. Rust ایمنی حافظه را تقویت می کند – یعنی همه مراجع به حافظه معتبر اشاره می کنند – بدون نیاز به استفاده از یک جمعآوری زباله یا شمارش مرجع در سایر زبانهای ایمن برای حافظه وجود دارد. برای تقویت همزمان ایمنی حافظه و جلوگیری از مسابقه دادههای همزمان، Rust’s Borrow Checker طول عمر همه منابع یک برنامه را در طول کامپایل ردیابی میکند. Rust برای برنامهنویسی سیستمها محبوب است، اما همچنین ویژگیهای سطح بالا از جمله برخی ساختارهای برنامهنویسی کاربردی را ارائه میدهد. توسعهدهنده نرمافزار Graydon Hoare هنگام کار در تحقیقات موزیلا در سال 2006، Rust را بهعنوان یک پروژه شخصی ایجاد کرد. موزیلا رسماً از این پروژه در سال 2009 حمایت مالی کرد. از اولین نسخه پایدار در می 2015، Rust پذیرفته شده است. توسط شرکت هایی مانند آمازون، دیسکورد، دراپ باکس، فیس بوک (متا)، گوگل (الفبا) و مایکروسافت.”
در مورد Rust به عنوان یک جاوا اسکریپت مشابه فکر نکنید. این زبان کاملاً متفاوت است. لطفا به نکات زیر توجه فرمایید.
- Rust یک زبان قابل کامپایل است.
- علیرغم هدف کلی آن، بیشتر شبیه یک رقیب در C++ (حتی C) به نظر می رسد. اگر شما قوم گولنگ آشنا هستید، لطفا، زنگ را با گولنگ مقایسه نکنید! آنها نیز متفاوت هستند.
- یکی از ویژگی های اصلی Multi-threading ایمن است.
-
موضوع “مرجع و قرض گرفتن” ممکن است برای مردم جاوا اسکریپت کمی دشوار باشد. لطفا روی آن تمرکز کنید!
- لطفا این منبع را بخوانید.
من می خواهم در این مقاله بر روی Multi-Threading ایمن تمرکز کنم زیرا درک این ویژگی راه شماره یک برای درک این زبان زیبا است. همچنین میدانم که موضوع ایمن Multi-Threading یکی از پیچیدهترین موضوعات در زبانهای خارج از Rust است. جاوا، Goalng و C++ نمونه های خوبی هستند. زبان Rust جامعه بزرگی دارد و منابع زیادی (مثلاً این یکی) در مورد آن وجود دارد. اما با کمبود مثال های کاربردی و توضیحات ساده مواجه شدم. با وجود آن، من یک مثال کلاسیک پیدا کردم که به شما کمک می کند تا در سریع ترین زمان ممکن وارد این موضوع شوید.
با فیلسوفان غذاخوری آشنا شوید!
Dining Philosophers Problem
یک کار کلاسیک Multi-Threading است که در اینجا توضیح داده شده است.
“پنج فیلسوف ساکت پشت یک میز گرد با کاسه های اسپاگتی می نشینند. چنگال هایی بین هر جفت فیلسوف مجاور قرار می گیرد. هر فیلسوف باید متناوب فکر کند و بخورد. اما یک فیلسوف تنها زمانی می تواند اسپاگتی بخورد که چنگال چپ و راست داشته باشد. چنگال را فقط یک فیلسوف میتواند نگه دارد و بنابراین یک فیلسوف فقط در صورتی میتواند از چنگال استفاده کند که فیلسوف دیگری از آن استفاده نکند.بعد از اینکه یک فیلسوف از غذا خوردن به پایان رسید، باید هر دو چنگال را زمین بگذارد تا چنگالها در دسترس دیگران قرار گیرد. یک فیلسوف می تواند چنگال را در سمت راست یا چپ خود بگیرد، اما نمی تواند قبل از گرفتن هر دو چنگال شروع به غذا خوردن کند. مشکل این است که چگونه یک رشته رفتاری (الگوریتمی همزمان) طراحی کنیم که هیچ فیلسوفی گرسنگی نبیند؛ یعنی هر یک میتواند برای همیشه به تناوب خوردن و فکر کردن ادامه دهد، با این فرض که هیچ فیلسوفی نمیتواند بداند چه زمانی ممکن است دیگران بخواهند غذا بخورند. یا فکر کن.”
در ابتدا، راه حل Rust مانند تصویر زیر بود. میتوانید اینجا پیدایش کنید
use std::sync::{Arc, Mutex};
use std::{thread, time};
struct Philosopher {
name: String,
left: usize,
right: usize,
}
impl Philosopher {
fn new(name: &str, left: usize, right: usize) -> Philosopher {
Philosopher {
name: name.to_string(),
left: left,
right: right,
}
}
fn eat(&self, table: &Table) {
let _left = table.forks[self.left].lock().unwrap();
let _right = table.forks[self.right].lock().unwrap();
println!("{} is eating.", self.name);
let delay = time::Duration::from_millis(1000);
thread::sleep(delay);
println!("{} is done eating.", self.name);
}
}
struct Table {
forks: Vec<Mutex<()>>,
}
fn main() {
let table = Arc::new(Table {
forks: vec![
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
],
});
let philosophers = vec![
Philosopher::new("Donald", 0, 1),
Philosopher::new("Larry", 1, 2),
Philosopher::new("Mark", 2, 3),
Philosopher::new("John", 3, 4),
Philosopher::new("Bruce", 0, 4),
];
let handles: Vec<_> = philosophers
.into_iter()
.map(|p| {
let table = table.clone();
thread::spawn(move || {
p.eat(&table);
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
من نمیخواهم کد بالا را زیاد کنکاش کنم، و حدس میزنم از انجام آن از سمت خود لذت خواهید برد. اما باید روی چند نکته اساسی تمرکز کنم.
بر کسی پوشیده نیست که وظیفه اصلی Multi-Threading در مورد جلوگیری از برخورد داده ها است. در مثال ما، تصادم به این معناست که فیلسوفان همسایه به طور همزمان چنگال مشابهی را برداشتند، زیرا هر فیلسوفی نخ خود را دارد و همزمان با دیگران غذا میخورد و فکر میکند. Mutex با آن مقابله می کند. Mutex به معنای طرد متقابل است، “فقط یکی در یک زمان”. به همین دلیل است که ارتباط فورک ها با mutexes های مرتبط ایده خوبی است.
در این مرحله، من می خواهم داستان خود را قطع کنم و یک نکته مهم را به شما بگویم. راستش این مثال پیچیده تر از آن است که من توضیح دهم. من فقط قصد دارم افراد جدید Rust را هیجان زده کنم. با عرض پوزش از این که به شما کارشناسان عزیز گفته ام. علیرغم اینکه Mutex یک استاندارد همزمان طلایی است، این یک نوشدارویی نیست. برخی از مسائل حتی در اینجا امکان پذیر است. من اطلاعات مفید بیشتری در مورد آن در مقاله ارائه خواهم کرد نقشه راه بخش لطفاً در مورد «بن بست»، «قطع زنده» و «گرسنگی» با دقت بخوانید.
کد مربوطه به شرح زیر است.
let table = Arc::new(Table {
forks: vec![
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
],
});
و
let _left = table.forks[self.left].lock().unwrap();
let _right = table.forks[self.right].lock().unwrap();
کد بالا در لغت به معنای زیر است.
فیلسوفی چند چنگال برمی دارد و نگه می دارد.
در این لحظه چه اتفاقی برای همسایگانش می افتد؟
می خواهند چنگال بردارند. اما چنگال قبلا گرفته شده است.
در این مورد با همسایه ها چه اتفاقی می افتد؟
آنها (یعنی رشته های مرتبط) منتظرند که اولین فیلسوف چنگال ها را آزاد کند (موتکس های خود را باز کند).
چرا منتظرند؟
به دلیل mutexes!
به کد زیر نگاه کنید.
fn eat(&self, table: &Table) {
let _left = table.forks[self.left].lock().unwrap();
let _right = table.forks[self.right].lock().unwrap();
println!("{} is eating.", self.name);
let delay = time::Duration::from_millis(1000);
thread::sleep(delay);
println!("{} is done eating.", self.name);
}
وقتی فیلسوف چنگال ها را ترک می کند؟
چنگال ها را می گیرد و 1 ثانیه منتظر می ماند. mutexe ها پس از این منتشر خواهند شد
eat
عملکرد تکمیل شده است.سایر همسایه های خوش شانس (راست و چپ) چنگال های مربوطه را می گیرند. به همسایه هایی که در رشته های جداگانه اجرا می شوند (به طور همزمان) توجه کنید.
همچنین، بیایید به کد زیر که مستقیماً به چند رشته ای مربوط می شود نگاه کنیم.
let handles: Vec<_> = philosophers
.into_iter()
.map(|p| {
let table = table.clone();
thread::spawn(move || {
p.eat(&table);
})
})
.collect();
for h in handles {
h.join().unwrap();
}
زمان اجرای نمونه اصلی فرا رسیده است.
git clone git@github.com:buchslava/dining-philosophers-problem.git
cd dining-philosophers-problem
git checkout original-version
cargo build
./target/debug/dining-philosophers
از خودم پرسیدم.
آیا می توان تمام نتایج را در حین اجرای برنامه (منظورم پرینت پیام هاست) جمع آوری کرد و بلافاصله پس از اتمام کل منطق ارائه کرد؟
من این کار را چالش برانگیز دیدم زیرا از فناوری های دیگر می دانم که تعامل بین رشته ای همیشه دردناک است. به عنوان یک قوم جاوا اسکریپت، برای اولین بار به چیزی شبیه به این فکر کردم Promise.all
تکنیک.
با کمال تعجب، تکنیک مشابهی را در وبلاگ YOSHUA WUYTS پیدا کردم. خواندن این منبع را به شدت توصیه می کنم.
لطفا به مقاله زیر نگاه کنید.
علاوه بر این، من جدول زیر را برای مردم جاوا اسکریپت دلپذیر یافتم.
جاوا اسکریپت | زنگ | شرح |
---|---|---|
وعده.همه حل شد | آینده::پیوستن | اتصال کوتاه نمی کند |
قول.همه | future::try_join | هنگامی که یک مقدار ورودی رد می شود، اتصال کوتاه می کند |
وعده.نژاد | آینده::انتخاب کنید | هنگامی که یک مقدار ورودی تسویه می شود، اتصال کوتاه می کند |
قول.هر | future::try_select | هنگامی که یک مقدار ورودی برآورده می شود، اتصال کوتاه می کند |
با توجه به اطلاعات بالا، راه حل جدید باید شبیه راه حل زیر باشد.
use async_std::future;
let a = future::ready(Ok(1));
let b = future::ready(Ok(2));
let c = future::try_join(a, b);
assert_eq!(c.await?, (1, 2));
به عنوان یک یادگیرنده بی حوصله، همزمان به رویکرد آینده و نمونه مبدا نگاه کردم. من کمی ناامید شدم زیرا نتوانستم نسخه اصلی را به روشی مبتنی بر آینده دوباره پیادهسازی کنم. راستش من وقت کافی برای این کار ندارم. علاوه بر این، با توجه به نتایج حفاری، مفهوم آینده به شدت با نخ ها متفاوت است. به همین دلیل است که مطمئن نیستم این ایده خوب باشد. این فعالیت را به تعویق انداختم.
تصمیم گرفتم راه دیگری را انتخاب کنم. من بیشتر تحقیق کردم و استفاده از ارسال پیام برای انتقال داده ها بین رشته ها را پیدا کردم.
فکر میکنم شما موافق باشید که روش زیر برای اجرای اصلی Dinning Philosophers بسیار دوستانه است.
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hi");
tx.send(val).unwrap();
});
let received = rx.recv().unwrap();
println!("Got: {}", received);
}
وقت آن است که به جلو برویم و راه حل خود را اصلاح کنیم. من می خواهم سورس کد کامل را ارائه کنم و مرحله به مرحله توضیح دهم.
use std::sync::{Arc, Mutex};
use std::{thread, time};
use std::sync::mpsc::{Sender};
use std::sync::mpsc;
struct Philosopher {
name: String,
left: usize,
right: usize,
}
impl Philosopher {
fn new(name: &str, left: usize, right: usize) -> Philosopher {
Philosopher {
name: name.to_string(),
left: left,
right: right,
}
}
fn eat(&self, table: &Table, sender: &Sender<String>) {
let _left = table.forks[self.left].lock().unwrap();
let _right = table.forks[self.right].lock().unwrap();
// println!("{} is eating.", self.name);
sender.send(format!("{} is eating.", self.name).to_string()).unwrap();
let delay = time::Duration::from_millis(1000);
thread::sleep(delay);
// println!("{} is done eating.", self.name);
sender.send(format!("{} is done eating.", self.name).to_string()).unwrap();
}
}
struct Table {
forks: Vec<Mutex<()>>,
}
fn main() {
let (tx, rx) = mpsc::channel();
let table = Arc::new(Table {
forks: vec![
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
],
});
let philosophers = vec![
Philosopher::new("Donald", 0, 1),
Philosopher::new("Larry", 1, 2),
Philosopher::new("Mark", 2, 3),
Philosopher::new("John", 3, 4),
Philosopher::new("Bruce", 0, 4),
];
let handles: Vec<_> = philosophers
.into_iter()
.map(|p| {
let table = table.clone();
let sender = tx.clone();
thread::spawn(move || {
p.eat(&table, &sender);
})
})
.collect();
for h in handles {
h.join().unwrap();
}
tx.send("Done".to_string()).unwrap();
let mut result: String = String::from("");
for received in rx {
if received == "Done" {
break;
}
result.push_str(&received);
result.push_str("\n");
}
println!("{}", result);
}
بسته های مرتبط را اضافه کنید
use std::sync::mpsc::{Sender};
use std::sync::mpsc;
کانال را راه اندازی کنید
fn main() {
let (tx, rx) = mpsc::channel();
// ...
}
ارسال فرستنده به eat
تابع
thread::spawn(move || {
p.eat(&table, &sender);
})
به جای چاپ فوری، اطلاعات مورد انتظار را ارسال کنید
fn eat(&self, table: &Table, sender: &Sender<String>) {
let _left = table.forks[self.left].lock().unwrap();
let _right = table.forks[self.right].lock().unwrap();
// println!("{} is eating.", self.name);
sender.send(format!("{} is eating.", self.name).to_string()).unwrap();
let delay = time::Duration::from_millis(1000);
thread::sleep(delay);
// println!("{} is done eating.", self.name);
sender.send(format!("{} is done eating.", self.name).to_string()).unwrap();
}
نتیجه نهایی را جمع آوری کنید
for h in handles {
h.join().unwrap();
}
tx.send("Done".to_string()).unwrap();
let mut result: String = String::from("");
for received in rx {
if received == "Done" {
break;
}
result.push_str(&received);
result.push_str("\n");
}
println!("{}", result);
به پیام “انجام شد” توجه کنید. این یک معیار پایان فرآیند است.
زمان اجرای راه حل نهایی فرا رسیده است.
git checkout main
cargo build
./target/debug/dining-philosophers
به نظر خوب میاد!
نقشه راه.
همانطور که قول داده بودم، برخی از موارد ضروری را به این کار اضافه خواهم کرد.
محدودیت های وظیفه
وقتی تمام فیلسوفان دقیقاً یک چنگال را در دست دارند، میتواند در این کار بن بست رخ دهد. این در حال حاضر در مقاله با نادیده گرفتن آن کار می شود the philosophers sit at a round table
: از آنجایی که دونالد و بروس از یک چنگال چپ (0) استفاده می کنند، یکی از آنها هرگز نمی تواند یک چنگال را نگه دارد، اگر دیگری تعداد چنگال ها را نگه دارد (به دلیل تلاش مداوم در ابتدا به سمت چپ).
اگر قسمت میز گرد را در نظر بگیریم، چنگال چپ بروس باید 4 و چنگال سمت راست او 0 باشد:
Philosopher::new("Bruce", 4, 0),
اکنون، این یک بن بست احتمالی را معرفی می کند. تکرار کردن آن سخت است، اما زمانی که بخش «عرضه بینهایت و تقاضای نامحدود فرض میشود» را اجرا کنیم، تکرار آن آسان است. همانطور که در زمان بی نهایت، ما در نهایت به بن بست خواهیم خورد که در آن همه فیلسوفان برای همیشه در فکر کردن گیر خواهند کرد.
یک راه آسان برای بازتولید آن، اضافه کردن 5 میلیثانیه انتظار بر روی چنگال چپ هر فیلسوف است. به بن بست خواهد رسید
use std::sync::{Arc, Mutex};
use std::{thread, time};
struct Philosopher {
name: String,
left: usize,
right: usize,
}
impl Philosopher {
fn new(name: &str, left: usize, right: usize) -> Philosopher {
Philosopher {
name: name.to_string(),
left: left,
right: right,
}
}
fn eat(&self, table: &Table) {
println!("{} is picking up the left fork.", self.name);
let _left = table.forks[self.left].lock().unwrap();
// added 5ms duration
thread::sleep(time::Duration::from_millis(5));
println!("{} is picking up the right fork.", self.name);
let _right = table.forks[self.right].lock().unwrap();
println!("{} is eating.", self.name);
let delay = time::Duration::from_millis(1000);
thread::sleep(delay);
println!("{} is done eating.", self.name);
}
}
struct Table {
forks: Vec<Mutex<()>>,
}
fn main() {
let table = Arc::new(Table {
forks: vec![
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
Mutex::new(()),
],
});
let philosophers = vec![
Philosopher::new("Donald", 0, 1),
Philosopher::new("Larry", 1, 2),
Philosopher::new("Mark", 2, 3),
Philosopher::new("John", 3, 4),
// changed from Philosopher::new("Bruce", 0, 4),
Philosopher::new("Bruce", 4, 0),
];
let handles: Vec<_> = philosophers
.into_iter()
.map(|p| {
let table = table.clone();
thread::spawn(move || {
p.eat(&table);
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
شما می توانید یک مثال کارآمد (اما واقعاً کار نمی کند …) را در اینجا پیدا کنید.
اطلاعات مفید و مراجع
به عنوان یک مردم همزمان، شما باید شروع به فکر کردن کنید Deadlock,
Livelock,
و Starvation.
لطفا در مورد آن در اینجا بخوانید
تکل زدن Deadlock,
Livelock,
و Starvation
آسان نیست، و هیچ گلوله نقره ای در اینجا وجود ندارد. با وجود اینکه میتوانید راهحلهای مختلف موجود در مورد موضوع را جستجو کنید. این یکی مثلا
همچنین، اگر محاسبات اتمی را یاد بگیرید، بهتر است. برای تبدیل شدن به نینجای همزمانی، در عمل از Rust Atomics and Locks Concurrency سطح پایین شروع کنید.
NodeJS
در نهایت، کنجکاوی شما را برآورده می کنم و نسخه NodeJS راه حل را به شما ارائه می کنم. از اینجا برداشته شد و کمی اصلاح شد…
شما می توانید آن را بخوانید و اجرا کنید.
cd nodejs
node index
امیدوارم افق NodeJS شما را نیز گسترش دهد.
هک مبارک!
PS: با تشکر از Eduardo Speroni برای نظرات بسیار مفید و برای کمک در طول کار من بر روی مقاله.