چندنخی (Multithreading) در پایتون؛ آموزش کامل با مثال

نمایش کد پایتون و نمودار اجرای هم‌زمان نخ‌ها درون یک فرایند روی مانیتور
آنچه در این مطلب می‌خوانید

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

مدیریت اجرای هم‌زمان وظایف از مباحث کاربردی آموزش پایتون است و مستقیماً بر سرعت پاسخ‌دهی نرم‌افزار تأثیر می‌گذارد. ماژول‌های توکار threading و concurrent.futures ابزارهای ساخت و مدیریت نخ‌ها را در اختیار برنامه‌نویس می‌گذارند. در این مقاله، نحوه کار نخ‌ها در حافظه، پیاده‌سازی گام‌به‌گام کدها، جلوگیری از تداخل داده‌ها با Lock، محدودیت قفل مفسر سراسری (GIL) و قابلیت جدید نخ‌بندی آزاد در نسخه‌های جدید پایتون را بررسی می‌کنیم.

چندنخی در پایتون چیست و چگونه کار می‌کند؟

برای درک عملکرد چندنخی در پایتون، باید رابطه فرایند و نخ در آموزش سیستم عامل و شیوه تخصیص حافظه و منابع پردازنده به آن‌ها را بشناسید. پایتون با این روش، فضای داده و منابع محاسباتی را میان نخ اصلی برنامه و نخ‌های فرعی به اشتراک می‌گذارد تا نخ‌ها بدون از دست رفتن اطلاعات با یکدیگر ارتباط داشته باشند.

تفاوت فرایند (Process) و نخ (Thread)

در مباحث پایه علوم کامپیوتر و آموزش برنامه نویسی، هر برنامه در حال اجرا، مانند یک مرورگر وب، «فرایند» (Process) نام دارد. سیستم‌عامل هر فرایند را مستقل مدیریت می‌کند. «نخ» (Thread) واحد کوچک‌تری درون یک فرایند است. هر فرایند دست‌کم یک نخ دارد و می‌تواند چندین نخ را هم‌زمان درون خود اجرا کند.

دیاگرام سه‌بعدی از یک فرایند با فضای حافظه مشترک و سه نخ با پشته‌های مجزا

اجزای فرایند و نخ در حافظه (RAM) این تفاوت‌ها را دارند:

  • فرایند (Process) شامل کد برنامه (دستورالعمل‌ها)، داده‌ها (متغیرها، بافرها و فضای کاری) و زمینه اجرا (وضعیت فعلی فرایند) است.
  • نخ (Thread) واحد کوچک‌تری است که به فرایند اصلی متصل است.
  • نخ‌های درون یک فرایند، فضای حافظه (Memory space) را به اشتراک می‌گذارند و به متغیرهای مشترک دسترسی دارند؛ اما داده‌های محلی و پشته (Stack) هر نخ مخصوص خود آن است.
آموزش برنامه‌نویسی شی‌ءگرا در پایتون
آموزش برنامه‌نویسی شی‌ءگرا در پایتون
1,199,000 599,500 تومان
50%

نحوه اجرای هم‌زمان نخ‌ها در پردازنده

فرایند تک‌نخی در هر لحظه یک وظیفه را اجرا می‌کند. فرایند چندنخی می‌تواند چندین وظیفه را هم‌زمان پیش ببرد. در پردازنده‌های تک‌هسته‌ای، جابه‌جایی سریع و مکرر میان نخ‌ها هم‌زمانی را ممکن می‌کند؛ به همین دلیل، به نظر می‌رسد نخ‌ها به‌صورت موازی در حال اجرا هستند.

بسته به نوع برنامه، نخ‌ها می‌توانند منتظر بمانند تا شرایط مشخصی برای ادامه کارشان فراهم شود.

آموزش پیاده‌سازی چندنخی در پایتون با ماژول threading

ماژول استاندارد و سطح بالای threading در پایتون برای ساخت و مدیریت نخ‌هاست. این ماژول رابط برنامه‌نویسی (API) ساده‌ای دارد که با آن می‌توانید جریان‌های مستقل اجرا را تعریف کنید.

مراحل ساخت و اجرای نخ (متدهای start و join)

برای ساخت و اجرای یک نخ جدید در پایتون، چهار مرحله اصلی را طی می‌کنید:

اسکرین‌شات محیط کدنویسی پایتون شامل کد متدهای start و join و خروجی ترمینال
  1. ابتدا باید با استفاده از دستور import در پایتون، ماژول استاندارد threading را به برنامه اضافه کنید.
  2. تابع هدف را تعریف می‌کنید. هر نخ می‌تواند برای اجرا یک شیء قابل فراخوانی، مانند تابع، داشته باشد. شناخت ساختار توابع در پایتون در این مرحله مفید است، چون دستورالعمل‌های نخ درون این تابع نوشته می‌شوند.
  3. بر پایه مفاهیم شی‌گرایی در پایتون، یک نمونه از کلاس threading.Thread می‌سازید. نام تابع هدف به پارامتر target و آرگومان‌های ورودی آن به شکل تاپل یا لیست به پارامتر args داده می‌شوند.
  4. با فراخوانی متد .start()، اجرای نخ آغاز می‌شود. متد .join() نیز نخ فراخواننده خود را مسدود می‌کند و منتظر نگه می‌دارد تا اجرای نخ فرعی به پایان برسد.

در مثال زیر، یک عملیات زمان‌بر، مانند دانلود فایل، با تابع time.sleep(۲) شبیه‌سازی می‌شود:

import threading
import time

def task(name):
    print(f"Thread {name}: starting...")
    time.sleep(2)  # شبیه‌سازی یک عملیات I/O مانند دانلود
    print(f"Thread {name}: finishing.")

# ساخت و شروع نخ
my_thread = threading.Thread(target=task, args=("A",))
my_thread.start()

print("Main thread: doing other work.")

# انتظار برای اتمام نخ فرعی پیش از خروج از برنامه
my_thread.join()
print("Main thread: all done.")

پس از my_thread.start()، نخ اصلی منتظر پایان کار نخ فرعی نمی‌ماند و دستور چاپ پیام “Main thread: doing other work.” را اجرا می‌کند. وقتی به خط my_thread.join() می‌رسد، منتظر می‌ماند تا کار نخ A تمام شود.

اجرای هم‌زمان چند تابع با آرگومان‌های ورودی

می‌توانید چند تابع متفاوت را هم‌زمان روی نخ‌های جداگانه اجرا کنید. در کد زیر، دو تابع برای محاسبه مجذور (square) و مکعب (cube) یک عدد تعریف شده‌اند و اجرای آن‌ها به دو نخ t1 و t2 سپرده شده است:

import threading
import time

def square(num):
    print(f"Square: {num * num}")
    time.sleep(1)

def cube(num):
    print(f"Cube: {num * num * num}")
    time.sleep(1)

t1 = threading.Thread(target=square, args=(4,))
t2 = threading.Thread(target=cube, args=(4,))

t1.start()
t2.start()

t1.join()
t2.join()

print("Done!")

خروجی این کد در کنسول می‌تواند به شکل زیر باشد:

Square: 16
Cube: 64
Done!

دو نخ t1 و t2 هم‌زمان اجرا می‌شوند، بنابراین ترتیب اجرای آن‌ها از پیش ثابت نیست. ممکن است در اجراهای مختلف، پیام یکی زودتر از دیگری چاپ شود؛ اما به دلیل استفاده از join()، عبارت Done! پس از پایان هر دو نخ چاپ خواهد شد.

مدیریت بهینه نخ‌ها با ThreadPoolExecutor

وقتی تعداد وظایف کوچک زیاد است، ساخت و مدیریت دستی نخ‌ها می‌تواند زمان‌بر باشد. کلاس ThreadPoolExecutor در ماژول concurrent.futures رابطی در سطح بالاتر دارد. با این رابط می‌توانید وظایف را بدون مسدود کردن نخ فراخواننده به نخ‌های پس‌زمینه بفرستید و هر زمان که نیاز دارید، نتایج آن‌ها را دریافت کنید.

استفاده از متدهای submit و map در استخر نخ

این کلاس امکان مدیریت تعداد نخ‌های کارگر را دارد. برای فرستادن وظایف به استخر نخ، دو روش متداول وجود دارد.

پنجره ویرایشگر کد پایتون با مثال ThreadPoolExecutor و خروجی مجذور اعداد در کنسول

در روش اول، متد submit وظایف را تک‌به‌تک به استخر نخ می‌فرستد:

from concurrent.futures import ThreadPoolExecutor

def worker(task):
    print(f"Task {task} running")

# ایجاد استخر نخ با ۲ نخ کارگر
with ThreadPoolExecutor(max_workers=2) as executor:
    # ارسال دو وظیفه برای اجرای هم‌زمان
    executor.submit(worker, 1)
    executor.submit(worker, 2)

این کد استخری با ۲ نخ کارگر می‌سازد. هر وظیفه پیام مربوط به خود، یعنی Task ۱ running و Task ۲ running، را چاپ می‌کند.

