مقدمه
به فصل پانزدهم خوش آمدید. در دنیای مدرن، کاربران انتظار دارند که برنامهها همیشه سریع و پاسخگو
(responsive) باشند، حتی زمانی که در حال انجام کارهای سنگین مانند دانلود فایل، پردازش داده یا
ارتباط با یک پایگاه داده هستند. کامپیوترهای امروزی نیز با داشتن چندین هستهی پردازشی (CPU
cores)، قابلیت انجام چندین کار را به صورت همزمان دارند. برای بهرهبرداری از این قابلیتها و
ساختن برنامههای مدرن، باید با مفاهیم همزمانی (Concurrency) و
موازیسازی (Parallelism) آشنا شویم. در این فصل، یاد میگیریم که چگونه با
استفاده از نخها (Threads) و ابزارهای سطح بالاتر در C#، برنامههایی بنویسیم که
بتوانند چندین عملیات را به صورت همزمان مدیریت و اجرا کنند.
همزمانی (Concurrency) در مقابل موازیسازی (Parallelism)
این دو مفهوم اغلب به جای یکدیگر استفاده میشوند، اما معنای متفاوتی دارند و درک این تفاوت بسیار
مهم است.
همزمانی (Concurrency)
همزمانی یعنی سروکار داشتن با چند کار به صورت همزمان. این لزوماً به معنای اجرای
همزمان آنها در یک لحظهی واحد نیست. یک برنامه میتواند بر روی یک هستهی CPU واحد نیز همزمان
باشد. در این حالت، سیستمعامل به سرعت بین وظایف مختلف جابجا میشود (فرآیندی به نام time-slicing)
و به هر کدام بخش کوچکی از زمان پردازنده را اختصاص میدهد. این کار این توهم را
ایجاد میکند که چند کار با هم در حال اجرا هستند، در حالی که در هر لحظه فقط یک کار در حال پیشرفت
است.
هدف اصلی همزمانی، افزایش پاسخگویی (responsiveness) برنامه است. برای مثال، در یک
برنامهی دسکتاپ، اگر یک عملیات سنگین روی نخ اصلی (UI thread) اجرا شود، کل برنامه "یخ میزند" و
کاربر نمیتواند با آن تعامل کند. با اجرای آن عملیات در یک نخ جداگانه، رابط کاربری پاسخگو باقی
میماند.
موازیسازی (Parallelism)
موازیسازی یعنی انجام دادن چند کار به صورت همزمان. این کار نیازمند سختافزار با
چندین هستهی پردازشی است. در این حالت، دو یا چند وظیفه میتوانند دقیقاً در یک لحظهی واحد و بر
روی هستههای مجزا اجرا شوند.
هدف اصلی موازیسازی، افزایش کارایی (performance) و سرعت بخشیدن به محاسبات است.
برای مثال، اگر بخواهیم یک تصویر بزرگ را پردازش کنیم، میتوانیم تصویر را به چهار قسمت تقسیم کرده
و هر قسمت را بر روی یک هستهی جداگانه پردازش کنیم. این کار زمان کل پردازش را به طور قابل توجهی
کاهش میدهد.
نخها (Threads): واحد سازندهی اجرا
واحد اصلی اجرا در یک سیستمعامل، نخ (Thread) است. یک نخ، دنبالهای از
دستورالعملهاست که سیستمعامل میتواند آن را به صورت مستقل اجرا کند. هر پروسه (برنامه) حداقل یک
نخ اصلی دارد. برای دستیابی به همزمانی و موازیسازی، ما نخهای جدیدی ایجاد میکنیم.
زمانبند (scheduler) سیستمعامل مسئول تخصیص این نخها به هستههای موجود CPU است. اگر تعداد نخها
از تعداد هستهها بیشتر باشد، سیستمعامل با جابجایی سریع بین آنها، همزمانی را شبیهسازی میکند.
اگر تعداد نخها کمتر یا مساوی تعداد هستهها باشد، میتواند آنها را به صورت واقعاً موازی اجرا
کند.
چرا برنامهنویسی چندنخی دشوار است؟
با وجود قدرت بالایی که چندنخی به ما میدهد، چالشهای جدید و پیچیدهای را نیز به همراه دارد:
- شرایط رقابتی (Race Conditions): زمانی رخ میدهد که چند نخ سعی میکنند به یک
دادهی مشترک در حافظه به صورت همزمان دسترسی پیدا کرده و آن را تغییر دهند. این کار میتواند
منجر به نتایج غیرقابل پیشبینی و نادرست شود. برای مثال، اگر دو نخ همزمان سعی کنند یک شمارنده
را یک واحد افزایش دهند، ممکن است در نهایت شمارنده فقط یک واحد افزایش یابد، نه دو واحد.
- بنبست (Deadlocks): وضعیتی که در آن دو یا چند نخ برای همیشه مسدود شدهاند،
زیرا هر کدام منتظر منبعی است که توسط دیگری قفل شده است.
- پیچیدگی و خطا: مدیریت دستی ایجاد، توقف و همگامسازی نخها بسیار پیچیده و
مستعد خطا است.
رویکردهای مدرن در C#
خوشبختانه، .NET ابزارهای سطح بالاتری را برای سادهسازی برنامهنویسی همزمان و موازی
فراهم کرده است که ما را از درگیری مستقیم با مدیریت نخها بینیاز میکند. در درسهای آینده با این
ابزارها آشنا خواهیم شد:
- کتابخانهی موازیسازی وظایف (Task Parallel Library - TPL): یک مجموعهی غنی
از APIها برای کار با "وظایف" (Tasks) که مفهومی سطح بالاتر از نخها هستند.
- کلمات کلیدی async و await: ابزار اصلی زبان C# برای نوشتن
کدهای ناهمزمان (asynchronous) که پاسخگو هستند و نخها را مسدود نمیکنند. این روش به خصوص
برای عملیات ورودی/خروجی (I/O-bound) مانند کار با شبکه و فایلها ایدهآل است.
- LINQ موازی (PLINQ): یک راه ساده برای موازیسازی کوئریهای LINQ
بر روی دادهها و بهرهبرداری از تمام هستههای CPU.
در درس بعدی، با نحوهی ایجاد دستی یک نخ ثانویه به عنوان اولین قدم در دنیای چندنخی آشنا خواهیم
شد.