FA-TOOLS — Header Component

آموزش threading در پایتون — چند رشتگی

چند رشتگی در پایتون: نگاهی سریع

  • هدف: اجرای همزمان بخش‌هایی از یک برنامه برای بهبود پاسخگویی و بهره‌وری.
  • مفهوم کلیدی: تعداد رشته‌ها (Threads) در یک فرآیند (Process) واحد. رشته‌ها منابع حافظه را به اشتراک می‌گذارند.
  • ماژول اصلی: threading در پایتون.
  • چالش بزرگ: GIL (Global Interpreter Lock). این قفل باعث می‌شود در هر لحظه فقط یک رشته پایتون اجرا شود، حتی در سیستم‌های چند هسته‌ای.
  • کاربرد اصلی: ایده‌آل برای کارهای ورودی/خروجی (I/O-bound) مانند درخواست‌های شبکه، عملیات دیسک یا انتظار برای کاربر.
  • عدم کارایی: برای کارهای محاسباتی (CPU-bound) مثل پردازش داده‌های سنگین. در این موارد، multiprocessing گزینه بهتری است.
  • مسائل همزمان‌سازی: نیاز به استفاده از قفل‌ها (Locks)، سمافورها (Semaphores) و رویدادها (Events) برای جلوگیری از مشکلات مانند Race Condition و Deadlock.
  • نکته کلیدی: Threading در پایتون باعث اجرای موازی واقعی (parallelism) نمی‌شود، بلکه باعث اجرای همزمان (concurrency) می‌شود.

فهرست مطالب

آموزش threading در پایتون — چند رشتگی — تصویر 2

۱. مقدمه: چرا به چند رشتگی نیاز داریم؟

آموزش threading در پایتون — چند رشتگی — تصویر 3

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

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

۲. چند رشتگی (Concurrency) در مقابل چند پردازشی (Parallelism)

قبل از شیرجه زدن در جزئیات، لازم است تفاوت بین دو مفهوم مهم را درک کنیم: Concurrency (همزمانی) و Parallelism (موازی‌سازی).

  • همزمانی (Concurrency): به توانایی برنامه برای مدیریت چندین کار در یک زمان اشاره دارد. این کارها لزوماً همزمان اجرا نمی‌شوند، بلکه ممکن است پردازنده بین آن‌ها به سرعت سوئیچ کند و توهم اجرای همزمان را ایجاد کند. این شبیه به یک آشپز است که همزمان چندین غذا را می‌پزد: او در یک لحظه روی یک غذا کار می‌کند، اما بین غذاها سوئیچ می‌کند. Threading در پایتون در اکثر موارد به همزمانی منجر می‌شود.
  • موازی‌سازی (Parallelism): به معنای اجرای واقعی چندین کار در یک زمان، معمولاً روی هسته‌های پردازشی جداگانه است. این یعنی چندین کار همزمان و مستقل در حال اجرا هستند. این شبیه به چندین آشپز است که هر کدام روی یک غذای جداگانه کار می‌کنند. برای موازی‌سازی واقعی در پایتون، اغلب از ماژول multiprocessing استفاده می‌شود.

۳. قفل مفسر سراسری (GIL): محدودیت و اهمیتت آن

یکی از مهم‌ترین مفاهیمی که هنگام کار با threading در پایتون باید بدانید، GIL (Global Interpreter Lock) است. GIL یک قفل داخلی در مفسر CPython (رایج‌ترین پیاده‌سازی پایتون) است که تضمین می‌کند در هر لحظه، فقط یک رشته پایتون می‌تواند در حال اجرا باشد.

چرا GIL وجود دارد؟

GIL به دلایل تاریخی و برای ساده‌سازی مدیریت حافظه و جلوگیری از مشکلات همزمانی پیچیده در مفسر پایتون طراحی شده است. بدون GIL، مدیریت اشیاء پایتون و جلوگیری از “race conditions” در سطح مفسر بسیار دشوارتر می‌شد.

این بدان معناست که حتی اگر شما یک سیستم با چندین هسته پردازشی داشته باشید، threading در پایتون نمی‌تواند از موازی‌سازی واقعی در کارهای CPU-bound (کارهایی که عمدتاً به محاسبات پردازنده وابسته هستند) بهره‌مند شود. رشته‌ها بین یک هسته سوئیچ می‌شوند، اما هرگز به صورت همزمان روی هسته‌های مختلف اجرا نمی‌شوند.

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

۴. چه زمانی از Threading استفاده کنیم؟

با توجه به وجود GIL، مهم است که بدانید چه زمانی threading در پایتون واقعاً مفید است:

  • کارهای I/O-bound (ورودی/خروجی محور): بهترین سناریو برای threading. این شامل عملیات زیر می‌شود:
    • درخواست‌های HTTP (مثل وب‌اسکرپینگ، API calls)
    • خواندن/نوشتن از دیسک (عملیات فایل)
    • عملیات دیتابیس
    • انتظار برای ورودی کاربر
    • عملیات شبکه

    در این موارد، وقتی یک رشته منتظر I/O است، GIL آزاد می‌شود و رشته دیگری می‌تواند کارهای CPU-bound را انجام دهد یا خود نیز منتظر I/O بماند.

  • کارهای CPU-bound (محاسبات محور): threading برای این نوع کارها معمولاً مفید نیست و حتی می‌تواند به دلیل سربار تعویض زمینه (context switching) عملکرد را کاهش دهد. اگر برنامه شما عمدتاً مشغول محاسبات سنگین است (مانند پردازش تصویر، محاسبات علمی پیچیده)، باید به سراغ multiprocessing بروید که از GIL عبور می‌کند.

۵. شروع کار با ماژول threading

ماژول threading ساده‌ترین راه برای شروع کار با چند رشتگی در پایتون است.

۵.۱. ایجاد یک رشته پایه

برای ایجاد یک رشته، ابتدا یک تابع تعریف می‌کنید که می‌خواهید به صورت جداگانه اجرا شود. سپس یک شیء Thread ایجاد کرده و تابع را به عنوان آرگومان target به آن می‌دهید.


import threading
import time

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

# ایجاد رشته‌ها
thread1 = threading.Thread(target=task, args=("Thread-1",))
thread2 = threading.Thread(target=task, args=("Thread-2",))

# شروع اجرای رشته‌ها
thread1.start()
thread2.start()

# منتظر ماندن برای اتمام کار رشته‌ها
thread1.join()
thread2.join()

print("همه رشته‌ها کار خود را به پایان رساندند.")
  • threading.Thread(target=function_name, args=(arg1, arg2)): شیء رشته را ایجاد می‌کند. target تابعی است که قرار است در رشته اجرا شود و args یک تاپل از آرگومان‌های تابع است.
  • thread.start(): رشته را برای اجرا آغاز می‌کند.
  • thread.join(): برنامه اصلی را متوقف می‌کند تا رشته مورد نظر کار خود را به پایان برساند و سپس ادامه می‌دهد. این برای اطمینان از اتمام همه رشته‌ها قبل از پایان برنامه اصلی مهم است.

۵.۲. ارسال آرگومان به رشته‌ها

همانطور که در مثال بالا دیدید، آرگومان‌ها را می‌توان با استفاده از پارامتر args به تابع هدف ارسال کرد. این پارامتر باید یک تاپل (tuple) باشد.


import threading
import time

def download_file(filename, url):
    print(f"شروع دانلود {filename} از {url}...")
    time.sleep(3) # شبیه‌سازی دانلود
    print(f"دانلود {filename} به پایان رسید.")

files_to_download = [
    ("image.jpg", "http://example.com/image.jpg"),
    ("document.pdf", "http://example.com/document.pdf"),
    ("video.mp4", "http://example.com/video.mp4")
]

threads = []
for filename, url in files_to_download:
    thread = threading.Thread(target=download_file, args=(filename, url))
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print("همه فایل‌ها دانلود شدند.")

۵.۳. زیرکلاس‌سازی Thread

روش دیگر برای ایجاد رشته‌ها، ارث‌بری از کلاس threading.Thread است. این روش زمانی مفید است که نیاز به ایجاد رشته‌های پیچیده‌تر با وضعیت داخلی (internal state) یا رفتار سفارشی داشته باشید.


import threading
import time