در روش دوم، متد map یک تابع مشخص را روی مجموعه‌ای از داده‌ها، مانند یک لیست، اعمال می‌کند:

from concurrent.futures import ThreadPoolExecutor

def square_number(n):
    return n * n

numbers = [1, 2, 3, 4, 5]

with ThreadPoolExecutor() as executor:
    results = executor.map(square_number, numbers)

print(list(results))  # Output: [1, 4, 9, 16, 25]

در این روش هم می‌توانید در صورت نیاز، پارامتر max_workers را هنگام فراخوانی ThreadPoolExecutor(max_workers=۲) مشخص کنید.

جلوگیری از شرایط رقابتی (Race Condition) با Lock

نخ‌های یک فرایند فضای حافظه را به اشتراک می‌گذارند. به همین دلیل، اگر چند نخ هم‌زمان برای خواندن و تغییر یک متغیر مشترک تلاش کنند، ممکن است داده‌ها دچار تداخل شوند.

نمایش مفهومی دو نخ در حال دسترسی به یک منبع مشترک با کنترل دروازه قفل

برای حل این مشکل و همگام‌سازی نخ‌ها، از شیء threading.Lock() استفاده می‌شود. قفل اجازه می‌دهد در هر لحظه فقط یک نخ بخش محافظت‌شده کد را اجرا کند و متغیر مشترک را تغییر دهد.

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}")

در این کد، هر یک از دو نخ ۱۰۰,۰۰۰ بار متغیر سراسری counter را یک واحد افزایش می‌دهد. بلوک مدیریت زمینه with lock: معادل فراخوانی acquire() هنگام ورود به بلوک و release() هنگام خروج از آن است. در نتیجه، مقدار نهایی شمارنده بدون تداخل، عدد ۲۰۰,۰۰۰ خواهد بود.

چه زمانی از چندنخی در پایتون استفاده کنیم؟ (۳ سناریوی کاربردی)

مالتی‌تردینگ در پایتون زمانی بیشترین کارایی را دارد که بخواهید برنامه را به چند زیروظیفه (Sub-Tasks) تقسیم کنید و این وظایف برای منابع خارجی زمان انتظار داشته باشند (I/O-bound). بهتر است زمانی از چندنخی استفاده کنید که نخ‌ها بتوانند مستقل از یکدیگر پیش بروند. سه سناریوی رایج را با کد بررسی می‌کنیم.

۱. مدیریت هم‌زمان درخواست‌های کاربران در وب

در آموزش برنامه نویسی وب، ممکن است چندین کاربر هم‌زمان درخواست بفرستند. چندنخی کمک می‌کند یک درخواست کند، پردازش درخواست‌های سایر کاربران را متوقف نکند:

import threading

def handle_request(user_id):
    print(f"Processing request for user {user_id}")

# ساخت چند نخ برای پردازش درخواست‌ها
threads = [threading.Thread(target=handle_request, args=(i,)) for i in range(1, 4)]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

۲. دانلود هم‌زمان چند فایل

می‌توانید به ازای هر فایل یک نخ بسازید و همه فایل‌ها را هم‌زمان دانلود کنید، بدون اینکه منتظر پایان دانلود یک فایل بمانید و بعد سراغ فایل بعدی بروید:

import threading
import time

def download_file(file_name):
    print(f"Downloading {file_name}...")
    time.sleep(2)  # شبیه‌سازی زمان دانلود
    print(f"{file_name} downloaded.")

files = ["file1.txt", "file2.txt", "file3.txt"]
threads = [threading.Thread(target=download_file, args=(file,)) for file in files]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

۳. جلوگیری از قفل شدن رابط گرافیکی (Tkinter)

در برنامه‌های دارای رابط گرافیکی (GUI)، اجرای عملیات زمان‌بر روی نخ اصلی می‌تواند پاسخ‌گویی پنجره برنامه را کند یا متوقف کند. هنگام کار با کتابخانه tkinter در پایتون، می‌توانید این پردازش‌ها را به یک نخ جداگانه در پس‌زمینه منتقل کنید تا رابط کاربری فعال بماند و همچنان پاسخ دهد:

پنجره کوچک رابط گرافیکی Tkinter با پیام Data Loaded در کنار ویرایشگر کد پایتون
import threading
import time
import tkinter as tk

def load_data():
    time.sleep(3)  # شبیه‌سازی یک عملیات کند
    label.config(text="Data Loaded!")

root = tk.Tk()
root.geometry("200x100")

label = tk.Label(root, text="Loading...")
label.pack()

