رفتن به محتوای اصلی
برنامه‌سازی پیشرفته با پایتون درس‌نامهٔ آزاد
درس پیشرفته ۳۵ دقیقه

هم‌روندی، موازی‌سازی و شبکه

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

هدف این درس

بتوانی مسابقهٔ داده‌ای را تشخیص دهی، با قفل و صف امن هم‌روندی کنی و کلاینت/سرور سوکتی بسازی.

در این مطلب ۳

دو برنامه که «هم‌زمان» پیش می‌روند لزوماً «موازی» نیستند. هم‌روندی (concurrency) یعنی مدیریت چند کارِ درهم‌رفته؛ موازی‌سازی (parallelism) یعنی اجرای واقعیِ هم‌زمان روی چند هسته. ریسه‌ها در پایتون برای کارِ منتظرِ I/O عالی‌اند، ولی به‌خاطر GIL برای محاسبات سنگینِ CPU موازیِ واقعی نمی‌سازند.

ریسه در برابر فرایند

فرایند حافظهٔ جدا دارد و سنگین است؛ ریسه سبک است ولی حافظه را با ریسه‌های هم‌فرایندش شریک می‌شود. همین شراکت، هم قدرت است هم دردسر: دردسرش اسم دارد — مسابقهٔ داده‌ای (race condition).

import threading

counter = 0
lock = threading.Lock()

def worker(n):
    global counter
    for _ in range(n):
        with lock:  # ناحیهٔ بحرانی: فقط یک ریسه در هر لحظه
            counter += 1

threads = [threading.Thread(target=worker, args=(10_000,)) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(counter)  # همیشه 40000 — بدون قفل، کمتر و تصادفی بود
تعریف ناحیهٔ بحرانی

بخشی از کد که به حالت مشترک دسترسی دارد و نباید دو ریسه هم‌زمان واردش شوند. قفل (Lock) ورود را یکی‌یکی می‌کند.

برداشت نادرست رایج

اشتباه رایج

چون GIL هست، در پایتون هیچ‌وقت مسابقهٔ داده‌ای رخ نمی‌دهد.

تصحیح

GIL فقط تضمین می‌کند بایت‌کدها درهم اجرا نشوند، نه این‌که عملیات چندقدمی (خواندن-تغییر-نوشتن) اتمی باشد. counter += 1 بدون قفل در CPython هم گم می‌کند.

به‌جای قفل دستی: صف امن‌ریسه‌ای

بهترین همگام‌سازی، کمترین حالت مشترک است. الگوی تولیدکننده/مصرف‌کننده با queue.Queue (که خودش امن‌ریسه‌ای است) جای قفل دستی:

import queue, threading

jobs = queue.Queue()
for i in range(10):
    jobs.put(f"job-{i}")

def consumer():
    while True:
        try:
            job = jobs.get_nowait()
        except queue.Empty:
            return
        print("انجام شد:", job)
        jobs.task_done()

workers = [threading.Thread(target=consumer) for _ in range(3)]
for w in workers: w.start()
for w in workers: w.join()

برای کارهای «اجرا و جواب بگیر» هم concurrent.futures.ThreadPoolExecutor تمیزترین ابزار است.

شبکه: سوکت TCP

سوکت، نقطهٔ پایانیِ یک اتصال است. در TCP: سرور گوش می‌دهد و قبول می‌کند، کلاینت وصل می‌شود، هر دو بایت می‌فرستند و می‌گیرند، آخرش اتصال بسته می‌شود.

# server.py
import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind(("127.0.0.1", 65432))
    s.listen()
    print("سرور گوش می‌دهد...")
    conn, addr = s.accept()
    with conn:
        print("وصل شد:", addr)
        data = conn.recv(1024)
        conn.sendall(b"echo:" + data)
# client.py
import socket

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect(("127.0.0.1", 65432))
    s.sendall("سلام سرور".encode("utf-8"))
    print(s.recv(1024).decode("utf-8"))

سنجش فهم — هم‌روندی و شبکه

۰ از ۳ پاسخ داده شده

  1. بدون قفل، حلقهٔ counter += 1 در چهار ریسه چه نتیجه‌ای می‌دهد؟
  2. برای محاسبات سنگین CPU-bound در پایتون چه باید کرد؟
  3. سوکت مستقیم رشتهٔ پایتون می‌فرستد و نیازی به encode/decode نیست.(درست / نادرست)

اول به همهٔ پرسش‌ها پاسخ دهید، بعد بررسی کنید.

مطالب مرتبط

از همین بخش و با برچسب‌های مشترک

این مطلب را خواندید؟ آن را علامت بزنید تا پیشرفت شما روی همین دستگاه ذخیره شود.

وضعیت پیشرفت برای هم‌روندی، موازی‌سازی و شبکه