راهنمای گمشده اتمی Rust :: سفارش

هنگام کار با برنامه نویسی همزمان در زنگ زدگی ، عملیات اتمی روشی قدرتمند برای مدیریت ایمن حالت مشترک ارائه می دهد. با این حال ، یکی از جنبه هایی که اغلب توسعه دهندگان را گیج می کند-به ویژه مواردی که همزمانی جدید و سطح پایین دارند- سفارش حافظه اتمیبشر در Ordering
انواع در Rust's std::sync::atomic
کنترل ماژول چگونه عملیات روی اتمی در میان موضوعات درک می شود ، و در ضمن عملکرد تعادل ، از صحت اطمینان می یابد.
در این مقاله ، ما تجزیه خواهیم کرد چه Atomic::Ordering
واقعاً به معنای این است که چرا اهمیت دارد ، و چگونه می توانید سفارش مناسب را برای مورد استفاده خود انتخاب کنیدبشر ما یک mutex را از ابتدا اجرا خواهیم کرد و تا متفاوت (Relaxed
با Acquire
با Release
با AcqRel
وت SeqCst
) سفارشات ، تجارت آنها را بررسی کنید و از نمونه های عملی با قیاس های دنیای واقعی برای درک مفهوم استفاده کنید
توضیح اتمیک
کلمه اتمی از کلمه یونانی گرفته شده است ἄτομος
، به معنای غیرقابل تفکیک ، چیزی که نمی تواند به قطعات کوچکتر برسد. در علوم کامپیوتر از آن برای توصیف عملیاتی که غیرقابل تفکیک است استفاده می شود: یا کاملاً تکمیل شده است ، یا هنوز اتفاق نیفتاده است.
Atomics
در زنگ زدگی برای انجام عملیات کوچک (اضافه کردن ، زیر مجموعه ، مقایسه و غیره) بر روی یک حافظه مشترک استفاده می شود. برخلاف یک عادی x=x+1
بیانیه ای که باید از طریق واکشی ، رمزگشایی ، اجرای ، نوشتن چرخه ، اتمیک از طرف دیگر در یک واحد اجرا می شود (یا حتی کمتر از آن) چرخه CPU. از این رو جلوگیری از دات داده شرط در بین موضوعات. این باعث می شود آن را برای اجرای عالی Mutexes
const LOCKED:bool = true;
const UNLOCKED:bool = false;
// Intentional incorrect implementation of a Mutex
pub struct Mutex <T> {
locked:AtomicBool,
v:UnsafeCell<T>,
}
impl<T> Mutex<T>{
pub fn new(t: T) -> Self {
Self {
locked: AtomicBool::new(UNLOCKED),
v: UnsafeCell::new(t),
}
}
// Takes in a closure/function (what to do after getting exclusive access of )
pub fn with_lock<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
// For the sake of understanding we'll use a SpinLock
// One should never utilise a SpinLock in production
while self.locked.load(Ordering::Relaxed) != UNLOCKED {}
self.locked.store(LOCKED, Ordering::Relaxed);
// Since Unsafe Cell returns a "unsafe" raw pointer, we need to typecast it to a "memory safe" mutable reference before passing it to our closure 'f'
let ret = f(unsafe { &mut *self.v.get() });
self.locked.store(UNLOCKED, Ordering::Relaxed);
ret
}
}
به خاطر سادگی ، فقط نادیده بگیرید سفارش و بیایید استفاده کنیم Ordering::Relaxed
چون همه ما هستیم افراد آرام به طور کلی چاشنی
اما چرا اتمیک؟
CPU و کامپایلرهای مدرن اغلب دستورالعمل ها را برای بهبود عملکرد و استفاده از CPU مجدداً سفارش می دهند ، با این حال ، این امر در صورت وجود چندین موجود مستقل بسیار مفید نیست (موضوعات IE) . این می تواند باعث شود مسابقه داده ها وت قفل های خواندن، متوقف کردن موضوعات برای مدت طولانی.
اکنون Mutex که ضعیف اجرا شده است ، طعمه این امر می شود ، دستورالعمل ها می توانند از بین بروند و همه ما خوب فکر کردن اجرای به هدر می رود. از آنجا که می تواند به چیزی مانند:
pub fn with_lock<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
self.locked.store(UNLOCKED, Ordering::Relaxed);
while self.locked.load(Ordering::Relaxed) != UNLOCKED {}
self.locked.store(LOCKED, Ordering::Relaxed);
let ret = f(unsafe { &mut *self.v.get() });
ret
}
که با یک موضوع دیگر که در حال حاضر قفل دارد ، می باشد. یا چیزی مانند
pub fn with_lock<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
self.locked.store(LOCKED, Ordering::Relaxed);
while self.locked.load(Ordering::Relaxed) != UNLOCKED {}
let ret = f(unsafe { &mut *self.v.get() });
self.locked.store(UNLOCKED, Ordering::Relaxed);
ret
}
این می تواند منجر به قفل شدن دسترسی خود به داده ها و حضور در حالت بن بست برای همیشه شود و منجر به انجماد این برنامه شود.
چگونه Atomic::Ordering
ما را از این نجات دهید؟
این دستورالعمل های ویژه ای را به کامپایلر می دهد ، یعنی چه موقع باید دوباره تنظیم شود و چه موقع لازم نیست.
Ordering::Acquire/Release
(با هم) – دیدن نوشتن های قبلی و خواندن های آینده را تضمین می کند
مفهوم
- تضمین می کند که تمام نوشتن های انجام شده قبل از انتشار موضوع دیگر داده ها قابل مشاهده استبشر
Release
- مانع شدن خواندن/نوشتن قبلی از حرکت پس از عملیات کسببشر
Acquire
مثال 1: تصور کنید T1
در حال آماده سازی یک است سفارش پیتزاوت T2
شخص تحویل است
fn thread1() {
DATA = 42;
FLAG.store(UNLOCKED, Ordering::Release); // Guarantees DATA is written before FLAG!
}
fn thread2() {
while FLAG.load(Ordering::Acquire) != UNLOCKED {} // Guarantees we see DATA update!
println!("{}", DATA); // Always prints 42.
}
اکنون به عنوان کامپایلر دستورالعمل ها را دریافت می کند:-
- مجموعه های T1 (آشپز)
DATA = 42
و سپس تلنگرFLAG
به درست - T2 (تحویل) منتظر است تا
FLAG == true
و فقط پس از آن جمع می شودDATA
بشر - فرصتی برای خواندن داده های قدیمی نیست!
مثال 2: فکر کنید T1
به عنوان یک انبار در حال تهیه یک بسته ، و T2
به عنوان یک کارگر تحویل آن را انتخاب می کند.
fn thread1() {
DATA = 42;
FLAG.store(UNLOCKED, Ordering::Release); // "Releases" the data 1st and then unlocks
}
fn thread2() {
while FLAG.load(Ordering::Acquire) != UNLOCKED {} // Guarantees we see previous writes
println!("{}", DATA); // Always prints 42.
}
به همین ترتیب:-
- T1 (انبار) سفارش را بسته بندی می کند (
DATA = 42
) ، سپس تنظیم می کندFLAG
به درست - T2 (تحویل) انتخاب نمی شود
FLAG == true
تا اینکهDATA = 42
کاملاً نوشته شده است - T2 همیشه مقدار صحیح را بدست می آورد.
نکته کلیدی: Release
تضمین می کند که همه نوشته های قبلی (مانند DATA = 42
) قبل از تنظیم قابل مشاهده هستند FLAG = true
بشر
Ordering::Acquire
– تضمین می کند که نوشته های قبلی دیده می شوند
مثال: تصور کنید T1
آشپز آشپز است و T2
پیشخدمت است
سناریو:
بعد از آشپز آشپز ..
- پیشخدمت (
T2
) بررسی می کند که آیا ظرف است آماده (Acquire
). - هنگامی که بلیط (پرچم) “آماده” مشخص شد ، پیشخدمت می داند ظرف کامل است.
- اما قبل از بررسیآنها ممکن است به هر ترتیب کارهای نامربوط انجام دهند.
while !FLAG.load(Ordering::Acquire) {} // Wait for dish to be marked as "Ready" and then load the data
println!("{}", DATA); // Guaranteed to be correct after acquire!
توضیح:
-
Acquire
تضمین می کند که وقتی پیشخدمت می بیندFLAG == true
، آنها همچنین ظرف کامل را مشاهده می کنند (DATA = 42
) و آن را انتخاب کنید. - با این حال ، پیش وظایف (مانند تنظیم صفحات) یعنی دستورالعمل های قبلی ممکن است قبل از بررسی اتفاق افتاده باشد.
با استفاده از Release
سفارش برای یک فروشگاه تضمین می کند که تمام تغییرات قبلی پس از فروشگاه قابل مشاهده است. بوها بار متصل مقدار ذخیره شده و دستور اجرای عملیات بعدی را مشاهده می کنید. با این حال ، در عملیات فروشگاه بار ، قسمت فروشگاه “آرام” می شود و ضمانت های سفارش قوی را از دست می دهد.
تضمین می کند که ما همه تغییرات حافظه را که توسط صاحبان قفل قبلی ایجاد شده است می بینیم
Ordering::Release
– دسترسی به آینده را تضمین می کند که تغییر ایجاد شده را مشاهده کنید
مثال: تصور کنید T1
آشپز آشپز است و T2
پیشخدمت است
سناریو:
قبل از اینکه پیشخدمت ظرف را انتخاب کند …
- سرآشپز (
T1
) باید تهیه ظرف را تمام کنید پیش از علامت گذاری سفارش به عنوان آماده (Release
). - یا در غیر این صورت ، مشتریان می توانند یک وعده غذایی نیمه پخته دریافت کنند.
قیاس کد:
DATA = 42;
FLAG.store(true, Ordering::Release); // Only set flag after food is ready(data is set to 42)
توضیح:
-
Release
مطمئن است همه چیز قبل از اینکه ابتدا اتفاق بیفتد (ظرف قبل از پرش پرچم آماده است).
یک بار اکتسابی قبل از مشاهده یک فروشگاه قبلی ، تمام عملیات را تضمین می کند و از داده های منسوخ یا متناقض جلوگیری می کند. این به عنوان یک مانع عمل می کند و قوام حافظه را در میان موضوعات اجرا می کند.
تضمین می کند که خواندن آینده این مقدار به روز شده را مشاهده می کند
Ordering::SeqCst
– نظم جهانی عملیات را تضمین می کند
مثال: تصور کنید T1
مشتری A و T2
مشتری ب است
سناریو:
قبل از انتقال پول …
- مشتری A (
T1
) پول را پس بگیرید از حساب آنها پیش از واریز آن به حساب B مشتری. - مشتری B (
T2
) اراده فقط سپرده را ببینید پس از اتمام خروج مشتری A. - هر دو عمل باید به ترتیب جهانی اتفاق بیفتد ، اطمینان حاصل شود که هیچ موضوعی (به عنوان مثال ، هیچ مشتری) عملیات خارج از نظم را مشاهده نمی کند ، حتی اگر هر دو موضوع در پردازنده های مختلف اجرا شوند.
قیاس کد:
ACCOUNT_A.withdraw(50);
ACCOUNT_B.deposit(50);
FLAG.store(true, Ordering::SeqCst); // The deposit operation will not be seen until the withdrawal is fully completed`
توضیح:
-
SeqCst
تضمین می کند که همه موضوعات عملیات را به ترتیب در سطح جهانی مشاهده می کنند. این بدان معنی است که خروج مشتری A دیده می شود پیش از سپرده مشتری Bبشر هیچ موضوع دیگری عملیات را به ترتیب دیگری مشاهده نمی کند ، بنابراین از شرایط مسابقه جلوگیری می کند و اطمینان از صحت حسابداری بانک می شود.
Ordering::Relaxed
– پیشخوان های اتمی (بدون هماهنگی تضمین شده)
سناریو
- بدون خواندن آن ، به متغیر مشترک تغییر دهید.
- برای پیشخوان استفاده می شود
fn thread_1() {
VISITOR_COUNT.fetch_add(1, Ordering::Relaxed);
}
fn thread_2(){
VISITOR_COUNT.fetch_add(1, Ordering::Relaxed);
}
خوب کار می کند زیرا:
- ما نه وقتی یک موضوع تعداد به روز شده را مشاهده می کند ، مراقبت کنید.
- تا زمانی که تعداد نهایی درست است، ما خوب هستیم
اگر ما استفاده می کردیم SeqCst
در عوض:
- هر به روزرسانی اجرا می شود همگام سازی جهانی، کاهش سرعت عملکرد. ## با استفاده از همه چیزهایی که اکنون می دانیم برای رفع اجرای MUTEX ما
هنگام باز کردن قفل و قفل کردن یک mutex: هنگامی که یک mutex قفل شد ، یک رابطه اتفاق قبل از وقوع بین عملکرد باز کردن قفل و عملکرد قفل بعدی در همان mutex ایجاد می شود. این تضمین می کند که:
نخ بعدی که Mutex را قفل می کند ، تمام تغییراتی را که توسط نخ ایجاد شده است ، مشاهده می کند.
const LOCKED:bool = true;
const UNLOCKED:bool = false;
// Correct implementation of a Mutex
pub struct Mutex <T> {
locked:AtomicBool,
v:UnsafeCell<T>,
}
impl<T> Mutex<T>{
pub fn new(t: T) -> Self {
Self {
locked: AtomicBool::new(UNLOCKED),
v: UnsafeCell::new(t),
}
}
// Takes in a closure/function (what to do after getting exclusive access of )
pub fn with_lock<R>(&self, f: impl FnOnce(&mut T) -> R) -> R {
// For the sake of understanding we'll use a SpinLock
// One should never utilise a SpinLock in production
// Acquires ensures visibility of all previous writes before the unlock happens.
while self.locked.load(Ordering::Acquire) != UNLOCKED {}
// Release ensures that all previous memory writes in this thread become visible to threads that later perform an Acquire load.
self.locked.store(LOCKED, Ordering::Release);
let ret = f(unsafe { &mut *self.v.get() });
// Proper Release ordering to ensure writes are visible before unlocking
self.locked.store(UNLOCKED, Ordering::Release);
ret
}
}
قانون ساده شست
عمل | سفارش استفاده شده | هدف |
---|---|---|
بارگیری (خواندن) | Ordering::Acquire |
این خواندن را تضمین می کند همه قبلی می نویسد قبل از Release ذخیره
اگر a |
ذخیره (نوشتن) | Ordering::Release |
تضمین کردن همه قبلی Acquire می نویسد قبل از باز کردن قفل قابل مشاهده هستند.
تضمین نمی کند |
منابع:
std :: Memory_order
انتشار زنگ زدگی و به دست آوردن حافظه
پوسته زنگ زدگی: اتمی و سفارش حافظه