9.4.17 الگو Deadlock Recovery

9.4.17 الگو Deadlock Recovery

9.4.17.1 توضیحات #

الگوی بازیابی از بن‌بست (Deadlock Recovery) یکی از الگوهای مهم در طراحی سیستم‌های همزمان (concurrent systems) است که به ما کمک می‌کند از شرایطی خطرناک به نام بن‌بست (deadlock) خارج شویم. در شرایط بن‌بست، دو یا چند گوروتین (یا نخ) در حالتی گیر می‌افتند که هر یک منتظر آزاد شدن منبعی است که توسط دیگری نگه داشته شده؛ در نتیجه هیچ‌کدام نمی‌توانند پیش بروند و کل سیستم در حالت توقف (freeze) باقی می‌ماند.

در زبان Go، به دلیل استفاده گسترده از goroutine و channel، احتمال وقوع بن‌بست در اثر طراحی نادرست بالا است. مثلاً اگر گوروتینی منتظر داده روی یک کانال بماند، در حالی که گوروتین ارسال‌کننده هرگز اجرا نشود یا مسدود شده باشد، بن‌بست اتفاق می‌افتد. در سیستم‌های واقعی، تشخیص و بازیابی از این وضعیت حیاتی است تا سیستم به‌صورت پیوسته و قابل اطمینان باقی بماند.

الگوی Deadlock Recovery معمولاً شامل سه مرحله است:
۱. نظارت (Monitoring): سیستم باید به‌صورت مداوم وضعیت گوروتین‌ها یا منابع مشترک را بررسی کند. این کار می‌تواند با استفاده از تایم‌اوت، لاگ‌گیری، یا ابزارهای profiler مانند pprof انجام شود.
۲. تشخیص (Detection): با تجزیه‌وتحلیل رفتار سیستم، مانند گوروتین‌هایی که برای مدت طولانی در حالت مسدود باقی مانده‌اند، می‌توان بن‌بست‌های احتمالی را شناسایی کرد.
۳. بازیابی (Recovery): پس از تشخیص، می‌توان با یکی از روش‌های زیر اقدام به آزادسازی سیستم کرد:

  • تلاش مجدد با backoff (بازگشت نمایی یا تصادفی)
  • بازتنظیم منابع یا صف‌ها
  • خاتمه دادن به گوروتین‌های مسدودشده
  • بازگردانی سیستم به حالت اولیه یا fail-safe

این الگو به‌ویژه در برنامه‌های distributed یا دارای state حساس مانند تراکنش‌های مالی، سیستم‌های صف (queue-based systems)، یا پایگاه‌داده‌های درون‌حافظه‌ای اهمیت دارد. اجرای صحیح این الگو باعث حفظ پایداری سیستم در برابر شرایط غیرمنتظره می‌شود، در حالی که غفلت از آن ممکن است به اختلال جدی یا از دست رفتن داده‌ها منجر شود.

9.4.17.2 دیاگرام #

sequenceDiagram participant Main participant G1 as Goroutine 1 participant G2 as Goroutine 2 participant DeadlockChecker as Select with Timeout Main->>G1: Start Main->>G2: Start G1->>mu1: Lock(mu1) G2->>mu2: Lock(mu2) G1->>mu2: Try Lock(mu2) ❌ G2->>mu1: Try Lock(mu1) ❌ Note over G1,G2: بن‌بست (Deadlock) ایجاد شد: G1 منتظر mu2، G2 منتظر mu1 DeadlockChecker->>DeadlockChecker: wait 3s... DeadlockChecker-->>Main: بن‌بست شناسایی شد ✅

9.4.17.3 نمونه کد #

 1package main
 2
 3import (
 4	"fmt"
 5	"sync"
 6	"time"
 7)
 8
 9func deadlockRecoveryExample() {
10	var mu1, mu2 sync.Mutex
11	done := make(chan string, 2)
12
13	// گوروتین اول: تلاش برای گرفتن mu1 سپس mu2
14	go func() {
15		mu1.Lock()
16		fmt.Println("G1: mu1 locked")
17		time.Sleep(1 * time.Second)
18
19		mu2.Lock()
20		fmt.Println("G1: mu2 locked")
21		time.Sleep(500 * time.Millisecond)
22
23		mu2.Unlock()
24		mu1.Unlock()
25		done <- "G1: done"
26	}()
27
28	// گوروتین دوم: تلاش برای گرفتن mu2 سپس mu1
29	go func() {
30		mu2.Lock()
31		fmt.Println("G2: mu2 locked")
32		time.Sleep(1 * time.Second)
33
34		mu1.Lock()
35		fmt.Println("G2: mu1 locked")
36		time.Sleep(500 * time.Millisecond)
37
38		mu1.Unlock()
39		mu2.Unlock()
40		done <- "G2: done"
41	}()
42
43	// انتظار برای پیام از گوروتین‌ها یا تشخیص بن‌بست
44	select {
45	case msg := <-done:
46		fmt.Println("✅ موفقیت:", msg)
47	case <-time.After(3 * time.Second):
48		fmt.Println("❌ بن‌بست شناسایی شد: یکی از گوروتین‌ها قفل شده است")
49	}
50}
51
52func main() {
53	deadlockRecoveryExample()
54}
1$ go run main.go
2G1: mu1 locked
3G2: mu2 locked
4❌ بن‌بست شناسایی شد: یکی از گوروتین‌ها قفل شده است

