برنامه نویسی

راهنمای گمشده اتمی 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بشر
  • فرصتی برای خواندن داده های قدیمی نیست!

یک نمودار کوچک که نشان می دهد چگونه 3 موضوع به داده ها تغییر می کنند

مثال 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 Relaxed فروشگاه انجام می شود ، پس از آن ممکن است/ممکن است بر اساس جایی که CPU مجدداً دستورالعمل را سفارش داده است ، ندید

ذخیره (نوشتن) Ordering::Release تضمین کردن همه قبلی Acquire می نویسد قبل از باز کردن قفل قابل مشاهده هستند.

تضمین نمی کند Relaxed به همان دلیلی که ممکن است قبل از نوشتن دوباره مرتب شود.


منابع:

std :: Memory_order
انتشار زنگ زدگی و به دست آوردن حافظه
پوسته زنگ زدگی: اتمی و سفارش حافظه

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا