مرا به خاطر بسپار

Python Threading چیست؟

بازدید: 95 آخرین به‌روزرسانی: 26 مرداد 1404

مقدمه

اگر در حوزه توسعه نرم‌افزارهای مدرن فعالیت دارید، می‌دانید که برنامه‌ها باید بتوانند چندین کار را به‌صورت کارآمد و با پاسخ‌گویی بالا انجام دهند. در این میان، پایتون زبانی قدرتمند و پرکاربرد، با قابلیت‌هایی که ارائه می‌دهد به زبانی محبوب تبدیل شده است. یکی از این قابلیت‌ها ماژول Threading است که اجرای هم‌زمان وظایف در یک فرآیند واحد را ممکن می‌سازد. این قابلیت به‌ویژه برای وظایف محدود شده توسط ورودی/خروجی (I/O-bound)، مانند درخواست‌های شبکه یا عملیات فایل، بسیار ارزشمند است. پیش از این به بررسی نخ‌ها در زبان جاوا در این مقاله پرداختیم اکنون با ما همراه باشید تا در این مقاله به بررسی دقیق‌تر Threading در پایتون بپردازیم.

نخ‌ها در پایتون

نخ (Thread) کوچک‌ترین واحد اجرایی در یک فرآیند است که امکان اجرای هم‌زمان چندین وظیفه را در یک فضای حافظه مشترک فراهم می‌کند. برخلاف فرآیندها که سنگین‌تر و مستقل هستند، نخ‌ها سبک‌وزن‌اند و برای وظایفی که از هم‌زمانی بهره می‌برند، مانند عملیات ورودی/خروجی، ایده‌آل هستند. ماژول Threading در پایتون، رابطی سطح بالا برای ایجاد و مدیریت نخ‌ها ارائه می‌دهد که بر پایه ماژول سطح پایین‌تر _thread ساخته شده است.
Threading عملکرد و پاسخ‌گویی برنامه را به دلایل زیر بهبود می‌بخشد:
  • امکان اجرای هم‌زمان وظایف محدود شده توسط ورودی/خروجی، مانند دانلود فایل یا پرس‌وجو از پایگاه داده.
  • بهبود استفاده از منابع با فعال نگه‌داشتن پردازنده در زمان انتظار برای عملیات ورودی/خروجی.
  • ساده‌سازی طراحی برنامه برای وظایفی که به‌طور طبیعی به‌صورت موازی اجرا می‌شوند، مانند مدیریت درخواست‌های متعدد کاربران در یک سرور وب.
قبل از عمیق‌تر شدن در این موضوع بهتر است با مفاهیم چندنخی آشنا شویم.

مفاهیم مورد نیاز چندنخی

  • نخ در مقابل فرآیند (Thread vs Process):
هر فرآیند،  فضای حافظه مخصوص به خود را دارد. ولی هر نخ،  فضای حافظه را با دیگر نخ‌های همان فرآیند به اشتراک می‌گذارد. به‌علاوه نخ‌ها سبک‌تر از فرآیندها هستند.
  • قفل مفسر سراسری (Global Interpreter Lock - GIL)
پایتون دارای GIL است که فقط اجازه اجرای یک نخ را در هر لحظه می‌دهد. این یعنی نخ‌ها برای کارهای مبتنی بر پردازش (CPU-bound) به صورت واقعاً موازی کار نمی‌کنند. با این حال برای کارهای مبتنی بر ورودی/خروجی (I/O-bound) که شامل زمان انتظار هستند مفیدند.
  • همزمانی (Concurrency) در مقابل موازی‌سازی (Parallelism):
در محیط‌های تک‌هسته‌ای، وقتی از چندنخی (multi-threading) استفاده می‌کنید، پردازنده بین نخ‌های مختلف سوئیچ می‌کند و به هر نخ یک برش زمانی کوتاه برای اجرا می‌دهد. به این فرآیند همزمانی  گفته می‌شود.
نکته مهم این است که همزمانی به معنی اجرای واقعاً موازی نیست! نخ‌ها به نوبت از هسته واحد استفاده می‌کنند و فقط یک توهم از موازی‌کاری ایجاد می‌شود تا برنامه پویاتر به نظر برسد.
اما قدرت واقعی چندنخی در محیط‌های چندهسته‌ای آشکار می‌شود، جایی که هر نخ می‌تواند به طور همزمان روی یک هسته پردازنده مجزا اجرا شود. این نوع اجرای موازی واقعی است که به بهبود چشمگیر عملکرد منجر می‌شود، زیرا هر نخ می‌تواند همزمان روی هسته متفاوتی اجرا شود بدون نیاز به سوئیچ کردن بین نخ‌ها.
  • چند-نخی (Multi-Threading) در مقابل چند-فرآیندی (Multi-Processing)
چندنخی به اجرای چندین نخ در یک فرآیند واحد اشاره دارد. نخ‌ها واحدهای اجرایی سبک‌وزنی هستند که در یک فضای حافظه مشترک اجرا می‌شوند. این ویژگی باعث می‌شود نخ‌ها بتوانند به‌راحتی داده‌ها را با یکدیگر به اشتراک بگذارند و ارتباط بین آن‌ها سریع‌تر و ساده‌تر باشد. ماژول Threading در پایتون ابزارهای لازم برای ایجاد و مدیریت نخ‌ها را فراهم می‌کند.
چندپردازشی شامل اجرای چندین فرآیند به‌صورت هم‌زمان است. برخلاف نخ‌ها، هر فرآیند فضای حافظه اختصاصی خود را دارد و کاملاً مستقل از سایر فرآیندها عمل می‌کند. ماژول Multiprocessing در پایتون امکان ایجاد و مدیریت فرآیندها را فراهم می‌کند.
  • چندنخی در مقابل برنامه‌نویسی ناهمگام (Asynchronous Programming)
برنامه‌نویسی ناهمگام روشی برای نوشتن کدهای رویدادمحور و غیرمسدودکننده است. در یک برنامه ناهمگام، وظایف زمان‌بر (مانند وظایف ورودی/خروجی) در پس‌زمینه انجام می‌شوند، بدون اینکه نخ اصلی را مسدود کنند. این واگذاری وظایف از طریق callbackها، promiseها یا سایر روش‌های ناهمگام انجام می‌شود.
برنامه‌نویسی ناهمگام برای سناریوهای محدود شده توسط ورودی/خروجی (I/O-bound)، مانند استخراج داده وب (Web Scraping)، درخواست‌های شبکه یا پرس‌وجوهای پایگاه داده، بسیار مناسب است. این روش از مسدود شدن جلوگیری می‌کند و استفاده از یک نخ واحد را به حداکثر می‌رساند.

استفاده از نخ‌ها

بیایید با یک مثال ساده از ایجاد و مدیریت نخ‌ها شروع کنیم که نحوه کار نخ‌ها را نشان می‌دهد.

این مثال یک کلاس نخ ایجاد می‌کند که نام نخ را پس از تأخیر تصادفی چاپ می‌کند:

در این مثال ما ماژول random پایتون، ماژول time و کلاس Thread از ماژول threading را وارد می‌کنیم. سپس کلاس Thread را زیرکلاس‌گیری کرده و متد __init__ آن را بازنویسی می‌کنیم تا یک آرگومان با برچسب nae بپذیرد.

برای شروع یک نخ، باید متد start آن را فراخوانی کنید. وقتی یک نخ را شروع می‌کنید، پایتون به‌طور خودکار متد run نخ را اجرا می‌کند. ما متد run را بازنویسی کرده‌ایم تا نخ یک مقدار تصادفی زمان برای وقفه انتخاب کند.

تابع random.randint در اینجا باعث می‌شود پایتون به‌طور تصادفی عددی بین ۳ تا ۱۵ را انتخاب کند. سپس نخ را به همان تعداد ثانیه که انتخاب شده، به خواب می‌بریم تا شبیه‌سازی کنیم که نخ در حال انجام کاری است. در نهایت، نام نخ را چاپ می‌کنیم تا کاربر بداند که نخ کار خود را به پایان رسانده است.

تابع create_threads پنج نخ ایجاد می‌کند و به هرکدام یک نام منحصربه‌فرد می‌دهد.
import random
import time
from threading import Thread

class MyThread(Thread):
    def __init__(self, name):
        Thread.__init__(self)
        self.name = name

    def run(self):
        amount = random.randint(3, 15)
        time.sleep(amount)
        print(f"Thread {self.name} is running")

def create_threads():
    for i in range(5):
        name = f"Thread #{i+1}"
        my_thread = MyThread(name)
        my_thread.start()