در این مثال، یک سناریوی کلاسیک از بن‌بست (Deadlock) در زبان Go شبیه‌سازی شده و با استفاده از مکانیزم select و time.After، وقوع آن تشخیص داده می‌شود. هدف این کد، نشان دادن چگونگی رخ دادن بن‌بست بین دو goroutine است که هر کدام سعی می‌کنند منابع مشترکی را قفل کنند، اما به دلیل ترتیب متفاوت در قفل‌گیری، در حالت انتظار دائمی قرار می‌گیرند.

در ابتدا، دو شیء قفل mu1 و mu2 از نوع sync.Mutex تعریف می‌شود. سپس دو goroutine راه‌اندازی می‌شوند. گوروتین اول (G1) ابتدا mu1 را قفل کرده و پس از کمی توقف، سعی می‌کند mu2 را نیز قفل کند. در مقابل، گوروتین دوم (G2) ابتدا mu2 را قفل کرده و پس از کمی توقف، سعی در قفل کردن mu1 دارد. به این ترتیب، هر کدام منتظر آزاد شدن قفلی هستند که در دست دیگری است و هیچ‌کدام نمی‌توانند ادامه دهند، در نتیجه بن‌بست واقعی رخ می‌دهد.

در بخش main، یک select برای خواندن پیام از کانال done تعریف شده که انتظار دارد یکی از goroutineها پس از انجام کار، پیامی ارسال کند. اما چون هر دو گوروتین در وضعیت قفل گیر افتاده‌اند و هیچ‌کدام به پایان نمی‌رسند، کانال done خالی می‌ماند. در نتیجه پس از ۳ ثانیه، بخش select وارد مسیر time.After می‌شود و پیام “بن‌بست شناسایی شد” چاپ می‌گردد.

این پیاده‌سازی ساده ولی گویا، نحوه وقوع بن‌بست، اهمیت ترتیب قفل‌گیری منابع، و روش تشخیص آن از طریق time-based watchdog را نشان می‌دهد. چنین مکانیزمی در سیستم‌های حساس به همزمانی بسیار ضروری است، چون بن‌بست می‌تواند کل سیستم را متوقف و ناپایدار کند. برای پیشگیری، طراحی قفل‌گیری منظم، استفاده از تایم‌اوت، context، و حتی الگوریتم‌هایی مانند TryLock یا timeout-based locking توصیه می‌شود.

9.4.17.4 کاربردها #

  • مدیریت منابع در برنامه‌های همزمان (Concurrent Resource Management):
    در سیستم‌هایی که گوروتین‌ها یا نخ‌ها به منابع مشترکی مانند فایل‌ها، حافظه، یا کانکشن‌های شبکه دسترسی دارند، استفاده از این الگو برای جلوگیری یا بازیابی از بن‌بست هنگام قفل‌گذاری (locking) منابع حیاتی است. با استفاده از تشخیص زمان‌محور، تلاش مجدد یا اولویت‌بندی دسترسی می‌توان از توقف کامل سیستم جلوگیری کرد.
  • پایگاه‌داده‌های توزیع‌شده و سیستم‌های تراکنشی (Distributed Databases & Transactions):
    در محیط‌های توزیع‌شده مانند دیتابیس‌های چندگره‌ای یا سیستم‌های ACID، تراکنش‌هایی که منتظر منابع قفل‌شده توسط سایر تراکنش‌ها هستند، ممکن است در حالت بن‌بست باقی بمانند. این الگو با تشخیص بن‌بست‌ها و اعمال سیاست‌هایی مانند abort و retry یا rollback، پایداری و در دسترس بودن سیستم را تضمین می‌کند.
  • سیستم‌های بلادرنگ و حساس به زمان (Real-Time Systems):
    در سیستم‌های بلادرنگ (مانند سامانه‌های کنترل صنعتی یا رباتیک)، حتی تأخیر جزئی می‌تواند بحرانی باشد. استفاده از الگوی بازیابی از بن‌بست باعث می‌شود سیستم بتواند به‌جای توقف کامل، در زمان محدود وضعیت را تشخیص داده و به روش fail-safe ادامه دهد.
  • اشکال‌زدایی و تحلیل همزمانی (Concurrency Debugging & Analysis):
    این الگو به توسعه‌دهندگان کمک می‌کند تا طراحی همزمانی برنامه را زیر نظر بگیرند و نقاطی که امکان وقوع بن‌بست دارند را شناسایی کنند. با ابزارهایی مثل pprof, trace و تحلیل کانال‌ها می‌توان مسیرهای اجرای بن‌بست‌زا را شناسایی و بازطراحی کرد.