# اجرای عملیات در یک نخ جداگانه
thread = threading.Thread(target=load_data)
thread.start()

root.mainloop()

در این مثال، پنجره برنامه با متن “Loading…” باز می‌شود. پس از ۳ ثانیه، متن برچسب توسط نخ پس‌زمینه به “Data Loaded!” تغییر می‌کند.

مقایسه چندنخی (Multithreading) و چندپردازشی (Multiprocessing)

انتخاب میان چندنخی و چندپردازشی به نوع وظیفه برنامه، یعنی وابستگی آن به ورودی/خروجی یا پردازنده، بستگی دارد. در چندپردازشی، از فرایندهای مجزا برای دور زدن قفل مفسر سراسری (GIL) و استفاده بهتر از منابع محاسباتی سیستم‌های چندهسته‌ای استفاده می‌شود. جدول زیر تفاوت‌های اصلی این دو روش را نشان می‌دهد:

ویژگیچندنخی (Multithreading)چندپردازشی (Multiprocessing)
تعریفاجرای هم‌زمان چندین نخ درون یک فرایند واحد با اشتراک حافظهاستفاده از فرایندهای مجزا
کارکرد در پایتونایجاد هم‌زمانی (Concurrency)؛ مناسب برای وظایف دارای انتظار (I/O-bound)مناسب برای استفاده بهتر از منابع محاسباتی در وظایف پردازشی (CPU-bound)
روش کار در سخت‌افزاراجرای نخ‌ها درون یک فرایند واحداستفاده از منابع محاسباتی ماشین‌های چندهسته‌ای از طریق فرایندهای جداگانه
پردازش توسط مفسر پایتونمحدود شده توسط قفل مفسر سراسری (GIL) در ساخت پیش‌فرضدور زدن محدودیت قفل مفسر سراسری (GIL) با استفاده از فرایندهای مجزا

محدودیت قفل مفسر سراسری (GIL) و چه زمانی نباید از چندنخی استفاده کرد؟

در اجرای استاندارد پایتون (CPython)، «قفل مفسر سراسری» (Global Interpreter Lock یا GIL) مانع اجرای هم‌زمان بایت‌کدهای پایتون توسط چندین نخ در یک فرایند می‌شود. به دلیل وجود GIL، در هر لحظه فقط یک نخ کد پایتون را اجرا می‌کند؛ هرچند برخی کتابخانه‌های متمرکز بر کارایی ممکن است بر این محدودیت غلبه کنند.

مقایسه بصری عبور تک‌به‌تک نخ‌ها از گلوگاه در برابر پردازش موازی در چند هسته پردازنده

بنابراین، GIL مزایای عملکردی چندنخی را برای وظایف سنگین پردازشی (CPU-bound) محدود می‌کند. اگر هدف برنامه استفاده بهتر از منابع محاسباتی ماشین‌های چندهسته‌ای در پردازش‌های سنگین است، توصیه می‌شود از ماژول multiprocessing یا concurrent.futures.ProcessPoolExecutor استفاده کنید.

قابلیت نخ‌بندی آزاد (Free Threading) از پایتون ۳.۱۳ به بعد

از نسخه ۳.۱۳ به بعد، بر اساس پیشنهاد PEP ۷۰۳، ساخت‌های «نخ‌بندی آزاد» (Free-threaded builds) می‌توانند قفل سراسری مفسر (GIL) را غیرفعال کنند. با غیرفعال شدن این قفل، اجرای موازی واقعی نخ‌ها ممکن می‌شود؛ اما این ویژگی به‌صورت پیش‌فرض فعال نیست.

نکات مربوط به مدیریت زمینه و ایمنی پیمایش‌گرها در نسخه‌های جدید عبارت‌اند از:

  • در کلاس Thread، پارامتر context که در نسخه ۳.۱۴ اضافه شده است، مشخص می‌کند نخ با چه زمینه‌ای (Context) شروع به کار کند. مقدار پیش‌فرض آن None است؛ یعنی پرچم sys.flags.thread_inherit_context این رفتار را کنترل می‌کند. مقدار پیش‌فرض این پرچم در ساخت‌های نخ‌آزاد (Free-threaded builds)، True است و نخ با یک کپی از زمینه فراخواننده start() شروع می‌شود. در سایر ساخت‌ها، مقدار آن False است و نخ با زمینه خالی شروع می‌شود.
  • پیمایش‌گرهای پایتون (Iterators) به‌صورت پیش‌فرض از دسترسی هم‌زمان پشتیبانی نمی‌کنند و بیشتر آن‌ها برای دسترسی هم‌زمان از چندین نخ تضمین خاصی ندارند. برای نمونه، پیمایش‌گرهای مولد یا Generator در صورت فراخوانی هم‌زمان متدهایشان خطای ValueError ایجاد می‌کنند. از پایتون ۳.۱۵، ابزارهایی مانند threading.serialize_iterator، دکوراتور در پایتون به نام threading.synchronized_iterator و تابع threading.concurrent_tee برای همگام‌سازی و اشتراک‌گذاری ایمن پیمایش‌گرها بین چندین نخ به ماژول threading اضافه شده‌اند.

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