if __name__ == "__main__":
    create_threads()
اگر این کد را اجرا کنید، باید چیزی مشابه نمونه زیر ببینید. خروجی ممکن است به این صورت باشد (ترتیب به دلیل تأخیرهای تصادفی متفاوت است):
Thread #2 is running
Thread #1 is running
Thread #3 is running
Thread #4 is running
Thread #5 is running
مثال کاربردی: دانلود فایل با چندنخی
برای یک مورد کاربردی‌تر، کلاسی برای دانلود فایل از اینترنت با استفاده از فرم‌های PDF سایت IRS ایجاد می‌کنیم:
import os
import urllib.request
from threading import Thread

class DownloadThread(Thread):
    def __init__(self, url, name):
        Thread.__init__(self)
        self.name = name
        self.url = url

    def run(self):
        handle = urllib.request.urlopen(self.url)
        fname = os.path.basename(self.url)
        with open(fname, "wb") as f_handler:
            while True:
                chunk = handle.read(1024)
                if not chunk:
                    break
                f_handler.write(chunk)
        print(f"{self.name} has finished downloading {self.url}!")

def main(urls):
    for item, url in enumerate(urls):
        name = f"Thread {item+1}"
        thread = DownloadThread(url, name)
        thread.start()

if __name__ == "__main__":
    urls = [
        "http://www.irs.gov/pub/irs-pdf/f1040.pdf",
        "http://www.irs.gov/pub/irs-pdf/f1040a.pdf",
        "http://www.irs.gov/pub/irs-pdf/f1040ez.pdf",
        "http://www.irs.gov/pub/irs-pdf/f1040es.pdf",
        "http://www.irs.gov/pub/irs-pdf/f1040sb.pdf"
    ]
    main(urls)

ماژول os برای مدیریت فایل و urllib.request برای دانلود استفاده می‌شود.
کلاس DownloadThread فایل را در قطعات 1 کیلوبایتی دانلود و ذخیره می‌کند.
تابع main برای هر URL یک نخ ایجاد و اجرا می‌کند.
خروجی نشان می‌دهد هر نخ چه زمانی دانلود فایل را به پایان می‌رساند:

Thread 1 has finished downloading http://www.irs.gov/pub/irs-pdf/f1040.pdf!
Thread 2 has finished downloading http://www.irs.gov/pub/irs-pdf/f1040a.pdf!

همگام‌سازی نخ‌ها

هنگامی که نخ‌ها به منابع مشترک دسترسی دارند، همگام‌سازی برای جلوگیری از شرایط رقابتی (نتایج غیرقابل پیش‌بینی به دلیل دسترسی هم‌زمان) و بن‌بست‌ها (توقف نخ‌ها در انتظار یکدیگر) ضروری است. ماژول threading ابزارهای همگام‌سازی متعددی ارائه می‌دهد.
قفل‌ها تضمین می‌کنند که تنها یک نخ در هر لحظه به بخش بحرانی کد دسترسی داشته باشد. در مثال زیر یک شیء Lock برای محافظت از counter ایجاد می‌شود. عبارت with lock قفل را به‌طور خودکار به دست آورده و آزاد می‌کند.
بدون قفل، افزایش هم‌زمان counter ممکن است به نتایج نادرست منجر شود.
import threading

counter = 0
lock = threading.Lock()

def increment():
    global counter
    for _ in range(100000):
        with lock:
            counter += 1

threads = [threading.Thread(target=increment) for _ in range(2)]
for thread in threads:
    thread.start()
for thread in threads:
    thread.join()

print(f"Final counter value: {counter}")
ابزارهای دیگری نیز برای همگام‌سازی وجود دارند. برای مثال:
RLock: قفلی قابل بازپس‌گیری که به یک نخ اجازه می‌دهد چندین بار آن را به دست آورد.
Semaphore: محدودکننده تعداد نخ‌های مجاز برای دسترسی به یک منبع (مثلاً Semaphore(3) برای 3 نخ همزمان).
Condition: ایجاد وضعیت انتظار برای نخ‌ها در صورت بروز شرایط خاصی  با استفاده از wait() و notify().
Queue: صفی ایمن برای ارتباط بین نخ‌ها (در ادامه توضیح داده شده است).

استفاده از صف‌ها برای ارتباط ایمن بین نخ‌ها

ماژول queue کلاس Queue را ارائه می‌دهد که برای هماهنگی وظایف بین نخ‌ها ایمن است و از انواع روش‌های FIFO، LIFO و اولویت‌دار پشتیبانی می‌کند. مثال زیر یک دانلودکننده فایل با استفاده از صف را نشان می‌دهد در این کد یک Queue برای نگهداری URL‌ها ایجاد می‌شود. پنج نخ پس‌زمینه (daemon) ایجاد شده و URL‌ها را از صف می‌گیرند. متدهای put، get و task_done برای مدیریت وظایف استفاده می‌شوند. متد queue.join منتظر تکمیل تمام وظایف می‌ماند.

import os
import threading
import urllib.request
from queue import Queue

class Downloader(threading.Thread):
    def __init__(self, queue):
        threading.Thread.__init__(self)
        self.queue = queue

    def run(self):
        while True:
            url = self.queue.get()
            self.download_file(url)
            self.queue.task_done()

    def download_file(self, url):
        handle = urllib.request.urlopen(url)
        fname = os.path.basename(url)
        with open(fname, "wb") as f:
            while True:
                chunk = handle.read(1024)
                if not chunk:
                    break
                f.write(chunk)
        print(f"Finished downloading {url}")

def main(urls):
    queue = Queue()
    for i in range(5):
        t = Downloader(queue)
        t.setDaemon(True)
        t.start()
    for url in urls:
        queue.put(url)
    queue.join()

if __name__ == "__main__":
    urls = [
        "http://www.irs.gov/pub/irs-pdf/f1040.pdf",
        "http://www.irs.gov/pub/irs-pdf/f1040a.pdf",
        "http://www.irs.gov/pub/irs-pdf/f1040ez.pdf",
        "http://www.irs.gov/pub/irs-pdf/f1040es.pdf",
        "http://www.irs.gov/pub/irs-pdf/f1040sb.pdf"
    ]
    main(urls)

استخر نخ‌ها

ماژول concurrent.futures کلاس ThreadPoolExecutor را برای مدیریت استخر نخ‌ها ارائه می‌دهد که سربار ایجاد نخ را کاهش می‌دهد:

from concurrent.futures import ThreadPoolExecutor
import time

def perform_task(task_id):
    print(f"Task {task_id} started")
    time.sleep(2)
    result = task_id * 2
    print(f"Task {task_id} completed with result: {result}")
    return result

with ThreadPoolExecutor(max_workers=3) as executor:
    tasks = [1, 2, 3, 4, 5]
    results = executor.map(perform_task, tasks)
    for result in results:
        print(f"Result: {result}")
ThreadPoolExecutor یک استخر با 3 نخ کارگر ایجاد می‌کند.
متد map وظایف را بین نخ‌ها توزیع کرده و نتایج را به ترتیب بازمی‌گرداند.

جمع‌بندی

چندنخی در پایتون ابزاری قدرتمند برای دستیابی به هم‌زمانی در برنامه‌های I/O-bound است. با درک نحوه ایجاد نخ‌ها، همگام‌سازی منابع مشترک، و استفاده از ابزارهایی مانند صف‌ها می‌توانید برنامه‌هایی پاسخ‌گو و کارآمد بسازید. اگرچه GIL چندنخی را برای وظایف CPU-bound محدود می‌کند، اما برای وظایف شامل انتظار، مانند عملیات شبکه یا فایل، ایده‌آل است. با مثال‌ها و دانش ارائه‌شده، آماده هستید تا چندنخی را به‌طور مؤثر در پروژه‌های خود پیاده‌سازی کنید.

سوالات متداول

  1. نخ‌های پس‌زمینه (Daemon) چه کاربردی دارند؟
نخ‌های پس‌زمینه برای وظایف پس‌زمینه مناسب‌اند و با خروج برنامه اصلی خاتمه می‌یابند.
  1. چگونه از شرایط رقابتی در چندنخی جلوگیری کنیم؟
از قفل‌ها، صف‌ها یا سایر ابزارهای همگام‌سازی برای محافظت از منابع مشترک استفاده کنید.
  1. ThreadPoolExecutor چه مزیتی دارد؟
     مدیریت نخ‌ها را ساده کرده و سربار ایجاد نخ را کاهش می‌دهد.
تا چه حد این مطلب برای شما مفید بود؟
بر اساس رای 0 نفر

اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.

ثبت نظر

نظر دادن