Threading vs Multiprocessing
Threading vs Multiprocessing
Threading vs Multiprocessing
لديك ثلاثة أدوات للتزامن في Python: asyncio (تعلّمته)، و threading (الخيوط)، و multiprocessing (العمليات). اختيار الأداة الخاطئة لمشكلتك يعني كوداً أبطأ أو أعقد دون فائدة حقيقية. في هذا الدرس ستفهم الفرق الحقيقي وتعرف متى تختار كل أسلوب.
قانون GIL — العائق الخفي
GIL اختصار لـ Global Interpreter Lock — قفل يمتلكه مترجم Python ولا يُطلقه إلا في لحظات محددة. القاعدة البسيطة: خيط Python واحد فقط يُنفّذ كود Python في أي لحظة، حتى لو كان جهازك يمتلك 8 أنوية.
لماذا يوجد GIL؟ لأن إدارة ذاكرة Python (reference counting) ليست آمنة للخيوط بدون قفل مركزي. إزالة GIL مهمة ضخمة تعمل عليها Python 3.13+ لكنها لم تكتمل بعد.
ماذا يعني هذا عملياً؟
┌──────────────────────────────┐
│ GIL (القفل) │
└──────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
[خيط-1] [خيط-2] [خيط-3]
ينتظر يُنفّذ ينتظر
(يملك GIL)
الخيوط تتبادل GIL — لكن واحداً فقط يعمل في كل لحظة. لذلك:
- مهام CPU-bound مع threading = بطيء (الخيوط تتنافس على نفس القفل)
- مهام I/O-bound مع threading = مناسب (الخيط يُطلق GIL أثناء الانتظار)
threading — الخيوط في Python
threading.Thread تُنشئ خيطاً جديداً. كل الخيوط تشترك في نفس الذاكرة (نفس المتغيرات العالمية) لكن لديها مكدس تنفيذ (call stack) مستقل.
import threading
import time
def download_file(filename, delay):
# محاكاة تنزيل ملف — Simulate file download
print(f"بدء تنزيل {filename}...")
time.sleep(delay) # انتظار I/O — I/O wait
print(f"اكتمل: {filename}")
# إنشاء خيطين — Create two threads
t1 = threading.Thread(target=download_file, args=("تقرير.pdf", 2))
t2 = threading.Thread(target=download_file, args=("صورة.jpg", 1))
start = time.time()
t1.start()
t2.start()
# انتظر انتهاء الخيطين — Wait for both threads
t1.join()
t2.join()
print(f"الوقت الكلي: {time.time() - start:.1f}s") # ~2s لا ~3s
ملاحظة Pyodide: بيئة الأكواد التفاعلية في هذه الصفحة تعمل داخل Web Worker ذو خيط واحد — لا يمكن تشغيل threading.Thread حقيقية فيها. الأمثلة أعلاه للقراءة وتنفيذها على جهازك.
متى تستخدم threading:
- تنزيل أو رفع ملفات متعددة في نفس الوقت
- استدعاء APIs متعددة تحتاجها معاً
- مهام تحتوي استدعاءات مكتبات C تُطلق GIL (مثل بعض عمليات numpy)
- كود قديم يستخدم مكتبات blocking لا تدعم asyncio
multiprocessing — العمليات المتعددة
multiprocessing.Process تُنشئ عملية (Process) مستقلة تماماً — ذاكرة مستقلة، مترجم Python مستقل، وبالتالي GIL مستقل لكل عملية. العمليات المتعددة هي الطريقة الوحيدة للحصول على تشغيل Python حقيقي على أنوية متعددة.
import multiprocessing
import time
def heavy_computation(n, result_list, index):
# حساب مكثف — CPU-intensive computation
total = sum(i * i for i in range(n))
result_list[index] = total
print(f"حساب {n}: {total}")
# بالتوازي على أنوية مختلفة — Parallel on different CPU cores
if __name__ == "__main__":
# Manager لمشاركة البيانات بين العمليات
# Manager for sharing data between processes
manager = multiprocessing.Manager()
results = manager.list([0, 0, 0])
processes = [
multiprocessing.Process(target=heavy_computation, args=(1_000_000, results, 0)),
multiprocessing.Process(target=heavy_computation, args=(2_000_000, results, 1)),
multiprocessing.Process(target=heavy_computation, args=(3_000_000, results, 2)),
]
for p in processes:
p.start()
for p in processes:
p.join()
print(f"النتائج: {list(results)}")
لماذا if __name__ == "__main__":؟ لأن multiprocessing على Windows وmacOS يستخدم spawn لإنشاء العمليات — يُعيد تشغيل الملف من الصفر في كل عملية. بدون هذا الشرط، كل عملية جديدة ستُنشئ عمليات أخرى بشكل لانهائي.
ملاحظة Pyodide: كذلك multiprocessing لا تعمل في Pyodide. الأمثلة للتنفيذ على جهازك المحلي.
Pool: أسهل من Process
multiprocessing.Pool تُدير مجموعة عمليات تلقائياً:
from multiprocessing import Pool
def square(n):
# تربيع العدد — Square the number
return n * n
if __name__ == "__main__":
numbers = [1, 2, 3, 4, 5, 6, 7, 8]
with Pool(processes=4) as pool:
# توزيع العمل على 4 عمليات — Distribute work to 4 processes
results = pool.map(square, numbers)
print(results) # [1, 4, 9, 16, 25, 36, 49, 64]
pool.map تعمل مثل map العادية لكنها تُوزع العمل على العمليات في pool. نتائجها بنفس ترتيب المدخلات.
ThreadPoolExecutor و ProcessPoolExecutor
المكتبة concurrent.futures تُقدم واجهة موحدة أبسط للخيوط والعمليات:
from concurrent.futures import ThreadPoolExecutor, ProcessPoolExecutor
import time
def fetch_url(url):
# محاكاة طلب HTTP — Simulate HTTP request
time.sleep(1)
return f"بيانات من {url}"
urls = ["url-1", "url-2", "url-3", "url-4"]
# مع خيوط (مناسب للـ I/O) — With threads (good for I/O)
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(fetch_url, urls))
print(results) # جُمعت في ~1 ثانية لا ~4 ثواني
# مع عمليات (مناسب للـ CPU) — With processes (good for CPU)
# with ProcessPoolExecutor(max_workers=4) as executor:
# results = list(executor.map(cpu_heavy_task, data))
مقارنة شاملة: اختر أداتك
| المعيار | asyncio | threading | multiprocessing |
|---|---|---|---|
| نوع المهمة | I/O-bound | I/O-bound | CPU-bound |
| خيوط حقيقية | لا (خيط واحد) | نعم (GIL يحدّ) | عمليات منفصلة |
| تعدد أنوية | لا | لا (GIL) | نعم |
| مشاركة الذاكرة | سهلة | سهلة (بحذر) | صعبة (تحتاج IPC) |
| استهلاك الذاكرة | منخفض | متوسط | مرتفع |
| التعقيد | متوسط | متوسط | مرتفع |
| مكتبات قديمة | يحتاج مكتبات async | يعمل مع أي مكتبة | يعمل مع أي مكتبة |
خوارزمية الاختيار
هل المهمة تنتظر شبكة/ملف/قاعدة بيانات؟
نعم → هل المكتبة تدعم async؟
نعم → asyncio (الخيار الأفضل)
لا → threading
لا → هل هي حسابات مكثفة؟
نعم → multiprocessing (الخيار الوحيد)
لا → ربما لا تحتاج تزامناً أصلاً
محاكاة التزامن: playground تعليمي
هذا المثال يُحاكي منطق التزامن بكود متزامن — يمكّنك من رؤية التأثير بدون خيوط حقيقية:
مزالق threading الشائعة
Race Condition: خيطان يُعدّلان نفس المتغير في نفس الوقت:
import threading
counter = 0
def increment():
global counter
# ❌ هذا غير آمن — خيطان قد يقرآن نفس القيمة
# Unsafe — two threads may read the same value
for _ in range(100_000):
counter += 1 # read → modify → write (3 خطوات!)
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start(); t2.start()
t1.join(); t2.join()
print(counter) # قد لا يكون 200_000! — May not be 200,000!
الحل: threading.Lock
import threading
counter = 0
lock = threading.Lock()
def safe_increment():
global counter
for _ in range(100_000):
with lock: # فقط خيط واحد يدخل في كل مرة — Only one thread enters at a time
counter += 1