اندروید 3.0 و نسخههای پلتفرم بعدی برای پشتیبانی از معماریهای چند پردازنده بهینه شدهاند. این سند مسائلی را معرفی میکند که هنگام نوشتن کدهای چند رشتهای برای سیستمهای متقارن چند پردازندهای در C، C++ و زبان برنامهنویسی جاوا (که از این به بعد بهخاطر اختصار به سادگی «جاوا» نامیده میشود) ایجاد میشود. این به عنوان یک آغازگر برای توسعه دهندگان برنامه اندروید در نظر گرفته شده است، نه به عنوان یک بحث کامل در مورد این موضوع.
مقدمه
SMP مخفف "Symmetric Multi-Processor" است. این طرحی را توصیف می کند که در آن دو یا چند هسته CPU یکسان به حافظه اصلی دسترسی دارند. تا چند سال پیش، تمام دستگاه های اندرویدی UP (یونی-پردازنده) بودند.
اکثر -اگر نه همه- دستگاههای اندرویدی همیشه چندین CPU داشتند، اما در گذشته تنها یکی از آنها برای اجرای برنامهها استفاده میشد، در حالی که دیگران بخشهای مختلف سختافزار دستگاه (مثلا رادیو) را مدیریت میکردند. پردازندهها ممکن است معماری متفاوتی داشته باشند و برنامههایی که روی آنها اجرا میشوند نمیتوانند از حافظه اصلی برای برقراری ارتباط با یکدیگر استفاده کنند.
اکثر دستگاههای اندرویدی که امروزه فروخته میشوند بر اساس طرحهای SMP ساخته شدهاند، که کار را برای توسعهدهندگان نرمافزار کمی پیچیدهتر میکند. شرایط مسابقه در یک برنامه چند رشته ای ممکن است باعث ایجاد مشکلات قابل مشاهده در یک تک پردازنده نشوند، اما ممکن است زمانی که دو یا چند رشته شما به طور همزمان روی هسته های مختلف اجرا می شوند، به طور منظم با شکست مواجه شوند. علاوه بر این، کد ممکن است زمانی که روی معماریهای پردازندههای مختلف یا حتی در پیادهسازیهای متفاوت یک معماری اجرا میشود، کم و بیش مستعد شکست باشد. کدی که به طور کامل روی x86 آزمایش شده است ممکن است در ARM به شدت خراب شود. زمانی که کد با یک کامپایلر مدرن تر کامپایل می شود ممکن است شروع به شکست کند.
بقیه این سند دلیل آن را توضیح می دهد و به شما می گوید که برای اطمینان از اینکه کد شما به درستی عمل می کند، چه کاری باید انجام دهید.
مدل های سازگاری حافظه: چرا SMP ها کمی متفاوت هستند
این یک نمای کلی با سرعت بالا و براق از یک موضوع پیچیده است. برخی از زمینه ها ناقص خواهند بود، اما هیچ یک از آنها نباید گمراه کننده یا اشتباه باشد. همانطور که در بخش بعدی خواهید دید، جزئیات در اینجا معمولاً مهم نیستند.
برای اشاره به درمان های کامل تر این موضوع، به ادامه مطلب در انتهای سند مراجعه کنید.
مدلهای سازگاری حافظه، یا اغلب فقط «مدلهای حافظه»، تضمینهایی را که زبان برنامهنویسی یا معماری سختافزار در مورد دسترسی به حافظه ایجاد میکند، توصیف میکند. به عنوان مثال، اگر مقداری را برای آدرس A بنویسید، و سپس یک مقدار را برای آدرس B بنویسید، مدل ممکن است تضمین کند که هر هسته CPU می بیند که این نوشته ها به ترتیب انجام می شود.
مدلی که اکثر برنامه نویسان به آن عادت دارند سازگاری متوالی است که به شرح زیر است ( Adve & Gharachorloo ) :
- به نظر می رسد که تمام عملیات حافظه در یک زمان اجرا می شوند
- به نظر می رسد که تمام عملیات در یک رشته واحد به ترتیبی که توسط برنامه آن پردازنده توضیح داده شده است اجرا می شوند.
اجازه دهید به طور موقت فرض کنیم که ما یک کامپایلر یا مفسر بسیار ساده داریم که هیچ شگفتی ایجاد نمی کند: این تخصیص ها را در کد منبع ترجمه می کند تا دستورالعمل ها را دقیقاً به ترتیب مربوطه بارگذاری و ذخیره کند، یک دستورالعمل برای هر دسترسی. همچنین برای سادگی فرض می کنیم که هر رشته بر روی پردازنده خود اجرا می شود.
اگر کمی به کد نگاه کنید و ببینید که مقداری خواندن و نوشتن از حافظه انجام میدهد، در معماری CPU که به طور متوالی سازگار است، میدانید که کد خواندن و نوشتن را به ترتیب مورد انتظار انجام میدهد. این امکان وجود دارد که CPU در واقع دستورات را دوباره ترتیب دهد و خواندن و نوشتن را به تأخیر بیندازد، اما هیچ راهی برای کدهای در حال اجرا روی دستگاه وجود ندارد که بگوید CPU کار دیگری به جز اجرای دستورالعملها به روشی ساده انجام میدهد. (ما ورودی/خروجی درایور دستگاه دارای نقشه حافظه را نادیده می گیریم.)
برای نشان دادن این نکات، در نظر گرفتن تکههای کوچک کد، که معمولاً به آن آزمونهای تورنسل میگویند، مفید است.
در اینجا یک مثال ساده با کد اجرا شده روی دو رشته آورده شده است:
موضوع 1 | موضوع 2 |
---|---|
A = 3 | reg0 = B |
در این و تمام مثالهای تورنسل آینده، مکانهای حافظه با حروف بزرگ (A، B، C) نشان داده میشوند و رجیسترهای CPU با "reg" شروع میشوند. تمام حافظه در ابتدا صفر است. دستورالعمل ها از بالا به پایین اجرا می شوند. در اینجا، رشته 1 مقدار 3 را در مکان A و سپس مقدار 5 را در مکان B ذخیره می کند. موضوع 2 مقدار را از مکان B در reg0 بارگیری می کند و سپس مقدار را از مکان A در reg1 بارگذاری می کند. (توجه داشته باشید که ما به ترتیبی می نویسیم و به ترتیب دیگر می خوانیم.)
Thread 1 و Thread 2 بر روی هسته های مختلف CPU اجرا می شوند. وقتی به کدهای چند رشته ای فکر می کنید، همیشه باید این فرض را داشته باشید.
سازگاری متوالی تضمین می کند که پس از اتمام اجرای هر دو رشته، ثبات ها در یکی از حالت های زیر باشند:
ثبت می کند | ایالات |
---|---|
reg0=5، reg1=3 | ممکن است (رشته 1 ابتدا اجرا شد) |
reg0=0، reg1=0 | ممکن است (رشته 2 ابتدا اجرا شد) |
reg0=0، reg1=3 | امکان پذیر (اجرای همزمان) |
reg0=5، reg1=0 | هرگز |
برای وارد شدن به موقعیتی که قبل از دیدن فروشگاه به A، B=5 را می بینیم، خواندن یا نوشتن باید خارج از نظم باشد. در یک ماشین متوالی سازگار، این نمی تواند اتفاق بیفتد.
پردازنده های یونی، از جمله x86 و ARM، معمولاً به صورت متوالی سازگار هستند. به نظر میرسد که نخها به شکلی درهم اجرا میشوند، زیرا هسته سیستم عامل بین آنها سوئیچ میکند. اکثر سیستم های SMP، از جمله x86 و ARM، به طور متوالی سازگار نیستند. به عنوان مثال، معمولاً سختافزارها در مسیر رسیدن به حافظه، ذخیرههایی را بافر میکنند تا فوراً به حافظه نرسند و برای هستههای دیگر قابل مشاهده نباشند.
جزئیات به طور قابل توجهی متفاوت است. به عنوان مثال، x86، اگرچه به طور متوالی سازگار نیست، اما همچنان تضمین می کند که reg0 = 5 و reg1 = 0 غیرممکن باقی می مانند. فروشگاه ها بافر هستند، اما نظم آنها حفظ می شود. از سوی دیگر، ARM این کار را نمی کند. ترتیب فروشگاههای بافر حفظ نمیشود و ممکن است فروشگاهها همزمان به تمام هستههای دیگر دسترسی نداشته باشند. این تفاوت ها برای برنامه نویسان اسمبلی مهم هستند. با این حال، همانطور که در زیر خواهیم دید، برنامه نویسان C، C++ یا جاوا می توانند و باید به گونه ای برنامه ریزی کنند که چنین تفاوت های معماری را پنهان کند.
تا کنون، ما به طور غیرواقعی فرض کردهایم که فقط سختافزار است که دستورات را دوباره سفارش میدهد. در واقعیت، کامپایلر دستورالعملها را برای بهبود عملکرد مجدداً مرتب میکند. در مثال ما، کامپایلر ممکن است تصمیم بگیرد که برخی از کدهای بعدی در Thread 2 قبل از اینکه به reg0 نیاز داشته باشد، به مقدار reg1 نیاز دارند و بنابراین ابتدا reg1 را بارگیری می کند. یا ممکن است برخی از کدهای قبلی قبلاً A را بارگذاری کرده باشند، و کامپایلر ممکن است تصمیم بگیرد به جای بارگذاری مجدد A، از آن مقدار دوباره استفاده کند. در هر صورت، بارهای reg0 و reg1 ممکن است دوباره مرتب شوند.
مرتب سازی مجدد دسترسی ها به مکان های مختلف حافظه، چه در سخت افزار یا در کامپایلر، مجاز است، زیرا بر اجرای یک رشته تأثیر نمی گذارد و می تواند عملکرد را به طور قابل توجهی بهبود بخشد. همانطور که خواهیم دید، با کمی دقت می توانیم از تأثیرگذاری آن بر نتایج برنامه های چند رشته ای نیز جلوگیری کنیم.
از آنجایی که کامپایلرها می توانند دسترسی های حافظه را مجددا ترتیب دهند، این مشکل در واقع برای SMP ها جدید نیست. حتی در یک تک پردازنده، یک کامپایلر میتواند بارها را به reg0 و reg1 در مثال ما تغییر دهد، و Thread 1 میتواند بین دستورالعملهای مرتب شده مجدد برنامهریزی شود. اما اگر کامپایلر ما دوباره ترتیب ندهد، ممکن است هرگز این مشکل را مشاهده نکنیم. در اکثر SMP های ARM، حتی بدون مرتب سازی مجدد کامپایلر، احتمالاً پس از تعداد بسیار زیادی از اجرای موفقیت آمیز، ترتیب مجدد مشاهده خواهد شد. مگر اینکه به زبان اسمبلی برنامه نویسی می کنید، SMP ها معمولاً این احتمال را افزایش می دهند که مشکلاتی را که همیشه وجود داشت ببینید.
برنامه نویسی بدون مسابقه داده
خوشبختانه، معمولاً یک راه آسان برای اجتناب از فکر کردن به هر یک از این جزئیات وجود دارد. اگر از برخی قوانین سرراست پیروی کنید، معمولاً میتوانید همه بخشهای قبلی را به جز بخش «ثبات متوالی» فراموش کنید. متأسفانه، در صورت نقض تصادفی این قوانین، سایر عوارض ممکن است قابل مشاهده باشند.
زبان های برنامه نویسی مدرن چیزی را تشویق می کنند که به عنوان سبک برنامه نویسی "بدون مسابقه داده" شناخته می شود. تا زمانی که شما قول می دهید که «مسابقه داده» را معرفی نکنید، و از تعداد معدودی ساختارهایی که خلاف آن را به کامپایلر می گویند اجتناب کنید، کامپایلر و سخت افزار قول می دهند که نتایج متوالی یکسانی را ارائه دهند. این واقعاً به این معنی نیست که آنها از مرتب سازی مجدد دسترسی به حافظه اجتناب می کنند. به این معنی است که اگر از قوانین پیروی کنید، نمیتوانید بگویید که دسترسیهای حافظه دوباره مرتب شدهاند. خیلی شبیه این است که به شما بگوییم سوسیس یک غذای خوشمزه و اشتها آور است، به شرطی که قول بدهید به کارخانه سوسیس و کالباس سر نزنید. مسابقه داده ها همان چیزی است که حقیقت زشت در مورد مرتب سازی مجدد حافظه را آشکار می کند.
"مسابقه داده" چیست؟
مسابقه داده زمانی رخ می دهد که حداقل دو رشته به طور همزمان به داده های معمولی مشابه دسترسی داشته باشند و حداقل یکی از آنها آن را تغییر دهد. منظور ما از "داده های معمولی" چیزی است که به طور خاص یک شی همگام سازی در نظر گرفته شده برای ارتباط رشته ای نیست. Mutexeها، متغیرهای شرط، فرارهای جاوا یا اشیاء اتمی C++ دادههای معمولی نیستند و دسترسیهای آنها مجاز است. در واقع از آنها برای جلوگیری از مسابقه داده در اشیاء دیگر استفاده می شود.
برای تعیین اینکه آیا دو رشته به طور همزمان به یک مکان حافظه دسترسی دارند، میتوانیم بحث ترتیب مجدد حافظه را از بالا نادیده بگیریم و ثبات ترتیبی را فرض کنیم. اگر A
و B
متغیرهای بولی معمولی هستند که در ابتدا نادرست هستند، برنامه زیر مسابقه داده ای ندارد:
موضوع 1 | موضوع 2 |
---|---|
if (A) B = true | if (B) A = true |
از آنجایی که عملیات مرتب نمیشوند، هر دو شرط نادرست ارزیابی میشوند و هیچیک از متغیرها هرگز بهروزرسانی نمیشوند. بنابراین نمی توان یک مسابقه داده وجود داشته باشد. نیازی نیست به این فکر کنید که اگر بار از A
و ذخیره به B
در Thread 1 به نحوی مرتب شود چه اتفاقی می افتد. کامپایلر مجاز نیست موضوع 1 را با بازنویسی آن به صورت " B = true; if (!A) B = false
" دوباره ترتیب دهد. این مثل درست کردن سوسیس در وسط شهر در روز روشن است.
نژادهای داده رسماً بر روی انواع پایه داخلی مانند اعداد صحیح و مراجع یا اشاره گر تعریف می شوند. اختصاص دادن به یک int
در حالی که همزمان آن را در یک رشته دیگر میخوانید، به وضوح یک مسابقه داده است. اما هم کتابخانه استاندارد C++ و هم کتابخانههای Java Collections نوشته شدهاند تا به شما اجازه دهند در مورد رقابتهای داده در سطح کتابخانه نیز استدلال کنید. آنها قول می دهند که نژادهای داده را معرفی نکنند مگر اینکه دسترسی های همزمان به همان کانتینر وجود داشته باشد که حداقل یکی از آنها آن را به روز می کند. به روز رسانی یک set<T>
در یک رشته در حالی که همزمان خواندن آن در رشته دیگر به کتابخانه اجازه می دهد تا یک مسابقه داده را معرفی کند و بنابراین می تواند به طور غیررسمی به عنوان یک "مسابقه داده در سطح کتابخانه" در نظر گرفته شود. برعکس، بهروزرسانی یک set<T>
در یک رشته، در حالی که خواندن مجموعهای دیگر در رشته دیگر، منجر به مسابقه داده نمیشود، زیرا کتابخانه قول میدهد که در آن مورد، مسابقه داده (سطح پایین) را معرفی نکند.
معمولاً دسترسی های همزمان به فیلدهای مختلف در یک ساختار داده نمی تواند یک مسابقه داده را معرفی کند. با این حال یک استثنا مهم برای این قاعده وجود دارد: دنباله های پیوسته فیلدهای بیت در C یا C++ به عنوان یک "محل حافظه" واحد در نظر گرفته می شوند. دسترسی به هر بیت فیلد در چنین دنباله ای به منزله دسترسی به همه آنها برای تعیین وجود یک مسابقه داده تلقی می شود. این نشان دهنده ناتوانی سخت افزار رایج در به روز رسانی تک تک بیت ها بدون خواندن و نوشتن مجدد بیت های مجاور است. برنامه نویسان جاوا هیچ نگرانی مشابهی ندارند.
اجتناب از مسابقه داده
زبان های برنامه نویسی مدرن تعدادی مکانیسم هماهنگ سازی را برای جلوگیری از رقابت داده ها ارائه می دهند. ابتدایی ترین ابزارها عبارتند از:
- قفل یا Mutexes
- Mutexes (C++11
std::mutex
، یاpthread_mutex_t
)، یا بلوکهایsynchronized
در جاوا را میتوان برای اطمینان از اینکه بخش خاصی از کد همزمان با بخشهای دیگر کد که به دادههای مشابهی دسترسی دارند اجرا نمیشود، استفاده کرد. ما به این و سایر امکانات مشابه به طور کلی به عنوان "قفل" اشاره خواهیم کرد. به دست آوردن یک قفل خاص قبل از دسترسی به یک ساختار داده مشترک و آزاد کردن آن پس از آن، از رقابت داده ها هنگام دسترسی به ساختار داده جلوگیری می کند. همچنین تضمین می کند که به روز رسانی ها و دسترسی ها اتمی هستند، یعنی هیچ به روز رسانی دیگری برای ساختار داده نمی تواند در وسط اجرا شود. این بدون شک رایج ترین ابزار برای جلوگیری از مسابقه داده ها است. استفاده از بلوکهایsynchronized
جاوا یا C++lock_guard
یاunique_lock
تضمین میکند که قفلها به درستی در صورت استثنا آزاد میشوند. - متغیرهای فرار/اتمی
- جاوا زمینه های
volatile
را فراهم می کند که از دسترسی همزمان بدون معرفی نژادهای داده پشتیبانی می کند. از سال 2011، C و C++ از متغیرهایatomic
و فیلدهایی با معنایی مشابه پشتیبانی می کنند. استفاده از اینها معمولاً دشوارتر از قفل است، زیرا آنها فقط اطمینان می دهند که دسترسی های فردی به یک متغیر واحد اتمی است. (در C++ این معمولاً به عملیات ساده خواندن، اصلاح و نوشتن، مانند افزایشها گسترش مییابد. جاوا برای آن به روش خاصی نیاز دارد.) برخلاف قفلها، متغیرهایvolatile
یاatomic
را نمیتوان مستقیماً برای جلوگیری از تداخل رشتههای دیگر با دنبالههای کد طولانیتر استفاده کرد. .
توجه به این نکته مهم است که volatile
معانی بسیار متفاوتی در C++ و جاوا دارد. در C++، volatile
از مسابقه دادهها جلوگیری نمیکند، اگرچه کدهای قدیمیتر اغلب از آن به عنوان راهحلی برای عدم وجود اشیاء atomic
استفاده میکنند. این دیگر توصیه نمی شود. در C++، از atomic<T>
برای متغیرهایی استفاده کنید که همزمان با چندین رشته قابل دسترسی هستند. C++ volatile
برای رجیسترهای دستگاه و موارد مشابه در نظر گرفته شده است.
از متغیرهای atomic
C/C++ یا متغیرهای volatile
جاوا میتوان برای جلوگیری از رقابت دادهها در سایر متغیرها استفاده کرد. اگر flag
به عنوان نوع atomic<bool>
یا atomic_bool
(C/C++) یا volatile boolean
(جاوا) اعلام شود، و در ابتدا false باشد، قطعه زیر بدون مسابقه داده است:
موضوع 1 | موضوع 2 |
---|---|
A = ... | while (!flag) {} |
از آنجایی که Thread 2 منتظر تنظیم flag
است، دسترسی به A
در Thread 2 باید پس از تخصیص به A
در Thread 1 و نه همزمان با آن اتفاق بیفتد. بنابراین هیچ مسابقه داده ای در A
وجود ندارد. مسابقه روی flag
به عنوان یک مسابقه داده به حساب نمی آید، زیرا دسترسی های فرار/اتمی "دسترسی های حافظه معمولی" نیستند.
پیاده سازی برای جلوگیری یا پنهان کردن مرتب سازی مجدد حافظه به اندازه کافی لازم است تا کدهایی مانند آزمون تورنسل قبلی مطابق انتظار رفتار کند. این امر معمولاً دسترسیهای حافظه فرار/اتمی را نسبت به دسترسیهای معمولی گرانتر میکند.
اگرچه مثال قبلی بدون مسابقه داده است، قفلها همراه با Object.wait()
در جاوا یا متغیرهای شرط در C/C++ معمولاً راهحل بهتری ارائه میدهند که شامل انتظار در یک حلقه در هنگام تخلیه باتری نیست.
وقتی ترتیب مجدد حافظه قابل مشاهده می شود
برنامه نویسی بدون مسابقه داده معمولاً ما را از برخورد صریح با مسائل مربوط به ترتیب مجدد دسترسی به حافظه نجات می دهد. با این حال، چندین مورد وجود دارد که در آن ترتیب مجدد قابل مشاهده است:- اگر برنامه شما دارای یک اشکال باشد که منجر به مسابقه داده ناخواسته می شود، تغییرات کامپایلر و سخت افزار می تواند قابل مشاهده باشد و رفتار برنامه شما ممکن است تعجب آور باشد. به عنوان مثال، اگر فراموش کرده باشیم که
flag
در مثال قبل فرار اعلام کنیم، ممکن است Thread 2 یکA
بدون مقدار اولیه را ببیند. یا ممکن است کامپایلر تصمیم بگیرد که پرچم نمی تواند در طول حلقه Thread 2 تغییر کند و برنامه را به
وقتی اشکال زدایی می کنید، ممکن است ببینید که این حلقه برای همیشه ادامه می یابد، علیرغم اینکهموضوع 1 موضوع 2 A = ...
flag = truereg0 = پرچم; در حالی که (!reg0) {}
... = الفflag
درست است. - C++ امکاناتی را برای آرامش صریح سازگاری متوالی فراهم می کند حتی اگر مسابقه ای وجود نداشته باشد. عملیات اتمی می تواند آرگومان
memory_order_
صریح ... را بگیرد. به طور مشابه، بستهjava.util.concurrent.atomic
مجموعه محدودتری از امکانات مشابه، به ویژهlazySet()
را فراهم می کند. و برنامه نویسان جاوا گهگاه از مسابقه داده های عمدی برای اثرات مشابه استفاده می کنند. همه اینها بهبود عملکرد را با هزینه زیادی در پیچیدگی برنامه نویسی ارائه می کنند. در زیر فقط به طور مختصر به آنها بحث می کنیم. - برخی از کدهای C و C++ به سبک قدیمیتر نوشته شدهاند که کاملاً با استانداردهای زبان فعلی سازگار نیست، که در آن از متغیرهای
volatile
بهجای متغیرهایatomic
استفاده میشود، و ترتیب حافظه با قرار دادن به اصطلاح حصارها یا موانع صراحتاً مجاز نیست. این نیاز به استدلال صریح در مورد ترتیب مجدد دسترسی و درک مدل های حافظه سخت افزاری دارد. یک سبک کدنویسی در امتداد این خطوط هنوز در هسته لینوکس استفاده می شود. نباید در برنامه های اندروید جدید استفاده شود و همچنین در اینجا بیشتر مورد بحث قرار نمی گیرد.
تمرین کنید
اشکال زدایی مشکلات ثبات حافظه می تواند بسیار دشوار باشد. اگر قفل مفقود، اعلان atomic
یا volatile
باعث میشود برخی از کدها دادههای قدیمی را بخوانند، ممکن است نتوانید دلیل آن را با بررسی تخلیه حافظه با دیباگر تشخیص دهید. تا زمانی که بتوانید درخواست اشکال زدایی را صادر کنید، هسته های CPU ممکن است همه مجموعه کامل دسترسی ها را مشاهده کرده باشند، و محتویات حافظه و رجیسترهای CPU در حالت "غیر ممکن" به نظر می رسد.
کارهایی که در C نباید انجام داد
در اینجا چند نمونه از کدهای نادرست را به همراه راه های ساده برای رفع آنها ارائه می دهیم. قبل از انجام این کار، باید در مورد استفاده از یک ویژگی اصلی زبان بحث کنیم.
C/C++ و "Vatile"
اعلانهای volatile
C و C++ ابزاری با هدف بسیار خاص هستند. آنها از کامپایلر از مرتب کردن مجدد یا حذف دسترسی های فرار جلوگیری می کنند. این می تواند برای دسترسی به کدهایی که به رجیسترهای دستگاه سخت افزاری، حافظه نگاشت شده به بیش از یک مکان یا در ارتباط با setjmp
کمک می کند. اما C و C++ volatile
، بر خلاف Java volatile
، برای ارتباط رشته طراحی نشده است.
در C و C++، دسترسیها به دادههای volatile
ممکن است با دادههای غیرفرّار قابل دسترسی مجدد باشد، و هیچ تضمینی برای اتمی بودن وجود ندارد. بنابراین volatile
نمی تواند برای به اشتراک گذاری داده ها بین رشته ها در کد قابل حمل استفاده شود، حتی در یک تک پردازنده. C volatile
معمولاً مانع از مرتب سازی مجدد دسترسی توسط سخت افزار نمی شود، بنابراین به خودی خود در محیط های SMP چند رشته ای حتی کمتر مفید است. این دلیلی است که C11 و C++11 از اجسام atomic
پشتیبانی می کنند. به جای آن باید از آن ها استفاده کنید.
بسیاری از کدهای قدیمی C و C++ هنوز از volatile
برای ارتباط رشته سوء استفاده می کنند. این اغلب برای دادههایی که در یک رجیستر ماشین قرار میگیرند درست عمل میکند، مشروط بر اینکه با حصارهای واضح یا در مواردی که ترتیب حافظه مهم نیست استفاده شود. اما کارکرد صحیح با کامپایلرهای آینده تضمین نمی شود.
نمونه ها
در بیشتر موارد بهتر است یک قفل (مانند pthread_mutex_t
یا C++11 std::mutex
) به جای یک عملیات اتمی داشته باشید، اما ما از قفل استفاده می کنیم تا نحوه استفاده از آنها را در یک موقعیت عملی نشان دهیم.
MyThing* gGlobalThing = NULL; // Wrong! See below. void initGlobalThing() // runs in Thread 1 { MyStruct* thing = malloc(sizeof(*thing)); memset(thing, 0, sizeof(*thing)); thing->x = 5; thing->y = 10; /* initialization complete, publish */ gGlobalThing = thing; } void useGlobalThing() // runs in Thread 2 { if (gGlobalThing != NULL) { int i = gGlobalThing->x; // could be 5, 0, or uninitialized data ... } }
ایده در اینجا این است که ما یک ساختار را تخصیص می دهیم، فیلدهای آن را مقداردهی اولیه می کنیم و در انتها با ذخیره آن در یک متغیر سراسری، آن را «انتشار» می کنیم. در آن نقطه، هر رشته دیگری می تواند آن را ببیند، اما این خوب است زیرا به طور کامل مقداردهی اولیه شده است، درست است؟
مشکل این است که ذخیره به gGlobalThing
را میتوان قبل از مقداردهی اولیه فیلدها مشاهده کرد، معمولاً به این دلیل که کامپایلر یا پردازنده فروشگاهها را به gGlobalThing
و thing->x
تغییر ترتیب دادهاند. رشته دیگری که از thing->x
می خواند می تواند 5، 0 یا حتی داده های اولیه را ببیند.
مشکل اصلی در اینجا مسابقه داده در gGlobalThing
است. اگر Thread 1 initGlobalThing()
فراخوانی کند در حالی که Thread 2 useGlobalThing()
فراخوانی کند، gGlobalThing
می توان در حین نوشتن خواند.
این را می توان با اعلام gGlobalThing
به عنوان اتمی برطرف کرد. در C++11:
atomic<MyThing*> gGlobalThing(NULL);
این تضمین می کند که نوشته ها به ترتیب مناسب برای موضوعات دیگر قابل مشاهده خواهند بود. همچنین تضمین می کند که از برخی حالت های خرابی دیگر که در غیر این صورت مجاز هستند، اما بعید است در سخت افزار واقعی اندروید رخ دهند، جلوگیری کند. به عنوان مثال، تضمین می کند که ما نمی توانیم یک نشانگر gGlobalThing
را ببینیم که فقط تا حدی نوشته شده است.
کارهایی که در جاوا نباید انجام داد
ما درباره برخی از ویژگیهای زبان جاوا صحبت نکردهایم، بنابراین ابتدا نگاهی گذرا به آنها خواهیم داشت.
جاوا از نظر فنی نیازی به کد ندارد تا عاری از مسابقه داده باشد. و مقدار کمی کد جاوا با دقت بسیار نوشته شده وجود دارد که در حضور مسابقه داده ها به درستی کار می کند. با این حال، نوشتن چنین کدی بسیار دشوار است و ما در زیر به طور خلاصه به آن می پردازیم. بدتر از همه، کارشناسانی که معنای چنین کدی را مشخص کردند، دیگر معتقد نیستند که این مشخصات صحیح است. (مشخصات برای کدهای بدون مسابقه داده مناسب است.)
در حال حاضر ما به مدل عاری از مسابقه دادهای پایبند هستیم، که جاوا اساساً تضمینهای مشابه C و C++ را برای آن ارائه میکند. باز هم، این زبان برخی از اصول اولیه را ارائه میکند که به وضوح سازگاری متوالی را کاهش میدهند، به ویژه فراخوانیهای lazySet()
و weakCompareAndSet()
در java.util.concurrent.atomic
. همانند C و C++، فعلاً این موارد را نادیده خواهیم گرفت.
کلمات کلیدی "همگام" و "فرار" جاوا
کلمه کلیدی "همگام" مکانیسم قفل داخلی زبان جاوا را فراهم می کند. هر شی دارای یک "مانیتور" مرتبط است که می تواند برای ارائه دسترسی منحصر به فرد متقابل استفاده شود. اگر دو رشته سعی کنند روی یک شی "همگام" شوند، یکی از آنها منتظر می ماند تا دیگری کامل شود.
همانطور که در بالا اشاره کردیم، volatile T
جاوا، آنالوگ atomic<T>
C++11 است. دسترسی همزمان به فیلدهای volatile
مجاز است و منجر به مسابقه داده نمی شود. نادیده گرفتن lazySet()
و همکاران. و مسابقه داده ها، وظیفه جاوا VM است که مطمئن شود که نتیجه همچنان به طور متوالی سازگار به نظر می رسد.
به طور خاص، اگر رشته 1 در یک فیلد volatile
بنویسد، و رشته 2 متعاقباً از همان فیلد خوانده شود و مقدار جدید نوشته شده را ببیند، رشته 2 نیز تضمین میشود که تمام نوشتههای قبلاً توسط رشته 1 را مشاهده کند. از نظر اثر حافظه، نوشتن به یک فرار شبیه به انتشار مانیتور است، و خواندن از یک فرار مانند یک مانیتور است.
یک تفاوت قابل توجه با atomic
C++ وجود دارد: اگر volatile int x;
در جاوا، x++
همان x = x + 1
است. بار اتمی را انجام می دهد، نتیجه را افزایش می دهد و سپس ذخیره اتمی را انجام می دهد. برخلاف C++، افزایش به طور کلی اتمی نیست. عملیات افزایش اتمی در عوض توسط java.util.concurrent.atomic
ارائه می شود.
نمونه ها
در اینجا یک پیاده سازی ساده و نادرست از یک شمارنده یکنواخت آورده شده است: ( نظریه و عمل جاوا: مدیریت نوسانات ) .
class Counter { private int mValue; public int get() { return mValue; } public void incr() { mValue++; } }
فرض کنید get()
و incr()
از چندین رشته فراخوانی می شوند، و ما می خواهیم مطمئن باشیم که هر رشته زمانی که get()
فراخوانی می شود، تعداد فعلی را می بیند. بارزترین مشکل این است که mValue++
در واقع سه عملیات است:
-
reg = mValue
-
reg = reg + 1
-
mValue = reg
اگر دو رشته به طور همزمان در incr()
اجرا شوند، ممکن است یکی از به روز رسانی ها از بین برود. برای اتمی کردن افزایش، باید incr()
«همگام» اعلام کنیم.
با این حال هنوز خراب است، به خصوص در SMP. هنوز یک مسابقه داده وجود دارد، زیرا get()
می تواند همزمان با incr()
به mValue
دسترسی داشته باشد. تحت قوانین جاوا، فراخوانی get()
می تواند با توجه به کدهای دیگر دوباره مرتب شود. برای مثال، اگر دو شمارنده را پشت سر هم بخوانیم، نتایج ممکن است ناسازگار به نظر برسند زیرا فراخوانیهای get()
که توسط سختافزار یا کامپایلر دوباره ترتیب دادهایم. میتوانیم با اعلام همگامسازی get()
مشکل را اصلاح کنیم. با این تغییر، کد به وضوح درست است.
متأسفانه، ما امکان جدال قفل را معرفی کرده ایم که می تواند عملکرد را مختل کند. بهجای اینکه get()
بهعنوان همگامسازی اعلام کنیم، میتوانیم mValue
با “Vatile” اعلام کنیم. (توجه داشته باشید که incr()
همچنان باید از synchronize
استفاده کند زیرا mValue++
در غیر این صورت یک عملیات اتمی واحد نیست.) با این کار از همه مسابقه داده ها جلوگیری می شود، بنابراین سازگاری متوالی حفظ می شود. incr()
تا حدودی کندتر خواهد بود، زیرا هم سربار ورود/خروج مانیتور و هم سربار مربوط به یک فروشگاه فرار را متحمل میشود، اما get()
سریعتر خواهد بود، بنابراین حتی در صورت عدم وجود اختلاف، اگر تعداد خواندهها بسیار بیشتر باشد، یک برد است. می نویسد. (برای راهی برای حذف کامل بلوک همگامسازی شده به AtomicInteger
نیز مراجعه کنید.)
در اینجا یک مثال دیگر، شبیه به نمونه های قبلی C است:
class MyGoodies { public int x, y; } class MyClass { static MyGoodies sGoodies; void initGoodies() { // runs in thread 1 MyGoodies goods = new MyGoodies(); goods.x = 5; goods.y = 10; sGoodies = goods; } void useGoodies() { // runs in thread 2 if (sGoodies != null) { int i = sGoodies.x; // could be 5 or 0 .... } } }
این مشکل مشابه کد C است، یعنی مسابقه داده در sGoodies
وجود دارد. بنابراین انتساب sGoodies = goods
ممکن است قبل از مقداردهی اولیه فیلدها در goods
مشاهده شود. اگر sGoodies
با کلمه کلیدی volatile
اعلام کنید، سازگاری ترتیبی بازیابی می شود و همه چیز همانطور که انتظار می رود کار خواهد کرد.
توجه داشته باشید که فقط خود مرجع sGoodies
فرار است. دسترسی به فیلدهای داخل آن نیست. هنگامی که sGoodies
volatile
است و ترتیب حافظه به درستی حفظ می شود، نمی توان همزمان به فیلدها دسترسی داشت. عبارت z = sGoodies.x
یک بار فرار از MyClass.sGoodies
و به دنبال آن یک بار غیر فرار از sGoodies.x
را انجام می دهد. اگر یک مرجع محلی MyGoodies localGoods = sGoodies
بسازید، آنگاه z = localGoods.x
بعدی هیچ بار فراری را انجام نخواهد داد.
یک اصطلاح رایج تر در برنامه نویسی جاوا، معروف "قفل کردن دوبار بررسی" است:
class MyClass { private Helper helper = null; public Helper getHelper() { if (helper == null) { synchronized (this) { if (helper == null) { helper = new Helper(); } } } return helper; } }
ایده این است که ما می خواهیم یک نمونه واحد از یک شی Helper
مرتبط با یک نمونه از MyClass
داشته باشیم. ما فقط باید یک بار آن را ایجاد کنیم، بنابراین آن را از طریق تابع getHelper()
اختصاصی ایجاد کرده و برمی گردانیم. برای جلوگیری از مسابقه ای که در آن دو رشته نمونه را ایجاد می کنند، باید ایجاد شی را همگام سازی کنیم. با این حال، ما نمیخواهیم سربار بلوک "همگامسازی شده" را در هر تماس بپردازیم، بنابراین فقط در صورتی این بخش را انجام میدهیم که helper
در حال حاضر تهی باشد.
این یک مسابقه داده در زمینه helper
دارد. می توان آن را همزمان با helper == null
در یک رشته دیگر تنظیم کرد.
برای اینکه ببینید چطور ممکن است شکست بخورد، همان کد را که کمی بازنویسی شده در نظر بگیرید، گویی به زبانی شبیه C کامپایل شده است (من چند فیلد عدد صحیح برای نمایش فعالیت سازنده Helper's
اضافه کرده ام):
if (helper == null) { synchronized() { if (helper == null) { newHelper = malloc(sizeof(Helper)); newHelper->x = 5; newHelper->y = 10; helper = newHelper; } } return helper; }
هیچ چیز مانعی ندارد که سختافزار یا کامپایلر فروشگاه را به helper
آنهایی که در فیلدهای x
/ y
هستند، سفارش دهد. رشته دیگری می تواند helper
غیر تهی را پیدا کند اما فیلدهای آن هنوز تنظیم نشده و آماده استفاده است. برای جزئیات بیشتر و حالتهای خرابی بیشتر، پیوند «قفل چک شده دوبار شکسته است» را در پیوست برای جزئیات بیشتر ببینید، یا مورد 71 («از مقداردهی اولیهسازی تنبل به طور عاقلانه استفاده کنید») در جاوا مؤثر جاش بلوخ، ویرایش دوم. .
دو راه برای رفع این مشکل وجود دارد:
- کار ساده را انجام دهید و بررسی بیرونی را حذف کنید. این تضمین می کند که ما هرگز ارزش
helper
را در خارج از یک بلوک همگام بررسی نمی کنیم. -
helper
فراری اعلام کنید. با این یک تغییر کوچک، کد موجود در مثال J-3 در جاوا 1.5 به بعد به درستی کار می کند. (شاید بخواهید یک دقیقه وقت بگذارید و خود را متقاعد کنید که این درست است.)
در اینجا یک مثال دیگر از رفتار volatile
است:
class MyClass { int data1, data2; volatile int vol1, vol2; void setValues() { // runs in Thread 1 data1 = 1; vol1 = 2; data2 = 3; } void useValues() { // runs in Thread 2 if (vol1 == 2) { int l1 = data1; // okay int l2 = data2; // wrong } } }
با نگاهی به useValues()
، اگر Thread 2 هنوز به روز رسانی به vol1
را مشاهده نکرده باشد، پس نمی تواند بداند که data1
یا data2
هنوز تنظیم شده است. هنگامی که بهروزرسانی vol1
را میبیند، میداند که میتوان به data1
به طور ایمن دسترسی پیدا کرد و بدون معرفی مسابقه داده، به درستی آن را خواند. با این حال، نمی تواند هیچ فرضی در مورد data2
داشته باشد، زیرا آن ذخیره پس از ذخیره فرار انجام شده است.
توجه داشته باشید که volatile
نمی تواند برای جلوگیری از ترتیب مجدد سایر دسترسی های حافظه که با یکدیگر رقابت می کنند استفاده شود. تضمینی برای تولید دستورالعمل حصار حافظه ماشین نیست. تنها زمانی می توان از آن برای جلوگیری از مسابقه داده با اجرای کد استفاده کرد که رشته دیگری شرایط خاصی را برآورده کند.
چه باید کرد
در C/C++، کلاس های همگام سازی C++11 مانند std::mutex
ترجیح دهید. اگر نه، از عملیات pthread
مربوطه استفاده کنید. اینها شامل حصارهای حافظه مناسب، ارائه رفتار صحیح (به طور متوالی، مگر اینکه خلاف آن مشخص شده باشد) و کارآمد در تمام نسخههای پلتفرم اندروید ارائه میدهند. حتما از آنها به درستی استفاده کنید. به عنوان مثال، به یاد داشته باشید که انتظارها متغیر شرط ممکن است به طور جعلی و بدون علامت بازگردند، و بنابراین باید در یک حلقه ظاهر شوند.
بهتر است از استفاده مستقیم از توابع اتمی خودداری کنید، مگر اینکه ساختار داده ای که پیاده سازی می کنید بسیار ساده باشد، مانند شمارنده. قفل کردن و باز کردن قفل mutex pthread هر کدام به یک عملیات اتمی نیاز دارد و اغلب هزینه کمتری از یک خطای کش دارد، اگر اختلافی وجود نداشته باشد، بنابراین با جایگزین کردن تماسهای mutex با عملیات اتمی صرفهجویی زیادی نمیکنید. طراحیهای بدون قفل برای ساختارهای دادهای غیر پیش پا افتاده به دقت بیشتری نیاز دارند تا اطمینان حاصل شود که عملیات سطح بالاتر در ساختار داده اتمی به نظر میرسند (به عنوان یک کل، نه فقط قطعات اتمی صریح آنها).
اگر از عملیات اتمی استفاده می کنید، سفارش راحت با memory_order
... یا lazySet()
ممکن است مزایای عملکردی را ارائه دهد، اما نیاز به درک عمیق تری نسبت به آنچه که تاکنون بیان کرده ایم دارد. بخش بزرگی از کدهای موجود با استفاده از اینها پس از این واقعیت کشف می شود که دارای اشکال است. در صورت امکان از این موارد اجتناب کنید. اگر موارد استفاده شما دقیقاً با یکی از موارد موجود در بخش بعدی مطابقت ندارد، مطمئن شوید که یا متخصص هستید یا با یکی از آنها مشورت کرده اید.
از استفاده از volatile
برای ارتباط رشته در C/C++ خودداری کنید.
در جاوا، مشکلات همزمانی اغلب با استفاده از یک کلاس ابزار مناسب از بسته java.util.concurrent
به بهترین وجه حل می شوند. کد به خوبی نوشته شده و به خوبی روی SMP تست شده است.
شاید مطمئن ترین کاری که می توانید انجام دهید این است که اشیاء خود را تغییرناپذیر کنید. اشیاء از کلاسهایی مانند رشته جاوا و عدد صحیح دادههایی را نگه میدارند که نمیتوان آنها را پس از ایجاد یک شی تغییر داد، و از همه پتانسیلهای نژاد داده در آن اشیا جلوگیری میکند. کتاب جاوا موثر، ویرایش دوم. دستورالعمل های خاصی در "مورد 15: به حداقل رساندن تغییرپذیری" دارد. به طور خاص به اهمیت اعلام فیلدهای جاوا "نهایی" ( Bloch ) توجه کنید.
حتی اگر یک شی غیرقابل تغییر باشد، به یاد داشته باشید که برقراری ارتباط آن با یک رشته دیگر بدون هیچ نوع همگام سازی یک مسابقه داده است. گاهی اوقات این می تواند در جاوا قابل قبول باشد (به زیر مراجعه کنید)، اما به دقت زیادی نیاز دارد و احتمالاً منجر به کدهای شکننده می شود. اگر عملکرد بسیار مهم نیست، یک اعلان volatile
اضافه کنید. در C++، برقراری ارتباط یک اشاره گر یا ارجاع به یک شیء تغییرناپذیر بدون همگام سازی مناسب، مانند هر مسابقه داده، یک اشکال است. در این مورد، به احتمال معقولی منجر به خرابی های متناوب می شود، زیرا، برای مثال، رشته دریافت کننده ممکن است به دلیل مرتب سازی مجدد فروشگاه، نشانگر جدول متد غیر اولیه را ببیند.
اگر نه یک کلاس کتابخانه موجود و نه یک کلاس تغییرناپذیر مناسب نیست، باید از دستور synchronized
جاوا یا C++ lock_guard
/ unique_lock
برای محافظت از دسترسیها به هر فیلدی که میتواند توسط بیش از یک رشته به آن دسترسی داشته باشد استفاده شود. اگر mutexes برای موقعیت شما کار نمی کند، باید فیلدهای مشترک را volatile
یا atomic
اعلام کنید، اما باید مراقب باشید تا تعاملات بین رشته ها را درک کنید. این اعلانها شما را از اشتباهات رایج برنامهنویسی همزمان نجات نمیدهند، اما به شما کمک میکنند از شکستهای مرموز مرتبط با بهینهسازی کامپایلرها و اشتباهات SMP جلوگیری کنید.
شما باید از "انتشار" ارجاع به یک شی، یعنی در دسترس قرار دادن آن برای موضوعات دیگر، در سازنده آن اجتناب کنید. این در C++ کمتر مهم است یا اگر به توصیه ما در جاوا "بدون مسابقه داده" پایبند باشید. اما اگر کد جاوا شما در زمینههای دیگری اجرا شود که در آن مدل امنیتی جاوا اهمیت دارد، همیشه توصیه خوبی است و بسیار مهم میشود، و کد غیرقابل اعتماد ممکن است با دسترسی به آن مرجع شیء «نشتشده» یک مسابقه داده را معرفی کند. همچنین بسیار مهم است که هشدارهای ما را نادیده بگیرید و از برخی از تکنیکهای بخش بعدی استفاده کنید. برای جزئیات بیشتر به ( تکنیک های ساخت و ساز ایمن در جاوا ) مراجعه کنید
کمی بیشتر در مورد دستورات حافظه ضعیف
C++11 و نسخههای بعدی مکانیسمهای واضحی را برای آرام کردن تضمینهای سازگاری متوالی برای برنامههای بدون مسابقه داده ارائه میکنند. آرگومان های صریح memory_order_relaxed
، memory_order_acquire
(فقط بارگیری)، و memory_order_release
(فقط ذخیره می کند) برای عملیات اتمی، هر کدام تضمین های ضعیف تری نسبت به پیش فرض، معمولاً ضمنی، memory_order_seq_cst
ارائه می دهند. memory_order_acq_rel
هر دو ضمانت memory_order_acquire
و memory_order_release
را برای عملیات نوشتن اتمی read-modify فراهم می کند. memory_order_consume
هنوز به اندازه کافی مشخص یا پیاده سازی نشده است تا مفید باشد و فعلاً باید نادیده گرفته شود.
متدهای lazySet
در Java.util.concurrent.atomic
شبیه به حافظه های C++ memory_order_release
هستند. متغیرهای معمولی جاوا گاهی اوقات به عنوان جایگزینی برای دسترسی های memory_order_relaxed
استفاده می شوند، اگرچه در واقع حتی ضعیف تر هستند. برخلاف C++، هیچ مکانیسم واقعی برای دسترسی های نامرتب به متغیرهایی که به عنوان volatile
اعلام می شوند وجود ندارد.
به طور کلی باید از این موارد اجتناب کنید، مگر اینکه دلایل عملکرد مبرمی برای استفاده از آنها وجود داشته باشد. در معماری های دستگاه ضعیف مانند ARM ، استفاده از آنها معمولاً به ترتیب چند ده چرخه دستگاه برای هر عملیات اتمی صرفه جویی می شود. در x86 ، پیروزی عملکرد محدود به فروشگاه ها است و احتمالاً کمتر مورد توجه قرار می گیرد. تا حدودی ضد بدنی ، ممکن است با تعداد هسته های بزرگتر ، سود آن کاهش یابد ، زیرا سیستم حافظه بیشتر به یک عامل محدود کننده تبدیل می شود.
معناشناسی کامل اتمیک های ضعیف پیچیده است. به طور کلی آنها نیاز به درک دقیقی از قوانین زبان دارند ، که ما به اینجا نمی رویم. به عنوان مثال:
- کامپایلر یا سخت افزار می تواند دسترسی به
memory_order_relaxed
را به یک بخش مهم که توسط یک قفل قفل و انتشار محدود شده است ، منتقل کند. این بدان معنی است که دو فروشگاهmemory_order_relaxed
ممکن است حتی اگر توسط یک بخش مهم از هم جدا شوند ، ممکن است خارج از نظم قابل مشاهده باشند. - یک متغیر جاوا معمولی ، هنگامی که به عنوان یک پیشخوان مشترک مورد آزار و اذیت قرار می گیرد ، ممکن است برای کاهش موضوع دیگر به نظر برسد ، حتی اگر فقط توسط یک موضوع دیگر افزایش یابد. اما این در مورد C ++ Atomic
memory_order_relaxed
صادق نیست.
با این کار به عنوان یک هشدار ، در اینجا تعداد کمی از اصطلاحات را ارائه می دهیم که به نظر می رسد بسیاری از موارد استفاده را برای اتمیک های ضعیف سفارش می دهد. بسیاری از این موارد فقط در مورد C ++ قابل استفاده است.
دسترسی های غیر مسابقه ای
کاملاً متداول است که یک متغیر اتمی است زیرا گاهی اوقات همزمان با نوشتن خوانده می شود ، اما همه دسترسی ها این مسئله را ندارند. به عنوان مثال ممکن است یک متغیر نیاز به اتمی داشته باشد زیرا در خارج از یک بخش مهم خوانده می شود ، اما تمام به روزرسانی ها توسط یک قفل محافظت می شوند. در این حالت ، خواندن که اتفاق می افتد توسط همان قفل محافظت می شود ، نمی تواند مسابقه دهد ، زیرا نمی تواند همزمان بنویسد. در چنین حالتی ، دسترسی غیر مسابقه ای (بار در این مورد) می تواند بدون تغییر صحت کد C ++ با memory_order_relaxed
حاشیه نویسی شود. اجرای قفل در حال حاضر سفارش حافظه مورد نیاز را با توجه به دسترسی توسط موضوعات دیگر اعمال می کند ، و memory_order_relaxed
مشخص می کند که اساساً هیچ محدودیت سفارش اضافی برای دسترسی اتمی لازم نیست.
هیچ آنالوگ واقعی در این مورد در جاوا وجود ندارد.
نتیجه برای صحت به آن اعتماد نمی شود
هنگامی که ما فقط برای تولید یک اشاره از یک بار مسابقه استفاده می کنیم ، به طور کلی خوب است که هیچ سفارش حافظه را برای بار اجرا نکنید. اگر مقدار قابل اعتماد نباشد ، ما همچنین نمی توانیم با اطمینان از نتیجه استفاده کنیم تا در مورد متغیرهای دیگر استنباط کنیم. بنابراین اگر سفارش حافظه تضمین نشود ، خوب است و بار با یک آرگومان memory_order_relaxed
تهیه می شود.
نمونه بارز این استفاده از c ++ compare_exchange
برای جایگزینی اتمی x
توسط f(x)
است. بار اولیه x
برای محاسبه f(x)
نیازی به قابل اعتماد بودن ندارد. اگر اشتباه کنیم ، compare_exchange
شکست خواهد خورد و دوباره امتحان خواهیم کرد. خوب است که بار اولیه x
استفاده از یک آرگومان memory_order_relaxed
؛ فقط سفارش حافظه برای موارد واقعی compare_exchange
.
داده های اتمی اصلاح شده اما خوانده نشده
گاهی اوقات داده ها به طور موازی توسط چندین موضوع اصلاح می شوند ، اما تا زمانی که محاسبه موازی کامل نشود ، بررسی نمی شوند. مثال خوب این پیشخوان است که به صورت اتمی افزایش می یابد (به عنوان مثال با استفاده از fetch_add()
در C ++ یا atomic_fetch_add_explicit()
در C) توسط چندین موضوع به صورت موازی ، اما نتیجه این تماس ها همیشه نادیده گرفته می شود. مقدار حاصل فقط در پایان خوانده می شود ، پس از اتمام تمام به روزرسانی ها.
در این حالت ، هیچ راهی برای گفتن اینکه آیا دسترسی به این داده ها مرتب شده است ، وجود ندارد ، و از این رو کد C ++ ممکن است از یک آرگومان memory_order_relaxed
استفاده کند.
پیشخوان های رویداد ساده یک نمونه مشترک از این هستند. از آنجا که بسیار رایج است ، ارزش برخی از مشاهدات در مورد این مورد را دارد:
- استفاده از
memory_order_relaxed
عملکرد را بهبود می بخشد ، اما ممکن است مهمترین مسئله عملکرد را برطرف نکند: هر بروزرسانی نیاز به دسترسی منحصر به فرد به خط حافظه نهان نگه داشتن پیشخوان دارد. این باعث می شود هر بار که یک موضوع جدید به پیشخوان دسترسی پیدا کند ، حافظه نهان از دست می رود. اگر به روزرسانی ها مکرر و متناوب بین موضوعات باشد ، بسیار سریعتر از به روزرسانی پیشخوان مشترک هر بار ، به عنوان مثال ، با استفاده از پیشخوان های محلی و محلی و جمع بندی آنها در پایان بسیار سریعتر است. - این روش با بخش قبلی قابل ترکیب است: می توان مقادیر تقریبی و غیرقابل اعتماد را در حالی که به روز می شوند بخوانید ، با تمام عملیات با استفاده از
memory_order_relaxed
. اما مهم است که مقادیر حاصل را کاملاً غیرقابل اعتماد درمان کنیم. فقط به این دلیل که به نظر می رسد تعداد آنها افزایش یافته است ، به این معنی نیست که موضوع دیگری را می توان شمارش کرد تا به نقطه ای برسد که در آن افزایش انجام شده است. در عوض ممکن است این افزایش با کد قبلی دوباره مرتب شود. (در مورد مورد مشابهی که قبلاً به آن اشاره کردیم ، C ++ تضمین می کند که بار دوم چنین پیشخوان مقداری کمتر از بار قبلی را در همان موضوع بر نمی گرداند. مگر اینکه البته پیشخوان سرریز شود.) - معمول است که کدی را پیدا کنید که سعی می کند مقادیر پیشخوان تقریبی را با انجام اتمی فردی (یا نه) بخواند و می نویسد ، اما باعث افزایش این اتمی کامل نمی شود. استدلال معمول این است که این "به اندازه کافی نزدیک" برای پیشخوان های عملکرد یا موارد مشابه است. به طور معمول نیست. هنگامی که به روزرسانی ها به اندازه کافی مکرر هستند (موردی که احتمالاً به آن اهمیت می دهید) ، بخش بزرگی از شمارش ها به طور معمول از بین می روند. در یک دستگاه چهار هسته ای ، بیش از نیمی از شمارش ها معمولاً از بین می روند. (تمرین آسان: یک سناریوی دو نخ را بسازید که در آن پیشخوان یک میلیون بار به روز شود ، اما مقدار پیشخوان نهایی یکی است.)
ارتباط پرچم ساده
یک فروشگاه memory_order_release
(یا عملیات Read-Mymify-Write) اطمینان می دهد که اگر متعاقباً یک بار memory_order_acquire
(یا عملیات خواندن-نوشتن-نوشتن) مقدار کتبی را بخواند ، سپس هر فروشگاهی (معمولی یا اتمی) را مشاهده می کند که قبل از A memory_order_release
بود فروشگاه در مقابل ، هر بارهایی که قبل از memory_order_release
وجود داشته باشد ، هیچ فروشگاهی را که از memory_order_acquire
استفاده می کنند ، مشاهده نمی کند. بر خلاف memory_order_relaxed
، این اجازه می دهد تا از چنین عملیات اتمی برای برقراری ارتباط یک موضوع به دیگری استفاده شود.
به عنوان مثال ، ما می توانیم نمونه قفل دوبل را از بالا در C ++ به عنوان بازنویسی کنیم
class MyClass { private: atomic<Helper*> helper {nullptr}; mutex mtx; public: Helper* getHelper() { Helper* myHelper = helper.load(memory_order_acquire); if (myHelper == nullptr) { lock_guard<mutex> lg(mtx); myHelper = helper.load(memory_order_relaxed); if (myHelper == nullptr) { myHelper = new Helper(); helper.store(myHelper, memory_order_release); } } return myHelper; } };
فروشگاه Load and Release اطمینان حاصل می کند که اگر یک helper
غیر تهی را ببینیم ، زمینه های آن را نیز به درستی آغاز خواهیم کرد. ما همچنین این مشاهدات قبلی را درج کرده ایم که بارهای غیر مسابقه می توانند از memory_order_relaxed
استفاده کنند.
یک برنامه نویس جاوا می تواند به عنوان یک java.util.concurrent.atomic.AtomicReference<Helper>
به عنوان یک فروشگاه Lazyset () از lazySet()
استفاده helper
. عملیات بار به استفاده از تماس های ساده get()
ادامه می یابد.
در هر دو مورد ، عملکرد ما در مسیر اولیه سازی متمرکز است ، که بعید است عملکرد بسیار مهم باشد. یک سازش قابل خواندن ممکن است:
Helper* getHelper() { Helper* myHelper = helper.load(memory_order_acquire); if (myHelper != nullptr) { return myHelper; } lock_guard<mutex> lg(mtx); if (helper == nullptr) { helper = new Helper(); } return helper; }
این همان مسیر سریع را فراهم می کند ، اما متوسل به عملیات پیش فرض ، پی در پی سازگار در مسیر آهسته غیرقانونی-بحرانی است.
حتی در اینجا ، helper.load(memory_order_acquire)
به احتمال زیاد همان کد را در معماری های فعلی پشتیبانی شده توسط اندرویدی به عنوان یک مرجع ساده (پی در پی سازگار) به helper
تولید می کند. واقعاً مفیدترین بهینه سازی در اینجا ممکن است معرفی myHelper
برای از بین بردن بار دوم باشد ، اگرچه یک کامپایلر آینده ممکن است به طور خودکار این کار را انجام دهد.
سفارش گرفتن/آزادی مانع از تأخیر در فروشگاه ها نمی شود و اطمینان نمی دهد که فروشگاه ها به ترتیب مداوم برای سایر موضوعات قابل مشاهده هستند. در نتیجه ، از یک الگوی کدگذاری مشکل ، اما نسبتاً متداول که نمونه ای از الگوریتم محرومیت متقابل Dekker است ، پشتیبانی نمی کند: همه موضوعات ابتدا پرچمی را تنظیم می کنند که نشان می دهد می خواهند کاری انجام دهند. اگر یک موضوع T پس از آن متوجه شود که هیچ موضوع دیگری در تلاش نیست کاری انجام دهد ، می تواند با خیال راحت ادامه یابد ، با دانستن اینکه هیچ تداخل وجود نخواهد داشت. هیچ موضوع دیگری قادر به ادامه کار نخواهد بود ، زیرا پرچم T هنوز تنظیم شده است. در صورت دسترسی به پرچم با استفاده از سفارش Acquire/Release ، این کار از بین می رود ، زیرا این امر مانع از آن نمی شود که پرچم نخ دیر برای دیگران قابل مشاهده باشد ، پس از انجام اشتباه. پیش فرض memory_order_seq_cst
از آن جلوگیری می کند.
زمینه های تغییر ناپذیر
اگر یک قسمت شیء در ابتدا استفاده شود و سپس هرگز تغییر نکند ، ممکن است با استفاده از دسترسی های ضعیف ، آن را اولیه و متعاقباً بخوانید. در C ++ ، می توان آن را به عنوان atomic
اعلام کرد و با استفاده از memory_order_relaxed
یا در جاوا قابل دسترسی بود ، می توان بدون volatile
اعلام کرد و بدون اقدامات ویژه دسترسی پیدا کرد. این امر مستلزم این است که همه موارد زیر را نگه دارید:
- باید از مقدار خود این زمینه بگویید که آیا قبلاً اولیه شده است یا خیر. برای دسترسی به این زمینه ، مقدار تست سریع مسیر و بازگشت باید فقط یک بار قسمت را بخوانید. در جاوا دومی ضروری است. حتی اگر تست های میدانی به صورت اولیه انجام شود ، بار دوم ممکن است مقدار ناخواسته قبلی را بخواند. در C ++ قانون "یک بار خوانده شده" صرفاً عمل خوبی است.
- هر دو اولیه سازی و بارهای بعدی باید اتمی باشند ، در آن به روزرسانی های جزئی قابل مشاهده نیست. برای جاوا ، این زمینه نباید
long
یاdouble
باشد. برای C ++ ، یک تکلیف اتمی مورد نیاز است. ساخت آن در محل کار نخواهد کرد ، زیرا ساخت یکatomic
اتمی نیست. - اولیه سازی های مکرر باید ایمن باشند ، زیرا چندین موضوع ممکن است مقدار نامعلوم را همزمان بخوانند. در C ++ ، این به طور کلی از الزام "قابل استفاده کپی" تحمیل شده برای همه انواع اتمی ناشی می شود. انواع دارای نشانگرهای متعلق به تو در تو در سازنده کپی نیاز به جابجایی دارند و به صورت بی اهمیت کپی نمی شوند. برای جاوا ، برخی از انواع مرجع قابل قبول هستند:
- منابع جاوا محدود به انواع غیرقابل تغییر است که فقط شامل زمینه های نهایی است. سازنده نوع تغییر ناپذیر نباید ارجاعی به شیء منتشر کند. در این حالت قوانین میدانی نهایی جاوا اطمینان حاصل می کند که اگر خواننده مرجع را ببیند ، زمینه های نهایی اولیه را نیز مشاهده می کند. C ++ هیچ آنالوگ با این قوانین ندارد و به این دلیل نیز به این دلیل قابل قبول نیست (علاوه بر نقض الزامات "ناپذیری کپی").
یادداشت های بسته شدن
در حالی که این سند بیش از آنکه سطح را خراشیده باشد ، بیش از یک غوغا کم عمق مدیریت نمی کند. این یک موضوع بسیار گسترده و عمیق است. برخی از مناطق برای اکتشاف بیشتر:
- مدل های حافظه واقعی جاوا و C ++ از نظر یک رابطه اتفاق قبل از وقوع بیان شده اند که مشخص می کند چه زمانی دو عمل تضمین می شود که به ترتیب خاصی رخ دهد. وقتی یک مسابقه داده را تعریف کردیم ، به طور غیررسمی در مورد دو دسترسی حافظه که به طور همزمان اتفاق می افتد صحبت کردیم. به طور رسمی این به عنوان هیچ کس قبل از دیگری تعریف نمی شود. یادگیری تعاریف واقعی اتفاقات قبل از اتفاقات و همزمان با مدل حافظه جاوا یا C ++ آموزنده است. اگرچه مفهوم شهودی "همزمان" به طور کلی به اندازه کافی خوب است ، اما این تعاریف آموزنده است ، به ویژه اگر در نظر دارید از عملیات اتمی ضعیف در C ++ استفاده کنید. (مشخصات جاوا فعلی فقط
lazySet()
بسیار غیررسمی تعریف می کند.) - در هنگام مرتب سازی کد ، چه کامپایلرهایی را انجام دهید و مجاز به انجام آن نیستند. (مشخصات JSR-133 نمونه های بسیار خوبی از تحولات حقوقی دارد که منجر به نتایج غیر منتظره می شود.)
- دریابید که چگونه کلاس های تغییر ناپذیر را در جاوا و C ++ بنویسید. (بیشتر از آن وجود دارد که فقط "بعد از ساخت و ساز تغییر نکنید".)
- توصیه ها را در بخش همزمانی جاوا مؤثر ، چاپ 2 درونی کنید. (به عنوان مثال ، شما باید از فراخوانی روش هایی که به معنای نادیده گرفتن در حالی که در داخل یک بلوک هماهنگ شده است ، خودداری کنید.)
- از طریق API های
java.util.concurrent
وjava.util.concurrent.atomic
بخوانید تا ببینید چه چیزی در دسترس است. استفاده از حاشیه نویسی های همزمانی مانند@ThreadSafe
و@GuardedBy
(از net.jcip.annotations) را در نظر بگیرید.
بخش خواندن بیشتر در پیوست پیوندهایی به اسناد و وب سایت ها دارد که بهتر این مباحث را روشن می کند.
ضمیمه
اجرای فروشگاه های هماهنگ سازی
(این چیزی نیست که بیشتر برنامه نویسان خود را در حال اجرای آن خواهند دید ، اما بحث در حال روشن است.)
برای انواع کوچک داخلی مانند int
و سخت افزار پشتیبانی شده توسط Android ، بار معمولی بار و دستورالعمل های فروشگاه اطمینان حاصل می کند که یک فروشگاه به طور کامل قابل مشاهده است ، یا اصلاً ، به یک پردازنده دیگر که در همان مکان بارگیری می شود ، قابل مشاهده است. بنابراین برخی از مفهوم اساسی "اتمی" به صورت رایگان ارائه می شود.
همانطور که قبلاً دیدیم ، این کافی نیست. به منظور اطمینان از قوام پی در پی ، ما نیز باید از تنظیم مجدد عملیات جلوگیری کنیم و اطمینان حاصل کنیم که عملیات حافظه به ترتیب مداوم برای سایر فرآیندهای قابل مشاهده است. به نظر می رسد که دومی در سخت افزار پشتیبانی شده توسط Android به صورت خودکار است ، مشروط بر اینکه ما برای اجرای سابق انتخاب های قاطعانه ای انجام دهیم ، بنابراین ما تا حد زیادی آن را در اینجا نادیده می گیریم.
ترتیب عملیات حافظه با جلوگیری از تنظیم مجدد توسط کامپایلر و جلوگیری از تنظیم مجدد توسط سخت افزار حفظ می شود. در اینجا ما روی دومی تمرکز می کنیم.
سفارش حافظه در ARMV7 ، X86 و MIPS با دستورالعمل های "حصار" اجرا می شود که تقریباً از دستورالعمل های مربوط به حصار جلوگیری می کند قبل از دستورالعمل های قبل از حصار. (این موارد معمولاً دستورالعمل های "مانع" نامیده می شوند ، اما این باعث سردرگمی با موانع pthread_barrier
-style می شود ، که خیلی بیشتر از این انجام می شود.) معنای دقیق دستورالعمل های حصار موضوعی نسبتاً پیچیده است که باید به روشی که در آن تضمین ها ارائه می شود ، بپردازد با انواع مختلف نرده ها در تعامل هستند ، و اینکه چگونه این ترکیب ها با سایر ضمانت های سفارش دهنده معمولاً توسط سخت افزار تهیه می شوند. این یک مرور کلی سطح بالا است ، بنابراین ما از این جزئیات براق خواهیم شد.
ابتدایی ترین نوع ضمانت سفارش این است که توسط C ++ memory_order_acquire
و memory_order_release
Atomic ارائه شده است: عملیات حافظه قبل از یک فروشگاه انتشار باید به دنبال بار اکتسابی قابل مشاهده باشد. در ARMV7 ، این توسط:
- قبل از دستورالعمل فروشگاه با یک دستورالعمل حصار مناسب. این مانع از دسترسی مجدد به حافظه قبلی با دستورالعمل فروشگاه می شود. (همچنین به طور غیر ضروری از تنظیم مجدد با دستورالعمل های بعدی فروشگاه جلوگیری می کند.)
- به دنبال دستورالعمل بار با یک دستورالعمل حصار مناسب ، جلوگیری از بارگذاری مجدد بار با دسترسی های بعدی. (و بار دیگر سفارشات غیر ضروری را با حداقل بارهای قبلی ارائه می دهد.)
این موارد برای سفارش C ++ به دست آوردن/انتشار کافی است. آنها برای volatile
جاوا یا C ++ به طور پی در پی سازگار هستند atomic
اما کافی نیستند.
برای دیدن چیز دیگری که ما به آن نیاز داریم ، قطعه الگوریتم Dekker را که قبلاً به طور خلاصه به آن اشاره کردیم ، در نظر بگیرید. flag1
و flag2
متغیرهای atomic
یا جاوا volatile
هستند ، هر دو در ابتدا نادرست هستند.
نخ 1 | موضوع 2 |
---|---|
flag1 = true | flag2 = true |
قوام پی در پی حاکی از آن است که یکی از تکالیف به flag
N ابتدا باید اجرا شود و در موضوع دیگر با آزمایش مشاهده شود. بنابراین ، ما هرگز شاهد این موضوعات نخواهیم بود که همزمان "پرشور" را اجرا می کنند.
اما شمشیربازی مورد نیاز برای سفارش انتشار فقط در ابتدا و پایان هر موضوع نرده هایی را اضافه می کند ، که در اینجا کمکی نمی کند. علاوه بر این ، ما باید اطمینان حاصل کنیم که اگر یک فروشگاه volatile
/ atomic
با یک بار volatile
/ atomic
دنبال شود ، این دو دوباره مرتب نمی شوند. این به طور معمول با اضافه کردن یک حصار نه فقط قبل از یک فروشگاه پی در پی سازگار ، بلکه پس از آن نیز اجرا می شود. (این دوباره بسیار قوی تر از حد مورد نیاز است ، زیرا این حصار به طور معمول دسترسی به حافظه قبلی را با توجه به همه موارد بعدی سفارش می دهد.)
در عوض می توانیم حصار اضافی را با بارهای پی در پی سازگار مرتبط کنیم. از آنجا که فروشگاه ها کمتر مکرر هستند ، کنوانسیون ای که ما توضیح دادیم رایج تر است و در Android استفاده می شود.
همانطور که در بخش قبلی دیدیم ، باید یک مانع فروشگاه/بار را بین این دو عمل وارد کنیم. کدهای اجرا شده در VM برای دسترسی بی ثبات چیزی شبیه به این است:
بار بی ثبات | فروشگاه بی ثبات |
---|---|
reg = A | fence for "release" (2) |
معماری های ماشین واقعی معمولاً انواع مختلفی از نرده ها را ارائه می دهند ، که انواع مختلفی از دسترسی ها را سفارش می دهند و ممکن است هزینه های متفاوتی داشته باشند. انتخاب بین این موارد ظریف است و تحت تأثیر لزوم اطمینان از اینکه فروشگاه ها به ترتیب مداوم در هسته های دیگر قابل مشاهده هستند ، و این که سفارش حافظه تحمیل شده توسط ترکیب آهنگ های متعدد به طور صحیح است. برای اطلاعات بیشتر ، لطفاً به صفحه دانشگاه کمبریج با نگاشتهای جمع آوری شده اتمی به پردازنده های واقعی مراجعه کنید.
در برخی از معماری ها ، به ویژه x86 ، موانع "Acquire" و "انتشار" غیر ضروری است ، زیرا سخت افزار همیشه به طور ضمنی ترتیب کافی را اعمال می کند. بنابراین در x86 فقط آخرین حصار (3) واقعاً تولید می شود. به طور مشابه در x86 ، عملیات اصلاح شده با نوشتن اتمی به طور ضمنی شامل یک حصار قوی است. بنابراین اینها هرگز به نرده ها احتیاج ندارند. در ARMV7 تمام نرده هایی که در بالا بحث کردیم مورد نیاز است.
ARMV8 دستورالعمل های LDAR و STLR را ارائه می دهد که مستقیماً نیازهای جاوا فرار یا C ++ را به طور متوالی و فروشگاه ها اجرا می کنند. اینها از محدودیت های غیرضروری مجدد که در بالا به آنها اشاره کردیم جلوگیری می کنند. کد اندرویدی 64 بیتی روی بازو از این موارد استفاده می کند. ما تصمیم گرفتیم که روی قرار دادن حصار ARMV7 در اینجا تمرکز کنیم زیرا این امر بیشتر به نیازهای واقعی می پردازد.
در ادامه مطلب
صفحات وب و اسنادی که عمق یا وسعت بیشتری دارند. مقالات به طور کلی مفیدتر از بالای لیست نزدیکتر هستند.
- مدل های قوام حافظه مشترک: یک آموزش
- اگر می خواهید عمیق تر به مدل های سازگاری حافظه بپردازید ، در سال 1995 توسط Adve & Gharachorloo نوشته شده است ، این مکان خوبی برای شروع است.
http://www.hpl.hp.com/techreports/compaq-dec/wrl-95-7.pdf - موانع حافظه
- مقاله کوچک خوب خلاصه موضوعات.
https://en.wikipedia.org/wiki/memory_barrier - مبانی موضوعات
- مقدمه ای برای برنامه نویسی چند رشته ای در C ++ و Java ، توسط هانس بوهم. بحث در مورد نژادهای داده و روشهای همگام سازی اساسی.
http://www.hbooehm.info/c+mm/threadsintro.html - همزمانی جاوا در عمل
- این کتاب که در سال 2006 منتشر شده است ، طیف گسترده ای از موضوعات را با جزئیات کامل در بر می گیرد. برای هرکسی که کد چند رشته ای در جاوا می نویسد بسیار توصیه می شود.
http://www.javaconcurrencyinpractice.com - JSR-133 (مدل حافظه جاوا) سؤالات متداول
- مقدمه ای ملایم با مدل حافظه جاوا ، از جمله توضیح هماهنگ سازی ، متغیرهای فرار و ساخت زمینه های نهایی. (کمی تاریخ ، به ویژه هنگامی که در مورد زبانهای دیگر بحث می کند.)
http://www.cs.umd.edu/~pugh/java/memorymodel/jsr-133-faq.html - اعتبار تحولات برنامه در مدل حافظه جاوا
- توضیح نسبتاً فنی در مورد مشکلات باقی مانده با مدل حافظه جاوا. این موضوعات در مورد برنامه های بدون داده مسابقه اعمال نمی شود.
http://citeseerx.ist.psu.edu/viewdoc/download؟doi=10.1.1.112.1790&rep=rep1&type=pdf - نمای کلی بسته java.util.concurrent
- مستندات بسته
java.util.concurrent
. در نزدیکی پایین صفحه ، بخشی با عنوان "خصوصیات قوام حافظه" قرار دارد که ضمانت های ارائه شده توسط کلاس های مختلف را توضیح می دهد.
خلاصه بستهjava.util.concurrent
- نظریه و عمل جاوا: تکنیک های ساخت و ساز ایمن در جاوا
- در این مقاله به تفصیل خطرات مربوط به فرار در هنگام ساخت شیء بررسی شده است و دستورالعملهایی را برای سازندگان امن موضوع ارائه می دهد.
http://www.ibm.com/developerworks/java/library/j-jtp0618.html - نظریه و عمل جاوا: مدیریت نوسانات
- یک مقاله خوب که آنچه را که می توانید و نمی توانید با زمینه های بی ثبات در جاوا انجام دهید ، توصیف می کند.
http://www.ibm.com/developerworks/java/library/j-jtp06197.html - اعلامیه "قفل دو چکیده شکسته است"
- توضیح مفصل بیل پو در مورد روشهای مختلفی که در آن قفل دو بررسی شده بدون
volatile
یاatomic
شکسته می شود. شامل C/C ++ و جاوا است.
http://www.cs.umd.edu/~pugh/java/memorymodel/doublecheckedlocking.html - [ARM] تست و کتاب آشپزی Barrier
- بحث در مورد مسائل SMP ARM ، که با قطعه های کوتاه کد بازو روشن شده است. اگر نمونه هایی را در این صفحه بسیار خاص پیدا کردید ، یا می خواهید توضیحات رسمی دستورالعمل DMB را بخوانید ، این مطلب را بخوانید. همچنین دستورالعمل های مورد استفاده برای موانع حافظه را در کد اجرایی (احتمالاً در صورت تولید کد در پرواز) مفید است). توجه داشته باشید که این امر ARMV8 را پیش بینی می کند ، که از دستورالعمل های سفارش حافظه اضافی نیز پشتیبانی می کند و به یک مدل حافظه تا حدودی قوی تر منتقل می شود. (برای جزئیات بیشتر به "کتابچه راهنمای معماری ARMS ARMV8" ، برای جزئیات بیشتر ARMV8-A مراجعه کنید.)
http://infocenter.arm.com/help/topic/com.arm.doc.genc007826/barrier_litmus_tests_and_cookbook_a08.pdf - موانع حافظه هسته لینوکس
- مستندات برای موانع حافظه هسته لینوکس. شامل برخی از نمونه های مفید و هنر ASCII است.
http://www.kernel.org/doc/documentation/memory-barriers.txt - ISO/IEC JTC1 SC22 WG21 (استاندارد C ++) 14882 (زبان برنامه نویسی C ++) ، بخش 1.10 و بند 29 ("کتابخانه عملیات اتمی")
- پیش نویس استاندارد برای ویژگی های عملکرد اتمی C ++. این نسخه نزدیک به استاندارد C ++ 14 است که شامل تغییرات جزئی در این منطقه از C ++ 11 است.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4527.pdf
(مقدمه: http://www.hpl.hp.com/techreports/2008/hpl-2008-56.pdf ) - ISO/IEC JTC1 SC22 WG14 (استاندارد C) 9899 (زبان برنامه نویسی C) فصل 7.16 ("Atomics <stdatomic.h>")
- پیش نویس استاندارد برای ویژگی های عملکرد اتمی ISO/IEC 9899-201X C. برای جزئیات بیشتر ، گزارش های نقص بعدی را نیز بررسی کنید.
http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf - C/C ++ 11 نقشه به پردازنده ها (دانشگاه کمبریج)
- مجموعه ترجمه های ترجمه های C ++ به مجموعه های مختلف دستورالعمل پردازنده مشترک ، Jaroslav Sevcik و Peter Sewell.
http://www.cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html - الگوریتم دکر
- "اولین راه حل صحیح شناخته شده برای مشکل محرومیت متقابل در برنامه نویسی همزمان". مقاله ویکی پدیا دارای الگوریتم کامل است ، با بحث در مورد چگونگی به روزرسانی آن برای کار با کامپایلرهای بهینه سازی مدرن و سخت افزار SMP.
https://en.wikipedia.org/wiki/dekker's_algorithm - نظرات در مورد ARM در مقابل آلفا و وابستگی آدرس
- نامه الکترونیکی در لیست پستی Arm-Cernel از Catalin Marinas. شامل خلاصه ای از وابستگی های آدرس و کنترل است.
http://linux.derkeiler.com/mailing-lists/kernel/2009-05/msg11811.html - آنچه هر برنامه نویس باید در مورد حافظه بداند
- مقاله ای بسیار طولانی و مفصل در مورد انواع مختلف حافظه ، به ویژه ذخیره های CPU ، توسط اولریش درپپر.
http://www.akkadia.org/drepper/cpumemory.pdf - استدلال در مورد مدل حافظه ضعیف سازگار
- این مقاله توسط Chong & Ishtiaq از Arm ، Ltd. نوشته شده است. سعی دارد مدل حافظه SMP ARM را به روشی دقیق اما در دسترس توصیف کند. تعریف "مشاهده" مورد استفاده در اینجا از این مقاله است. باز هم ، این امر ARMV8 را پیش می برد.
http://portal.acm.org/ft_gateway.cfm؟id=1353528&type=pdf&coll=&dl=&cfid=96099715&cftoken=57505711 - کتاب آشپزی JSR-133 برای نویسندگان کامپایلر
- داگ لی این را به عنوان همراه با مستندات JSR-133 (مدل حافظه جاوا) نوشت. این مجموعه شامل مجموعه اولیه دستورالعمل های اجرای مدل حافظه جاوا است که توسط بسیاری از نویسندگان کامپایلر استفاده شده است ، و هنوز هم به طور گسترده مورد استناد قرار می گیرد و احتمالاً بینش ارائه می دهد. متأسفانه ، چهار نوع حصار مورد بحث در اینجا مطابقت خوبی برای معماری های پشتیبانی شده توسط اندرویدی نیست ، و نقشه های فوق C ++ 11 اکنون منبع بهتری از دستور العمل های دقیق ، حتی برای جاوا هستند.
http://g.oswego.edu/dl/jmm/cookbook.html - x86-tso: یک مدل برنامه نویس دقیق و قابل استفاده برای چند پردازنده x86
- شرح دقیق مدل حافظه x86. توضیحات دقیق مدل حافظه بازو متأسفانه به طور قابل توجهی پیچیده تر است.
http://www.cl.cam.ac.uk/~pes20/weakmemory/cacm.pdf