class MyCustomThread(threading.Thread):
    def __init__(self, name, delay):
        super().__init__()
        self.name = name
        self.delay = delay
        print(f"رشته {self.name} ایجاد شد.")

    def run(self):
        # متدی که وقتی رشته شروع می‌شود، اجرا می‌شود.
        print(f"رشته {self.name}: شروع به کار برای {self.delay} ثانیه...")
        time.sleep(self.delay)
        print(f"رشته {self.name}: کار تمام شد.")

# ایجاد نمونه‌ها از کلاس سفارشی
thread1 = MyCustomThread("Custom-Thread-1", 4)
thread2 = MyCustomThread("Custom-Thread-2", 2)

# شروع رشته‌ها
thread1.start()
thread2.start()

# منتظر ماندن برای اتمام
thread1.join()
thread2.join()

print("همه رشته‌های سفارشی کار خود را به پایان رساندند.")

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

۶. همزمان‌سازی رشته‌ها

وقتی چندین رشته به داده‌های مشترک دسترسی پیدا می‌کنند و آن‌ها را تغییر می‌دهند، ممکن است مشکلاتی مانند “Race Conditions” پیش بیاید. برای جلوگیری از این مشکلات و تضمین صحت داده‌ها، نیاز به مکانیسم‌های همزمان‌سازی داریم.

۶.۱. شرایط رقابت (Race Conditions)

Race Condition زمانی رخ می‌دهد که دو یا چند رشته به یک منبع مشترک (مثل یک متغیر) دسترسی پیدا کرده و آن را تغییر می‌دهند و ترتیب این عملیات بر نتیجه نهایی تاثیر می‌گذارد. نتیجه در چنین شرایطی غیرقابل پیش‌بینی و نادرست خواهد بود.


import threading

shared_counter = 0

def increment_counter():
    global shared_counter
    for _ in range(100000):
        shared_counter += 1

threads = []
for _ in range(5):
    thread = threading.Thread(target=increment_counter)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(f"مقدار نهایی شمارنده: {shared_counter}")
# انتظار داریم 500000 باشد، اما معمولا کمتر است.

در مثال بالا، انتظار داریم مقدار نهایی shared_counter برابر با 5 * 100000 = 500000 باشد. اما به دلیل Race Condition، احتمالاً نتیجه کمتر خواهد بود. عملیات shared_counter += 1 در واقع سه عملیات است: خواندن مقدار، اضافه کردن ۱، و نوشتن مقدار جدید. اگر دو رشته همزمان مقدار را بخوانند، هر دو ۱ واحد اضافه کنند و بنویسند، تنها یک واحد به شمارنده اضافه می‌شود.

۶.۲. قفل‌ها (Lock)

قفل (Lock) ساده‌ترین مکانیسم همزمان‌سازی است. این قفل تضمین می‌کند که فقط یک رشته در یک زمان می‌تواند به یک بخش خاص از کد (بخش بحرانی یا Critical Section) دسترسی داشته باشد. سایر رشته‌ها تا زمانی که قفل آزاد نشود، منتظر می‌مانند.


import threading

shared_counter = 0
counter_lock = threading.Lock() # ایجاد یک قفل

def increment_counter_safe():
    global shared_counter
    for _ in range(100000):
        counter_lock.acquire() # قفل کردن
        try:
            shared_counter += 1
        finally:
            counter_lock.release() # آزاد کردن قفل

threads = []
for _ in range(5):
    thread = threading.Thread(target=increment_counter_safe)
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print(f"مقدار نهایی شمارنده (با قفل): {shared_counter}")
# اکنون مقدار نهایی همیشه 500000 خواهد بود.

استفاده از with statement برای قفل‌ها بهترین روش است، زیرا تضمین می‌کند که قفل حتی در صورت بروز خطا نیز آزاد شود:


def increment_counter_with_lock():
    global shared_counter
    for _ in range(100000):
        with counter_lock: # قفل کردن و آزاد کردن خودکار
            shared_counter += 1

۶.۳. قفل‌های بازگشتی (RLock)

یک Lock معمولی فقط می‌تواند یک بار توسط یک رشته گرفته شود. اگر همان رشته دوباره بخواهد آن را بگیرد، منتظر می‌ماند و منجر به بن‌بست (deadlock) می‌شود. RLock (Reentrant Lock) به یک رشته اجازه می‌دهد تا قفلی را که قبلاً گرفته، چندین بار بگیرد. این زمانی مفید است که یک تابع که قفل را گرفته، یک تابع دیگر را فراخوانی کند که آن هم نیاز به گرفتن همان قفل دارد.


import threading

r_lock = threading.RLock()

def func_a():
    with r_lock:
        print("func_a قفل را گرفت.")
        func_b()
    print("func_a قفل را آزاد کرد.")

def func_b():
    with r_lock:
        print("func_b قفل را گرفت (همان قفل func_a).")

# اگر از Lock معمولی استفاده می‌کردیم، این منجر به Deadlock می‌شد.
thread = threading.Thread(target=func_a)
thread.start()
thread.join()

۶.۴. سمافورها (Semaphore)

سمافورها برای کنترل دسترسی به منابعی استفاده می‌شوند که تعداد محدودی از آن‌ها وجود دارد. برخلاف Lock که فقط یک رشته می‌تواند آن را بگیرد، سمافور اجازه می‌دهد تا N رشته به صورت همزمان به یک منبع دسترسی داشته باشند.


import threading
import time

# حداکثر 3 رشته می‌توانند همزمان به این بخش دسترسی داشته باشند.
max_connections = threading.Semaphore(3)

def access_resource(thread_id):
    print(f"رشته {thread_id}: تلاش برای دسترسی به منابع...")
    with max_connections:
        print(f"رشته {thread_id}: به منابع دسترسی یافت.")
        time.sleep(2) # شبیه‌سازی استفاده از منابع
        print(f"رشته {thread_id}: منابع را آزاد کرد.")

threads = []
for i in range(1, 6):
    thread = threading.Thread(target=access_resource, args=(i,))
    threads.append(thread)
    thread.start()

for thread in threads:
    thread.join()

print("همه رشته‌ها کار خود را با منابع انجام دادند.")

۶.۵. رویدادها (Event)

یک Event به رشته‌ها امکان می‌دهد با ارسال سیگنال به یکدیگر، با هم ارتباط برقرار کنند. یک رشته می‌تواند منتظر یک رویداد بماند و رشته دیگر می‌تواند آن رویداد را «تنظیم» (set) کند تا رشته‌های منتظر را بیدار کند.


import threading
import time

event = threading.Event()

def waiter():
    print("رشته منتظر: منتظر سیگنال...")
    event.wait() # منتظر می‌ماند تا event.set() فراخوانی شود
    print("رشته منتظر: سیگنال دریافت شد و ادامه پیدا می‌کند.")

def signaler():
    time.sleep(3)
    print("رشته سیگنال‌دهنده: سیگنال را تنظیم می‌کند.")
    event.set() # سیگنال را تنظیم می‌کند و رشته‌های منتظر را بیدار می‌کند.

thread_waiter = threading.Thread(target=waiter)
thread_signaler = threading.Thread(target=signaler)

thread_waiter.start()
thread_signaler.start()

thread_waiter.join()
thread_signaler.join()

print("برنامه به پایان رسید.")

۶.۶. شرایط (Condition)

Condition یک ابزار همزمان‌سازی پیشرفته‌تر است که بر پایه Lock عمل می‌کند. این ابزار به رشته‌ها اجازه می‌دهد تا منتظر یک «شرط» خاص بمانند و زمانی که آن شرط برآورده شد، توسط رشته‌ای دیگر بیدار شوند. این برای الگوهای تولیدکننده-مصرف‌کننده (Producer-Consumer) بسیار مفید است.


import threading
import time
import random

buffer = []
condition = threading.Condition()
MAX_ITEMS = 5

def producer():
    for _ in range(10):
        time.sleep(random.uniform(0.1, 0.5))
        with condition:
            while len(buffer) == MAX_ITEMS:
                print("تولیدکننده: بافر پر است، منتظر مصرف‌کننده...")
                condition.wait() # منتظر می‌ماند تا بافر خالی شود
            item = random.randint(1, 100)
            buffer.append(item)
            print(f"تولیدکننده: تولید شد {item}. بافر: {buffer}")
            condition.notify() # به یک مصرف‌کننده اطلاع می‌دهد

def consumer():
    for _ in range(10):
        time.sleep(random.uniform(0.1, 0.5))
        with condition:
            while not buffer:
                print("مصرف‌کننده: بافر خالی است، منتظر تولیدکننده...")
                condition.wait() # منتظر می‌ماند تا بافر پر شود
            item = buffer.pop(0)
            print(f"مصرف‌کننده: مصرف شد {item}. بافر: {buffer}")
            condition.notify() # به یک تولیدکننده اطلاع می‌دهد

producer_thread = threading.Thread(target=producer)
consumer_thread = threading.Thread(target=consumer)

producer_thread.start()
consumer_thread.start()

producer_thread.join()
consumer_thread.join()

print("مثال تولیدکننده-مصرف‌کننده به پایان رسید.")

۷. مدیریت رشته‌ها با Thread Pools

ایجاد و مدیریت تعداد زیادی رشته به صورت دستی می‌تواند پیچیده و پرهزینه باشد. ThreadPoolExecutor از ماژول concurrent.futures یک راه ساده‌تر و کارآمدتر برای مدیریت مجموعه‌ای از رشته‌ها (Thread Pool) ارائه می‌دهد. این به شما اجازه می‌دهد تا وظایف را به یک استخر از رشته‌های موجود ارسال کنید و از سربار ایجاد/حذف رشته برای هر کار جدید جلوگیری کنید.


from concurrent.futures import ThreadPoolExecutor
import time

def process_data(data):
    print(f"پردازش داده: {data} آغاز شد.")
    time.sleep(data / 10) # شبیه‌سازی کار
    print(f"پردازش داده: {data} پایان یافت.")
    return f"نتیجه برای {data}"

data_list = [10, 5, 8, 12, 3, 7]

with ThreadPoolExecutor(max_workers=3) as executor:
    # ارسال وظایف به Thread Pool
    futures = [executor.submit(process_data, data) for data in data_list]

    # منتظر ماندن برای نتایج
    for future in futures:
        print(f"نتیجه: {future.result()}")

print("همه داده‌ها پردازش شدند.")

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

۸. بهترین شیوه‌ها برای Threading در پایتون

برای استفاده موثر و بدون مشکل از threading، رعایت نکات زیر ضروری است:

  • فقط برای کارهای I/O-bound استفاده کنید: به دلیل GIL، Threading برای کارهای CPU-bound مناسب نیست. برای این نوع کارها، multiprocessing را ترجیح دهید.
  • از حداقل تعداد رشته ممکن استفاده کنید: هر رشته سربار (overhead) خاص خود را دارد. ایجاد تعداد زیادی رشته می‌تواند به جای بهبود، عملکرد را کاهش دهد.
  • از Queue برای ارتباط استفاده کنید: ماژول queue (یا collections.deque) روش امن و ساده‌ای برای انتقال داده‌ها بین رشته‌ها ارائه می‌دهد و از Race Conditions جلوگیری می‌کند.
  • از حالت مشترک قابل تغییر (Shared Mutable State) اجتناب کنید: هر زمان که ممکن است، از دسترسی مستقیم چندین رشته به یک متغیر مشترک که تغییر می‌کند، خودداری کنید. در صورت لزوم، حتماً از مکانیسم‌های همزمان‌سازی مانند Lock استفاده کنید.
  • کوتاه نگه داشتن بخش‌های بحرانی: بخش‌هایی از کد که توسط قفل‌ها محافظت می‌شوند (Critical Sections) باید تا حد امکان کوتاه باشند تا از Bottleneck شدن و کاهش عملکرد جلوگیری شود.
  • مدیریت استثناها: مطمئن شوید که استثناهای رخ داده در رشته‌ها به درستی مدیریت می‌شوند، در غیر این صورت ممکن است رشته‌ها به صورت بی‌صدا از کار بیفتند.
  • عیب‌یابی دشوار است: مشکلات مربوط به همزمانی اغلب سخت‌ترین اشکالات برای یافتن و رفع کردن هستند، زیرا ممکن است به صورت غیرقابل پیش‌بینی و فقط تحت شرایط خاصی رخ دهند.

۹. Threading در مقابل Multiprocessing

انتخاب بین threading و multiprocessing به نوع کاری که برنامه شما انجام می‌دهد بستگی دارد. این جدول به شما کمک می‌کند تا تصمیم‌گیری بهتری داشته باشید.

ویژگی Threading (چند رشتگی) Multiprocessing (چند پردازشی)
تعریف اجرای همزمان چندین رشته در یک فرآیند. اجرای همزمان چندین فرآیند مستقل.
GIL (قفل مفسر سراسری) محدود به یک رشته در هر لحظه (برای کارهای CPU-bound). هر فرآیند دارای GIL خود است، بنابراین می‌تواند از هسته‌های چندگانه بهره ببرد.
به اشتراک‌گذاری حافظه به راحتی متغیرهای سراسری و حافظه را به اشتراک می‌گذارد (نیاز به همزمان‌سازی). هر فرآیند حافظه خود را دارد (نیاز به مکانیزم‌های خاص برای ارتباط).
ایجاد و سربار سبک‌تر و سریع‌تر (سربار کم در ایجاد و تعویض زمینه). سنگین‌تر و کندتر (سربار بیشتر در ایجاد و تعویض زمینه).
کاربرد ایده‌آل کارهای I/O-bound (مثل دانلود فایل، درخواست شبکه، انتظار برای دیتابیس). کارهای CPU-bound (مثل محاسبات سنگین، پردازش داده‌های بزرگ).
پیچیدگی همزمان‌سازی به دلیل حافظه مشترک، احتمال Race Conditions و Deadlocks بیشتر است. به دلیل حافظه جداگانه، مشکلات همزمان‌سازی کمتر است، اما ارتباط بین فرآیندها پیچیدگی خود را دارد.

۱۰. عیب‌یابی سریع و مشکلات رایج

کار با چند رشتگی می‌تواند چالش‌برانگیز باشد. در اینجا به برخی از مشکلات رایج و راه‌حل‌های آن‌ها اشاره می‌کنیم:

۱۰.۱. بن‌بست (Deadlock)

بن‌بست زمانی رخ می‌دهد که دو یا چند رشته منتظر قفلی هستند که توسط رشته دیگری گرفته شده است و هیچ یک از آن‌ها نمی‌تواند ادامه دهد زیرا منتظر آزاد شدن قفل توسط دیگری است.

مثال بن‌بست:

رشته A قفل X را می‌گیرد، سپس سعی می‌کند قفل Y را بگیرد.
رشته B قفل Y را می‌گیرد، سپس سعی می‌کند قفل X را بگیرد.

در این حالت، هیچ یک نمی‌توانند ادامه دهند.

  • راه‌حل:
    • همیشه قفل‌ها را به یک ترتیب ثابت بگیرید.
    • از timeout در متد acquire() استفاده کنید تا رشته برای همیشه منتظر نماند.
    • از RLock زمانی که یک رشته نیاز به گرفتن دوباره همان قفل را دارد، استفاده کنید.

۱۰.۲. Livelock و Starvation

  • Livelock: شبیه بن‌بست است، اما رشته‌ها به جای منتظر ماندن، دائماً حالت خود را تغییر می‌دهند و تلاش می‌کنند تا کار خود را انجام دهند اما هیچ پیشرفتی نمی‌کنند. این ممکن است زمانی رخ دهد که رشته‌ها با ادب بیش از حد سعی در جلوگیری از بن‌بست داشته باشند.
  • Starvation (قحطی): زمانی رخ می‌دهد که یک یا چند رشته هرگز نتوانند به یک منبع مشترک دسترسی پیدا کنند، زیرا رشته‌های دیگر همیشه زودتر به آن دسترسی می‌یابند.
  • راه‌حل:
    • اطمینان از انصاف در دسترسی به منابع (مثلاً با اولویت‌بندی یا استفاده از سمافورهای مدیریت‌شده).
    • استفاده از سیستم‌های صف (Queues) برای توزیع وظایف به صورت عادلانه.

۱۱. نتیجه‌گیری

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

