اگر تابهحال در پروژهای که کنترل موجودی اهمیت دارد کدنویسی کرده باشید حتماً با مشکل همزمانی خرید مواجه شدهاید.
فرض کنید یک فروشگاه آنلاین برای فروش موبایل را کدنویسی میکنید. در این پروژه کنترل موجودی برای شما از اهمیت بالایی برخوردار است، زیرا نمیخواهید اجناسی که دیگر در انبار موجود نیستند را به مشتریان بفروشید.
زمانی که موجودی کالای شما چندین عدد باشد اگر همزمان دو نفر با هم یک کالا را خرید کنند مشکلی به وجود نمیآید. این دو این خرید با موفقیت ثبت میشود و دو عدد از موجودی انبار کم میشود.
اما اگر فقط تنها یک عدد از یک کالا در انبار موجود دارید و همزمان دهها نفر برای خرید این محصول شما اقدام کنند چه اتفاقی در انتظار شماست؟ فرض کنید همه این خریدها در یکلحظه ثبت بشوند.
شما یک محصول در انبار دارید؛ ولی آن را به چند نفر فروختهاید؟
حالا محصول را برای کدام یک ارسال میکنید؟
علت اصلی این مشکل چیست؟
در متدی که خرید را برای کاربر ثبت میکند معمولاً با همچین کدی بررسی میکنیم که موجودی کالا بیشتر یا برابر 1 باشد.
این کد موجودی کالا را دریافت میکند و اگر بیشتر و یا برابر 1 بود خرید را ثبت میکند، در برنامههای Single Thread بدون هیچ مشکلی این کد کنترل موجودی را برای ما انجام میدهد.
اما در برنامههای MultiThread همیشه این کد بهدرستی اجرا نمیشود. چرا؟ بهخاطر Context Switch که بین تردها توسط سیستمعامل انجام میشود.
Context Switch چیست؟
اگر از یک کامپیوتر با CPU هشت هستهای استفاده کنیم، CPU در هر لحظه فقط 8 عدد کار همزمان میتواند انجام دهد، البته با استفاده از Hyper-Threading هر هسته CPU در یکلحظه میتواند 2 کار بهصورت موازی انجام دهد و این یعنی یک کامپیوتر با 8 هسته در هر لحظه فقط توانایی انجام 16 کار همزمان را دارد.
اما به تصویر زیر دقت کنید. در یک کامپیوتر با پردازنده 8 هستهای 335 فرایند (Process) در حال اجرا است و این فرایندها در مجموع 5353 , ترد را ایجاد کردهاند که همه این Threadها توسط 8 هسته CPU در حال انجام کارهای خود هستند.
Thread چیست؟
Thread یا نخ، یک مکان ایزوله برای اجرای کدهای برنامه در CPU است. معمولاً هر برنامه از 1 تا n ترد میتواند داشته باشد. وقتی یک برنامه Console Application در داتنت ایجاد میکنیم کدهای این برنامه در یک Thread اجرا میشوند. البته خودمان میتوانیم Threadهای بیشتر برای برنامه ایجاد کنیم.
حالا که با مفهوم Thread آشنا شدیم یکبار دیگر به تصویر بالا نگاهی بیندازیم.
تعداد 5,353 عدد Thread داریم که باید در 8 هسته CPU انجام شوند و هر هسته همزمان میتواند 2 عدد Thread را اجرا کند. پس یعنی این 5 هزار Thread در هر لحظه فقط 16 عدد از آنها میتواند در CPU اجرا شوند.
اینجاست که سیستمعامل از Context Switch استفاده میکند تا به نظر برسد همه Threadها همزمان در حال اجرا هستند.
Context Switch چطور انجام میشود؟
در هر کامپیوتر ممکن است چندین هزار Thread در لحظه وجود داشته باشد که این Threadها کارهای برنامههای مختلف در حال اجرا را انجام میدهند و برای انجام کار هم نیاز به هستههای CPU دارند.
اما تعداد هستههای CPU نسبت به تعداد Threadها بسیار محدود است و سیستمعامل با توجه به الگوریتمهای خاصی که دارد در هر لحظه تعدادی از Threadها را انتخاب میکند و هستههای CPU را در اختیار آن Threadهای انتخاب شده قرار میدهد تا بتوانند محاسبات خود را توسط CPU انجام دهند.
اما دقت کنید مدت زمانی که هسته CPU در اختیار یک Thread قرار داده میشود بسیار محدود است و ممکن است در آن بازه زمانی بسیار کم نتواند کار خود را کامل انجام دهد. اگر Thread در این بازه زمانی که هسته CPU در اختیار آن قرار داده شده است نتواند کار خود را انجام دهد، سیستمعامل CPU را از Thread میگیرد و به Thread دیگری تحویل میدهد و Thread جاری دوباره در صف قرار می گیرد تا بتواند بعد از گذشت مدت زمانی، دوباره هسته CPU را تحویل بگیرد. به این فرایند گرفتن هسته CPU از ترد Context Switch میگویند.
درضمن قسمتی از حافظه در اختیار Threadها قرار داده میشود که اطلاعات خود را نگهداری کنند و بعد از آنکه دوباره هسته CPU در اختیار آنها قرار داده شد بتوانند ادامه فرایند را انجام دهند.
مشکل همزمانی پیدا شد
در برنامههای MultiThreading بهخاطر همین Context Switch که بین Thread ها انجام میشود احتمال دارد که یک محصول باقیمانده در انبار را به چند نفر بفروشیم! البته اگر چندین نفر در یکلحظه برای خرید اقدام کنند ممکن است این اتفاق ناگوار رخ دهد.
حالا با هم برنامه زیر را با دو Thread و یک عملیات Context Switch بررسی کنیم و ببینیم دقیقاً این مشکل به چه صورت رخ میدهد.
فرض کنید دو عدد Thread با نامهای Thread-A و Thread-B داریم. این برنامه همزمان است و این دو یا چند Thread بهصورت کاملاً همزمان میتوانند از کد ثبت سفارش خرید استفاده کنند.
مرحله 1 : Thread -A وارد خط 46 میشود و بررسی میکند که آیا موجودی کافی است یا خیر؟ که عدد 1 را دریافت میکند و یعنی هنوز یک عدد از کالا موجود است و میتواند خرید خود را انجام دهد.
مرحله 2: بهمحض این که Thread-A وارد خط 47 میشود عملیات Context Switch توسط سیستمعامل انجام میشود و Thread-A به مدتزمان کاملاً نامشخصی نمیتواند ادامه فرایند را انجام دهد.
مرحله 3 : Thread-B از راه میرسد و وارد خط 46 میشود و این ترد هم موجودی را میگیرید و چون هنوز این کالا موجود است وارد خط 47 میشود و ادامه فرایند را تا ثبت کامل خرید و حتی کسر موجودی انجام میدهد. خرید Thread-B کامل و با موفقیت انجام میشود.
مرحله 4: و حالا دوباره سیستمعامل یکی از هستههای CPU را در اختیار Thread-A قرار میدهد تا ادامه کار خود را از هما خط 47 انجام دهد.
خب Thread-A قبلاً موجودی را چک کرده است میرود برای ادامه فرایند ثبت خرید و موجودی را هم کسر میکند.
به همین راحتی دو سفارش برای کالایی که تنها یک موجودی داشت ثبت شد.
دلیل اصلی این مشکل چه بود؟ مشکل از اینجا نشات میگیرد که چند ترد دسترسی به دادههای مشترک دارند و بدون اطلاع از هم دادههای مشترک را ویرایش میکنند. در این مثال دادههای مشترک موجودی کالا است.
چطور مشکل را حل کنیم؟
راهحل این مشکل این است که از ویرایش همزمان دادههای مشترک بین تردها جلوگیری کنیم.
چطوری میتوانیم این کار را انجام دهیم؟ باید قسمتی از کد را بهصورت Sync انجام دهیم. یعنی در هر لحظه فقط یک Thread مجوز خواندن و یا نوشتن دادههای مشترک را داشته باشد و Threadهای دیگر در یک صف صبر کنند که نوبت به آنها برسد و بتوانند کار خود را ادامه دهند.
استفاده از دستور lock در سیشارپ برای قفل گذاری
در زبان سیشارپ با استفاده از کلمه کلیدی lock میتوانیم قسمتی از کد را قفل کنیم که در لحظه فقط یک نفر بتواند آن قسمت از کد که معمولاً ناحیه بحرانی نامیده میشود را اجرا کند.
و حالا با استفاده از lock یکبار دیگر فرایند را Trace کنیم.
مرحله 1 : Thread-A وارد خط 47 میشود و شئ locker را قفل میکند و هیچ Thread دیگری نمیتواند از خط 47 عبور کند تا زمانی که قفل locker توسط Thread-A آزاد نشود.
این ترد وارد خط 49 میشود و موجودی را چک میکند و ادامه فرایند را برای ثبت سفارش را انجام میدهد.
مرحله 2 : Thread-B وارد خط 47 میشود و میخواهد شیء locker را قفل کند، اما نمیتواند؛ چون قبلاً توسط یک Thread دیگر قفل شده و هنوز آزاد نشده است. پس مجبور است در همینجا صبر کند تا قفل توسط تردی که آن را در اختیار دارد آزاد شود.
مرحله 3 : Thread-A همه فرایندهای موردنیاز برای ثبت سفارش را انجام میدهد و به خط 55 میرسد و قفل را آزاد میکند.
مرحله 4: و حالا Thread-B شیء locker را قفل میکند و ادامه کار را انجام میدهد. اما به خط 49 میرسد و میبیند که موجودی در انبار برابر 0 است و نمیتواند سفارش را ثبت نماید.
به همین راحتی توانستیم مشکل همزمانی کد ثبت سفارش در یک پروژه فروشگاهی را برطرف نماییم.
توجه کد متد GetInventory بهصورت نمادین نوشته شده است.
و در این مثال فرض شده است که محصول موردنظر فقط یک عدد در انبار موجود است.
البته روشهای دیگری هم در داتنت برای قفل گذاری بر روی تردها وجود دارد؛ اما سادهترین و پرکاربردترین آن روشها همین کلمه کلیدی lock است.
در فصل 17 از دوره ستارگان سیشارپ بهصورت تخصصی روشهای قفل گذاری در داتنت را بررسی کردهایم.
فیلم 🎬 نحوه استفاده از دستور lock در Asp.Net Core را برای شما آماده اکرده ایم و در BugetoTV آپلود شده، می توانید مشاهده نمایید.
برای افزودن دیدگاه خود، نیاز است ابتدا وارد حساب کاربریتان شوید