آموزش برنامه‌نویسی با پایتون (پیشرفته)
آموزش برنامه‌نویسی با پایتون (پیشرفته)
2,399,000 1,319,450 تومان
45%

مروری بر سایر ماژول‌ها و ابزارهای چندنخی در پایتون

پایتون در کنار کلاس‌های اصلی، ابزارها و اشیای مکملی هم دارد که برنامه‌نویسان با آن‌ها می‌توانند نخ‌ها را دقیق‌تر کنترل کنند:

  • ماژول threading رابط‌های سطح بالای نخ‌بندی را بر پایه ماژول سطح پایین‌تر _thread می‌سازد. همچنین کمینه‌ترین قفل همگام‌سازی در پایتون (Lock) به‌طور مستقیم توسط ماژول افزونه _thread پیاده‌سازی می‌شود.
  • ماژول queue رابطی ایمن در برابر نخ (Thread-safe) برای تبادل داده میان نخ‌های در حال اجرا فراهم می‌کند.
  • ماژول threading علاوه بر Lock، اشیای تخصصی دیگری برای همگام‌سازی و زمان‌بندی دارد: RLock قفل بازگشتی است که یک نخ واحد می‌تواند چندین بار آن را اخذ کند؛ Semaphore و BoundedSemaphore سمافور هستند؛ Barrier مانع است؛ Condition متغیر شرطی است که به یک یا چند نخ اجازه می‌دهد منتظر اعلان نخ دیگر بمانند؛ Event رویداد و پرچم داخلی بین نخ‌ها را مدیریت می‌کند؛ و Timer زیرکلاسی از Thread است که یک عملیات را تنها پس از گذشت زمان مشخص اجرا می‌کند.

برای عملیات دارای انتظار (I/O-bound)، استفاده از ماژول threading یا ThreadPoolExecutor همراه با سازوکارهای همگام‌سازی مانند Lock و queue راهکار مناسبی در پایتون است. در پردازش‌های سنگین محاسباتی (CPU-bound) نیز می‌توان از multiprocessing، ProcessPoolExecutor یا قابلیت نخ‌بندی آزاد در پایتون ۳.۱۳ به بعد استفاده کرد.

https://maktabkhooneh.org/mag/python-multithreading/
تصویر مهناز محمدی

مهناز محمدی

0 0 امتیازها
امتیاز دهی به محتوا
مشترک شوید
اطلاع از
guest
0 دیدگاه
قدیمی ترین
جدید ترین دیدگاه با تعداد رای زیاد
مقالات بیشتر
نمایش همزمان کدهای پایتون در ویرایشگر و خروجی وب‌اپلیکیشن در مرورگر

ساخت وب‌اپلیکیشن با پایتون؛ آموزش عملی Flask و مقایسه با Django

نمایش کدهای تبدیل نوع داده با توابع int و float و str در محیط ویرایشگر پایتون

آموزش تبدیل داده در پایتون؛ راهنمای تبدیل انواع داده (Type Casting)

محیط کار برنامه‌نویسی شامل کدهای HTML، اسکریپت پایتون BeautifulSoup و خروجی جدول CSV در یک مانیتور

آموزش کامل BeautifulSoup؛ راهنمای گام‌به‌گام وب‌اسکرپینگ با پایتون

مانیتور حاوی کد پایتون برای کتابخانه datetime در کنار تقویم و ساعت رومیزی

کتابخانه datetime در پایتون؛ آموزش کامل کار با تاریخ و زمان

اجرای دستور pip install numpy در پنجره ترمینال با تم تیره و نمایش پیام موفقیت نصب

نصب NumPy در پایتون؛ آموزش گام‌به‌گام در ویندوز، مک و لینوکس

نمایش کد پایتون برای تشخیص اعداد اول ۲، ۳، ۵، ۷ و ۲۳ از عدد مرکب ۲۴ روی مانیتور

تشخیص اعداد اول در پایتون؛ کد و الگوریتم‌های بهینه