با استفاده صحیح از قفل‌ها، سمافورها، رویدادها و مدیریت مناسب Thread Poolها، می‌توانید برنامه‌های پایتون کارآمدتر و پایداری بسازید. به یاد داشته باشید که همیشه نوع کار (I/O-bound یا CPU-bound) را در نظر بگیرید و برسی کنید که آیا threading بهترین گزینه است یا multiprocessing.

۱۲. سوالات متداول (FAQ)

۱. آیا Threading در پایتون باعث اجرای موازی واقعی می‌شود؟

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

۲. چه زمانی باید از Threading استفاده کنم؟

Threading برای کارهای I/O-bound (ورودی/خروجی محور) بسیار مفید است، مانند درخواست‌های شبکه، عملیات فایل، دسترسی به دیتابیس یا انتظار برای ورودی کاربر. در این موارد، زمانی که یک رشته منتظر I/O است، GIL آزاد می‌شود و رشته‌های دیگر می‌توانند اجرا شوند. برای کارهای CPU-bound (محاسبات محور) معمولاً multiprocessing گزینه بهتری است.

۳. چگونه می‌توانم از Race Conditions در Threading جلوگیری کنم؟

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

۴. آیا Threading در پایتون سربار دارد؟

بله، هر رشته سربار مربوط به ایجاد، مدیریت و تعویض زمینه (context switching) را دارد. در حالی که این سربار کمتر از ایجاد یک فرآیند جدید است، اگر تعداد رشته‌ها بیش از حد زیاد شود، می‌تواند عملکرد برنامه را کاهش دهد. استفاده از ThreadPoolExecutor می‌تواند به مدیریت بهتر این سربار کمک کند.

{
“@context”: “https://schema.org”,
“@type”: “FAQPage”,
“mainEntity”: [
{
“@type”: “Question”,
“name”: “آیا Threading در پایتون باعث اجرای موازی واقعی می‌شود؟”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “خیر، به دلیل وجود GIL (Global Interpreter Lock) در CPython، فقط یک رشته پایتون می‌تواند در هر لحظه اجرا شود، حتی در سیستم‌های چند هسته‌ای. Threading در پایتون بیشتر برای دستیابی به همزمانی (Concurrency) مفید است، نه موازی‌سازی (Parallelism) واقعی.”
}
},
{
“@type”: “Question”,
“name”: “چه زمانی باید از Threading استفاده کنم؟”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “Threading برای کارهای I/O-bound (ورودی/خروجی محور) بسیار مفید است، مانند درخواست‌های شبکه، عملیات فایل، دسترسی به دیتابیس یا انتظار برای ورودی کاربر. در این موارد، زمانی که یک رشته منتظر I/O است، GIL آزاد می‌شود و رشته‌های دیگر می‌توانند اجرا شوند. برای کارهای CPU-bound (محاسبات محور) معمولاً multiprocessing گزینه بهتری است.”
}
},
{
“@type”: “Question”,
“name”: “چگونه می‌توانم از Race Conditions در Threading جلوگیری کنم؟”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “برای جلوگیری از Race Conditions، باید از مکانیسم‌های همزمان‌سازی مانند قفل‌ها (threading.Lock)، سمافورها (threading.Semaphore) یا متغیرهای شرطی (threading.Condition) استفاده کنید تا اطمینان حاصل شود که در هر لحظه فقط یک رشته به بخش‌های بحرانی کد که داده‌های مشترک را تغییر می‌دهند، دسترسی دارد.”
}
},
{
“@type”: “Question”,
“name”: “آیا Threading در پایتون سربار دارد؟”,
“acceptedAnswer”: {
“@type”: “Answer”,
“text”: “بله، هر رشته سربار مربوط به ایجاد، مدیریت و تعویض زمینه (context switching) را دارد. در حالی که این سربار کمتر از ایجاد یک فرآیند جدید است، اگر تعداد رشته‌ها بیش از حد زیاد شود، می‌تواند عملکرد برنامه را کاهش دهد. استفاده از ThreadPoolExecutor می‌تواند به مدیریت بهتر این سربار کمک کند.”
}
}
]
}

Table of Contents

آخرین نوشته‌ها