کتابخانه threading در پایتون
ماژول threading در پایتون ابزارهایی برای اجرای همزمان چند ریسمان (Thread) در یک فرآیند واحد فراهم میکند. این ماژول برای مشکلاتی که وابسته به ورودی/خروجی (I/O) یا مسائلی با انتظار و همزمانی سبک هستند مناسب است. در این مقاله به مفاهیم پایه، نمونههای عملی، و نکات پیشرفته میپردازیم تا بتوانید از threading بهصورت صحیح و مؤثر استفاده کنید.
چرا از threading استفاده کنیم؟
چندریسمانی به برنامه اجازه میدهد وظایف مختلف را همزمان اجرا کند؛ مثلاً خواندن فایل، دریافت داده از شبکه و بهروزرسانی رابط کاربری. اگر برنامه شما منتظر I/O است، threading میتواند زمان پاسخدهی را بهبود دهد. اما برای محاسبات سنگین CPU باید به محدودیت GIL توجه کنید (در پایتون استاندارد GIL اجرای همزمان واقعی پردازشی را محدود میکند).
مفاهیم پایه
- Thread: واحد پایه اجرا در ماژول threading.
- Lock / RLock: قفل برای محافظت از منابع مشترک و جلوگیری از شرایط رقابتی.
- Event: سیگنالی برای هماهنگسازی بین ریسمانها.
- Condition: برای منتظر گذاشتن و اطلاعرسانی با شرایط پیچیدهتر.
- Semaphore: محدود کردن تعداد ریسمانهای همزمان دسترسییافته به منبع.
مثال ساده: ایجاد و اجرای Thread
import threading
import time
def worker(name, delay):
for i in range(3):
print(f"Worker {name} iteration {i}")
time.sleep(delay)
t1 = threading.Thread(target=worker, args=("A", 1))
t2 = threading.Thread(target=worker, args=("B", 0.5))
t1.start()
t2.start()
t1.join()
t2.join()
print("All threads finished")در این مثال دو ریسمان تعریف شده که توابع worker را اجرا میکنند. متد start ریسمان را اجرا و متد join منتظر پایان آن میماند. این الگو پایهایترین روش استفاده از threading است و برای کارهای I/O مناسب میباشد.
نکات مربوط به مثال بالا
- استفاده از sleep برای شبیهسازی I/O یا تأخیر مفید است.
- بدون join ممکن است برنامه اصلی پیش از اتمام ریسمانها خاتمه یابد.
محافظت از دادههای مشترک با Lock
import threading
counter = 0
lock = threading.Lock()
def increment(n):
global counter
for _ in range(n):
with lock:
temp = counter
temp += 1
counter = temp
threads = [threading.Thread(target=increment, args=(100000,)) for _ in range(4)]
for t in threads:
t.start()
for t in threads:
t.join()
print("Counter:", counter)در این قطعه، از یک Lock برای جلوگیری از «شرایط رقابتی» هنگام افزایش شمارنده استفاده شده است. عبارت with lock بهصورت خودکار acquire و در پایان release را انجام میدهد. بدون قفل، مقدار نهایی counter احتمالاً کمتر از انتظار خواهد بود زیرا دو ریسمان ممکن است مقدار قدیمی را بخوانند و بازنویسی کنند.
بهینهسازی و نکات
- تا حد امکان بخشهایی که قفل گرفته میشود کوتاه و سریع باشد تا تنگنای همزمانی کاهش یابد.
- درصورتیکه تابع دارای عملیات زمانبر I/O است، قفل را قبل/بعد از آن نگه ندارید؛ زیرا مفید نخواهد بود و باعث کاهش کارایی میشود.
هماهنگسازی با Event
import threading
import time
ready = threading.Event()
def waiter():
print("Waiter: waiting for ready signal")
ready.wait()
print("Waiter: got signal, proceeding")
def setter():
time.sleep(2)
print("Setter: setting the event")
ready.set()
t1 = threading.Thread(target=waiter)
t2 = threading.Thread(target=setter)
t1.start()
t2.start()
t1.join()
t2.join()Event یک پرچم باینری است که ریسمانها میتوانند منتظر آن بمانند (wait) یا آن را فعال کنند (set). در مثال بالا waiter تا زمانی که setter پس از دو ثانیه ready.set() را اجرا نکند، متوقف میماند. این الگو برای همزمانسازی ساده مناسب است.
سایر ابزارهای همزمانی در threading
| ابزار | کاربرد |
|---|---|
| Lock / RLock | محافظت از دادههای مشترک؛ RLock اجازه قفلِ مجدد توسط همان ریسمان را میدهد. |
| Event | همزمانسازی با سیگنال ساده (انتظار/فعالسازی). |
| Condition | الگوهای پیشرفتهتر انتظار/اطلاعرسانی با شرطهای پیچیده. |
| Semaphore | محدود کردن تعداد ریسمانهایی که همزمان به یک منبع دسترسی دارند. |
محدودیتها و مقایسه با multiprocessing
مهم است بدانید که در CPython محدودیت GIL وجود دارد؛ بنابراین برای کارهای محاسباتی سنگین threading معمولاً تسریع محسوسی ایجاد نمیکند. در این موارد استفاده از multiprocessing یا پیادهسازی با C-extension یا کتابخانههای موازیسازی سطح پایین توصیه میشود. اما برای عملیات I/O، شبکه، و تعامل با رابط کاربری threading هنوز گزینهٔ مناسبی است.
الگوها و بهترین شیوهها
- از کامپوننتهای سطح بالاتر مثل concurrent.futures.ThreadPoolExecutor برای مدیریت مجموعهای از کارها استفاده کنید؛ این روش سادهتر و ایمنتر است.
- قفلها را بهصورت دقیق و فقط روی کد ضروری اعمال کنید.
- برای اشتراکگذاری دادهها بهجای متغیرهای گلوبال، از صفها (queue.Queue) استفاده کنید که از پیش thread-safe هستند.
- ریسمانها را daemon کنید اگر میخواهید آنها با پایان برنامه اصلی تعطیل شوند، اما مراقب باشید که منابع درستی آزاد شوند.
مثال استفاده از Queue با threading (الگو)
import threading
import queue
import time
q = queue.Queue()
def producer():
for i in range(5):
print("Producing", i)
q.put(i)
time.sleep(0.5)
q.put(None) # sentinel
def consumer():
while True:
item = q.get()
if item is None:
break
print("Consumed", item)
print("Consumer done")
t1 = threading.Thread(target=producer)
t2 = threading.Thread(target=consumer)
t1.start()
t2.start()
t1.join()
t2.join()در این الگو از queue.Queue برای رد و بدل پیام بین producer و consumer استفاده شده است. Queue خود thread-safe است و نیاز به قفل دستی را کاهش میدهد. sentinel (در اینجا None) برای اطلاعرسانی پایان تولید مورد استفاده قرار گرفته است.
نتیجهگیری و دیدگاههای تخصصی
ماژول threading در پایتون ابزار قدرتمندی برای همزمانسازی وظایف I/O و رابط کاربری فراهم میآورد. با اینحال باید محدودیتهای GIL را بشناسید و از ابزارهای صحیح (Lock، Event، Condition، Semaphore، Queue) در سناریوهای مناسب استفاده کنید. برای مدیریت سادهتر مجموعهای از کارها، concurrent.futures.ThreadPoolExecutor را در نظر بگیرید و همیشه الگوهای ایمن و کوتاهمدت برای قفلگذاری را دنبال کنید.
با رعایت این نکات میتوانید از مزایای همزمانی در پایتون بهرهمند شوید بدون آنکه منجر به خطاها و پیچیدگیهای غیرضروری شوید.
آیا این مطلب برای شما مفید بود ؟




