آشنایی با مفهوم نخ در پایتون | راهنمای پیشرفته

در این مقاله در مورد روش افزایش سرعت کدهای پایتون و ساخت اپلیکیشنهای چندنخی (multi-threaded) و همچنین برخی جنبههای مهم مفهوم نخ در پایتون یا همان Thread در پایتون صحبت خواهیم کرد. ابتدا توضیحاتی در مورد برخی تفاوتهای نسخههای پایتون (2.7 تا 3.x) ارائه میکنیم و سپس برخی مفاهیم نخها را معرفی کرده و در نهایت مثالهایی از شیوه آغاز استفاده از ترد در پایتون مطرح خواهیم کرد.
پایتون و کاربردهای آن
پایتون زبان جذابی است. این زبان کاربردهای بسیار متنوعی از برنامهنویسی هوش مصنوعی، تحلیل داده، توسعه وبسایت، وب اسکرپینگ و دستکاری دادهها دارد و حتی اسکریپتهای ساده فیبوناچی با این زبان نوشته میشوند. در واقع کاربردهای پایتون بیشمار هستند و هر چه بیشتر آن را بیاموزید، کاربردهای بیشتری برای آن مییابید.
پایتون 2.7 متوقف شده است، اما هنوز به طور گسترده از سوی جامعه کاربران و سازمانهای مختلف مورد استفاده قرار میگیرد. پشتیبانی از این نسخه در ابتدای سال 2020 متوقف شده است و یک نسخه نهایی مهم به شماره 2.7.18 در 20 آوریل 2020 انتشار یافته است. با توجه به این واقعیتها همه توسعهدهندگان باید این نسخه را کنار گذاشته و شروع به استفاده از نسخههای 3.x این زبان بکنند.
پایتون 3 مجموعه قابلیتهای جدیدی را نسبت به سلف خود اضافه کرده است که شامل موارد فهرست زیر است:
- معرفی گزاره print در برابر تابع ()print
- تقسیم با اعداد صحیح
- پشتیبانی یونیکد
- تغییر ()xrange به ()range
- ایجاد استثنا و مدیریت آنها
- نشت متغیر حلقه و متغیر سراسری
- تغییر ()raw_input به ()input
- رندسازی Bankers
البته فهرست تغییرهای نسخههای پایتون به این موارد محدود نمیشوند. برای کسب اطلاعات بیشتر بهتر است مستندات پایتون 2.7 (+) و پایتون 3.8 (+) را بررسی کنید.
مفاهیم مرتبط با نخ (Thread)
پیش از آن که وارد مثالهای کاربرد نخ در پایتون شویم، برخی مفاهیم کلیدی هستند که باید به طور کلی مرور کنیم.
کرنل
کرنل سیستم وظایف بسیار متنوعی را بر عهده دارد. کرنل حلقه ارتباطی بین سختافزار دستگاه و نرمافزار است. همچنین جدول زمانبندی که هر پردازش روی هر هسته پردازنده و در هر چرخه باید اجرا شود را مدیریت میکند. این کرنل است که به توسعهدهنده امکان میدهد تا یک نخ ایجاد کرده و به حافظه روی ماشین دسترسی پیدا کند. فراخوانیهای کرنل توسط زبانهای برنامهنویسی کپسولهسازی میشوند و از این رو جلوی انجام کارهای خطرناکی مانند نوشتن در مکان نادرستی از حافظه گرفته میشود. هر چه یک زبان برنامهنویسی سطح بالاتری داشته باشد، API-های بیشتری برای اتصال زبان سطح بالا به فراخوانیهای سطح پایین مورد استفاده قرار میگیرند.
پردازش
«پردازش» (Process) روش سیستم عامل برای مدیریت وهلههای اجرایی یک برنامه است که در یک فایل اجرایی قرار دارند. هر پردازش که ایجاد میشود یک نخ اصلی منفرد نیز آغاز میشود. یک پردازش میزبان فضای ذخیره مشترکی است که همه نخهای درون پردازش در آن قرار میگیرند، از این رو پردازش را میتوان به مثابه یک «محیط» برای نخها در نظر گرفت که دادههای درونش را پردازش میکند.
چند وظیفگی پیشدستانه
در روزهای آغازین ظهور رایانهها، CPU-ها تنها یک هسته منفرد داشتند که یک جریان ورودی منفرد را میپذیرفت. به این ترتیب هر زمان تنها یک پردازش میتوانست اجرا شود. به مرور که رایانهها تکامل یافتند، هستههای بیشتری به پردازندهها افزوده شد و امکان داشتن چندین جریان همزمان از دستورالعملها فراهم آمد از این رو اینک میتوانستید چند پردازش را با هم اجرا کنید. به روش زمانبندی سیستم عامل برای اجرای وظایف روی پردازنده «چند وظیفگی پیشدستانه» (Preemptive Multitasking) گفته میشود. به این ترتیب به جای این که منتظر بمانیم یک وظیفه منفرد تکمیل شود تا بتوانیم به وظیفه بعدی برویم، با استفاده از چند وظیفگی پیشدستانه میتوانیم یک زمانبندی بر مبنای مرور فهرست پردازشها داشته باشیم و بررسی کنیم کدام پردازش میتواند اجرا شود و آن را روی چرخه زمانی آتی هسته خالی بعدی اجرا کنیم.
نخ در پایتون
نخ یا ترد یک وهله اجرایی از جریان منفرد دستورالعملهای زبان ماشین را کپسولهسازی میکند. چندین نخ میتوانند درون یک پردازش منفرد قرار داشته باشند. اگر تاکنون با موقعیتی مواجه شدهاید که برنامهای درست پس از کلیک کردن روی دکمه «محاسبه/رندر» قفل شود، این احتمال وجود دارد که همه پردازشهای آن روی یک نخ منفرد اجرا میشوند و از این رو UI برنامه قفل میشود. بنابراین نباید کارهای محاسباتی سنگین را روی نخ UI اجرا کنید.
نخ دارای خصوصیات زیر است:
- یک شناسه نخ (TID) یکتا برای هر نخ در یک پردازش وجود دارد که البته با در نظر گرفتن همه نخهای موجود در سیستم یکتا نیست.
- «پشته فراخوانی» (Call Stack) وجود دارد که دستورالعملهایی که نخ در طی زمان خود روی هسته اجرا میکند در آن قرار دارند.
- مقادیر همه رجیسترهای با مقاصد خاص و عام در نخ قرار دارند. به طور معمول تنها رجیسترهای حالت کاربر (user-mode) وجود دارند، مگر این که برنامه خاصی باشد.
- یک بلوک از حافظه با مقاصد عام به نام فضای ذخیرهسازی لوکال نخ (TSL) وجود دارد.
- دسترسی به حافظه در اختیار همه نخهای درون پردازش قرار گرفته است.
کتابخانههای نخ
بسیاری از کتابخانههای نخبندی برخی از کارکردهای مقدماتی کار با نخ را که شامل فهرست زیر میشوند، عرضه کردهاند:
- ایجاد یک نخ
- خاتمه یک نخ
- درخواست خروج از یک نخ
- خوابیدن نخ با تعیین زمان
- اعطای باقیمانده زمان یک نخ به نخ دیگر
- انتظار برای پایان دادن کار نخ و الحاق مجدد به نخ اصلی
به طور معمول، یک نخ تا زمانی که خاتمه یافته است، کار میکند، با این حال گاهی اوقات یک نخ ممکن است منتظر یک رویداد بماند تا پس از وقوع آن ادامه یابد و تا زمانی که تکمیل نشده است، پردازنده را در اختیار خود بگیرد. برای این که از اجرای طولانی مدت نخ روی هسته جلوگیری کنیم و به نخهای دیگر نیز اجازه دهیم که اجرا شوند، سه روش در اختیار توسعهدهنده قرار داده شده است که شامل «نظرسنجی» (Polling) «مسدودسازی» (Blocking) و «اعطا» (Yielding) میشود.
نظرسنجی نخ
در این روش یک نخ در حلقه تنگی قرار میگیرد که تنها زمانی خارج میشود که شرط حلقه برقرار شود. از این رو به همراه جریان دستورالعملها یا اجرای دستورالعمل یکسان check_condition() تداوم نمییابد.
مسدودسازی نخ
مسدود کردن نخ موجب میشود که نخ تا زمان برقرار شدن شرط مورد نظر به خواب برود. این روش موجب ذخیره شدن همه پردازشها و حافظه لوکال نخ روی پردازنده روی RAM یا HDD/SDD میشود و کرنل شناسه نخ و شرط را به خاطر میسپارد. زمانی که شرط در نهایت برقرار شد، کرنل اطلاعات را مجدداً از حافظه فراخوانی کرده و بر اساس زمانبندی، شروع به پردازش میکند.
اعطای نخ
روش اعطا (Yielding) آمیزهای از دو روش قبلی یعنی نظرسنجی و مسدودسازی است. زمانی که شرط مورد نظر نخ برقرار نباشد، نخ، زمان باقیمانده خود را به CPU میبخشد تا نخ دیگری پردازش شود و یا در صورتی که نخ دیگری نباشد، خودش تا آن هنگام که زمانش روی پردازنده تمام شود صبر میکند.
نخبندی
اکنون که با برخی مفاهیم ابتدایی نخها آشنا شدید، میتوانیم شروع به بررسی کاربردهای نخبندی بکنیم. چنان که پیشتر در بخش مفاهیم نخ اشاره کردیم، هرگز نباید کارهای محاسباتی را روی نخ UI اجرا کنید. به مثال زیر توجه کنید.
فرض کنید از یک برنامه مدلسازی 3 بعدی استفاده میکنید که روی یک نخ اجرا میشود. GUI، محاسبات و همه چیز روی همان یک نخ منفرد قرار دارد. شما یک شیء با 10،000،000 رأس طراحی میکنید و میخواهید آن را با جزییات ظریف رندر بگیرید. روی دکمه Render کلیک میکنید و برنامه بدون هیچ سرنخی قفل میشود. دلیل این امر آن است محاسبات همه زمان نخ را روی پردازنده اشغال کردهاند و برنامه امکان تداوم اجرای حلقه UI خود را ندارد.
یک راهحل این مشکل آن است که از یک نخ دوم برای اجرای محاسبات استفاده کنید و دادهها را در آن ذخیره کنید تا نخ اصلی (یعنی آن نخی که GUI برنامه قرار دارد) به اجرای خود ادامه بدهید تا بتوانید از برنامه استفاده کنید.
پایتون یک ماژول به این منظور ارائه کرده است که threading (+) نام دارد. ما در مثالهایی که در ادامه این راهنما ارائه میکنیم، از امکانات این ماژول بهره خواهیم گرفت.
تبدیل فوریه گسسته
تبدیل فوریه گسسته (DFT) در ویکیپدیا (+) چنین تعریف شده است:
در ریاضیات، تبدیل فوریه گسسته یک دنباله متناهی از نمونههای با فضای برابر از یک تابع را به دنبالهای با طول یکسان از نمونههای با فضای برابر از «تبدیل فوریه گسسته-زمان» (DTFT) تبدیل میکند که یک تابع مختلط-مقدار از فرکانس است.
البته شما الزامی به درک کامل این تابع ندارید، چون تنها یک نمونه برای نمایش مزیتهای نخبندی محسوب میشود. کد کامل آن را میتوانید اینجا (+) مشاهده کنید.
ابتدا shebang (+) را نصب و ایمپورتهای اسکریپت را انجام میدهیم. در این مورد تنها به ماژولهای math ،random ،threading و time نیاز داریم. ماژول Math برای تابع DFT ،random برای کمک به تولید اعداد تصادفی، threading برای مقصود اصلی ما که نمایش مزیتهای نخبندی است و time نیز برای نمایش تفاوت زمانی بین تابعهای نخبندی شده و غیر نخبندیشده ایمپورت میشوند.
در ادامه خود تابع DFT را میبینیم:
همچنین در ادامه نسخه استاندارد تابع DFT را میبینید. توجه کنید که در این نسخه یک اندیس آغاز و پایان وجود دارد. دلیل این امر آن است که هر نخ چیزی به منبع داده یکسانی دسترسی دارد و میتواند خودش را در رقابت بازنویسی کند، مگر این که به طور خاص به آن اعلام کنیم که دادهها را کجا بنویسد.

در ادامه برخی دادههای ساختگی برای تابعهای DFT ایجاد کردیم. این آرایهها همچنان باید پر باشند تا به درستی اندیس شوند و از این رو برای مقادیر outreal و outimage تعداد 128 عدد 0 تعیین میکنیم.
در نهایت تابعها را به صورت متوالی اجرا میکنیم تا توانایی هر کدام را برای محاسبه خروجی از ورودی بررسی کنیم. زمانی که این کد را روی رایانه خود اجرا کنید، نتایج ممکن است متفاوت باشد، اما ظاهر کلی آن باید چیزی مانند تصویر زیر باشد:
سخن پایانی
اینک شما موفق شدهاید، یک اسکریپت پایتون چندنخی بسازید. امیدواریم با مطالعه این مطلب، در مورد کاربرد ترد در پایتون اطلاعاتی به دست آورده باشید.