Qarşılıqlı İstisna nədir?
Qarşılıqlı İstisna (Mutual Exclusion) — paralel və ya konkurrent proqramlaşdırmada bir neçə proses və ya thread-in eyni anda paylaşılan resursa (shared resource) girişinin qarşısını alan, yalnız bir prosesin kritik bölgəyə (critical section) daxil olmasına icazə verən sinxronizasiya prinsipidir. Mutual exclusion race condition (yarış vəziyyəti) və data corruption (məlumat korlanması) problemlərinin həllinə yönəlmiş fundamental konsepsiyadır. Qısaca "mutex" və ya "muteks" olaraq da adlandırılır.
Qarşılıqlı İstisna nədir?
Müasir kompüter sistemlərində bir neçə proses və ya thread eyni anda işləyir (multitasking, multithreading). Bu proseslər tez-tez paylaşılan resurslara — yaddaş sahələri, fayllar, database, printer, şəbəkə bağlantısı — giriş edir. Əgər bir neçə proses eyni anda eyni məlumata yazma əməliyyatı aparsa, nəticə qeyri-müəyyən və səhv ola bilər. Qarşılıqlı istisna bu problemin həllidir.
Kritik Bölgə (Critical Section): Proqram kodunun paylaşılan resursa giriş edən hissəsi. Kritik bölgədə yalnız bir proses/thread eyni anda ola bilər. Digərləri gözləməlidir (wait/block).
Nümunə ssenari (bank hesabı): İki proses eyni bank hesabından pul çıxır:
- Hesabda 1000 AZN var
- Proses A: 500 AZN çıxarmaq istəyir
- Proses B: 600 AZN çıxarmaq istəyir
Qarşılıqlı istisna olmadan:
- Proses A balansı oxuyur: 1000 AZN
- Proses B balansı oxuyur: 1000 AZN (A hələ yazmayıb)
- Proses A hesablayır: 1000 - 500 = 500, yazır
- Proses B hesablayır: 1000 - 600 = 400, yazır
- Nəticə: Hesabda 400 AZN qalır (səhvdir, 400 AZN olmalı idi və ya 1000 - 500 = 500 və ya rədd)
Qarşılıqlı istisna ilə:
- Proses A lock (kilid) alır
- Proses A balansı oxuyur, yenilənməsini yazır
- Proses A unlock edir
- Proses B lock alır (indi icazə verilir)
- Proses B balansı oxuyur, yenilənməsini yazır
- Proses B unlock edir
- Nəticə: Düzgün məlumat
Qarşılıqlı İstisnanın şərtləri
Dijkstra (Edsger Dijkstra, kompüter elmi pioneeri) mutual exclusion üçün dörd əsas tələb müəyyənləşdirmişdir:
1. Mutual Exclusion (Qarşılıqlı İstisna özü): Kritik bölgədə maksimum bir proses ola bilər. İki proses eyni anda kritik bölgədə ola bilməz.
2. Progress (İrəliləmə): Əgər heç bir proses kritik bölgədə deyilsə və bəzi proseslər daxil olmaq istəyirsə, seçim prosesi sonlu vaxtda baş verməlidir və bir proses seçilməlidir. Deadlock (ölü kilid) olmamalıdır.
3. Bounded Waiting (Məhdud Gözləmə): Proses kritik bölgəyə daxil olmaq üçün sonsuz gözləyə bilməz. Gözləmə müddəti məhduddur. Starvation (aclıq) problemi olmamalıdır — bir proses daim atlanmamalıdır.
4. No Assumptions (Fərziyyə yoxdur): Proseslərin sürəti və ya sayı haqqında fərziyyə edilməməlidir. Həll hər şəraitdə işləməlidir.
Qarşılıqlı İstisna mexanizmləri
1. Lock (Kilid) / Mutex
Mutex (Mutual Exclusion Lock): Ən sadə və ən geniş yayılmış mexanizm. Binary variable (0 və ya 1, unlocked və ya locked).
İşləmə prinsipi:
- Proses kritik bölgəyə daxil olmaq istəyəndə mutex-i "lock" edir
- Mutex artıq locked-dirsə, proses block olur (gözləyir)
- Kritik bölgədən çıxanda mutex "unlock" edilir
- Gözləyən proses (əgər varsa) oyanır və mutex-i lock edir
Kod nümunəsi (C, POSIX threads):
#include <pthread.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_counter = 0;
void* thread_function(void* arg) {
pthread_mutex_lock(&mutex); // Lock
// Critical section
shared_counter++;
printf("Counter: %d\n", shared_counter);
pthread_mutex_unlock(&mutex); // Unlock
return NULL;
}
Spinlock: Lock almaq üçün busy-waiting (aktiv gözləmə) edən mutex növü. Proses block olmur, loop-da davamlı yoxlayır (spin). Qısa kritik bölgələr üçün effektiv, lakin CPU waste edir.
Recursive Mutex: Eyni thread tərəfindən bir neçə dəfə lock edilə bilər. Unlock sayı lock sayına bərabər olmalıdır.
2. Semaphore (Semafor)
Semaphore: Mutex-dən daha ümumi mexanizm. Counter variable (0, 1, 2, ..., N). İki əməliyyat: wait (P, down) və signal (V, up).
Binary Semaphore: Counter 0 və ya 1. Mutex ilə ekvivalentdir.
Counting Semaphore: Counter 0-dan N-ə qədər. N sayda resursa giriş idarə edir. Məsələn, database connection pool-da 10 connection varsa, semaphore counter = 10.
İşləmə prinsipi:
- wait() / P(): Counter-i azaldır. Əgər counter < 0 olsa, proses block olur.
- signal() / V(): Counter-i artırır. Block olmuş proseslərdən birini oyadır.
Kod nümunəsi (C, POSIX):
#include <semaphore.h>
sem_t semaphore;
sem_init(&semaphore, 0, 1); // Initial value = 1 (binary semaphore)
void* thread_function(void* arg) {
sem_wait(&semaphore); // P operation, lock
// Critical section
printf("In critical section\n");
sem_post(&semaphore); // V operation, unlock
return NULL;
}
Producer-Consumer problem-də istifadə:
sem_t empty; // Empty buffer slots
sem_t full; // Full buffer slots
sem_t mutex; // Mutex for buffer access
sem_init(&empty, 0, BUFFER_SIZE);
sem_init(&full, 0, 0);
sem_init(&mutex, 0, 1);
// Producer
void* producer(void* arg) {
while(1) {
produce_item();
sem_wait(&empty); // Wait for empty slot
sem_wait(&mutex); // Lock buffer
add_to_buffer();
sem_post(&mutex); // Unlock buffer
sem_post(&full); // Signal full slot
}
}
// Consumer
void* consumer(void* arg) {
while(1) {
sem_wait(&full); // Wait for full slot
sem_wait(&mutex); // Lock buffer
remove_from_buffer();
sem_post(&mutex); // Unlock buffer
sem_post(&empty); // Signal empty slot
consume_item();
}
}
3. Monitor
Monitor: Yüksək səviyyəli sinxronizasiya konstruksiyası. Object-oriented proqramlaşdırmada istifadə olunur. Paylaşılan məlumat və ona giriş metodları bir vahiddə birləşir. Mutual exclusion avtomatik təmin olunur.
Xüsusiyyətlər:
- Yalnız bir thread monitor-un metodlarını eyni anda icra edə bilər
- Condition variables (şərt dəyişənləri) ilə wait və notify
- Java synchronized metodlar və blocks monitor konsepsiyasından istifadə edir
Kod nümunəsi (Java):
class BankAccount {
private int balance = 1000;
// Synchronized method - monitor
public synchronized void withdraw(int amount) {
if (balance >= amount) {
balance -= amount;
System.out.println("Withdrew: " + amount);
} else {
System.out.println("Insufficient funds");
}
}
public synchronized void deposit(int amount) {
balance += amount;
System.out.println("Deposited: " + amount);
}
public synchronized int getBalance() {
return balance;
}
}
Condition Variables:
class ProducerConsumer {
private Queue<Integer> buffer = new LinkedList<>();
private int capacity = 10;
public synchronized void produce(int item) throws InterruptedException {
while (buffer.size() == capacity) {
wait(); // Wait if buffer full
}
buffer.add(item);
notify(); // Notify consumer
}
public synchronized int consume() throws InterruptedException {
while (buffer.isEmpty()) {
wait(); // Wait if buffer empty
}
int item = buffer.remove();
notify(); // Notify producer
return item;
}
}
4. Read-Write Lock (Oxuma-Yazma Kilidi)
Read-Write Lock: Oxuma və yazma əməliyyatları üçün fərqli lock strategiyası.
Prinsip:
- Bir neçə reader (oxuyan) eyni anda resursa giriş edə bilər (oxuma data-nı dəyişdirmir)
- Writer (yazan) eksklüziv giriş tələb edir — heç bir reader və ya digər writer olmamalıdır
- Writer gözləyərkən yeni reader-lər block olur (writer priority)
İstifadə halı: Database, cache, shared data structure-lər. Oxuma əməliyyatları çox, yazma az.
Kod nümunəsi (C++, std::shared_mutex):
#include <shared_mutex>
std::shared_mutex rw_mutex;
int shared_data = 0;
// Reader thread
void reader() {
std::shared_lock<std::shared_mutex> lock(rw_mutex);
// Multiple readers can be here simultaneously
int value = shared_data;
std::cout << "Read: " << value << std::endl;
}
// Writer thread
void writer(int new_value) {
std::unique_lock<std::shared_mutex> lock(rw_mutex);
// Only one writer, no readers
shared_data = new_value;
std::cout << "Written: " << new_value << std::endl;
}
5. Peterson's Algorithm (Software solution)
Peterson's Algorithm: Hardware dəstəyi olmadan iki proses üçün mutual exclusion təmin edən software həlli. 1981-ci ildə Gary Peterson tərəfindən.
Algoritm (2 proses üçün):
bool flag[2] = {false, false};
int turn = 0;
// Process 0
void process0() {
flag[0] = true;
turn = 1;
while (flag[1] && turn == 1); // Busy wait
// Critical section
flag[0] = false;
}
// Process 1
void process1() {
flag[1] = true;
turn = 0;
while (flag[0] && turn == 0); // Busy wait
// Critical section
flag[1] = false;
}
Qeyd: Müasir CPU-lar instruction reordering (təlimat sırasının dəyişdirilməsi) edə bilir və Peterson's Algorithm bu halda işləməyə bilər. Memory barrier/fence lazımdır.
6. Hardware-based solutions
Test-and-Set: Atomic (bölünməz) hardware təlimatı. Variable-i test edir və eyni anda yeni dəyər set edir.
bool test_and_set(bool *target) {
bool old_value = *target;
*target = true;
return old_value;
}
// Lock implementation
bool lock = false;
void enter_critical_section() {
while (test_and_set(&lock)); // Spin until lock acquired
}
void exit_critical_section() {
lock = false;
}
Compare-and-Swap (CAS): Modern CPU-larda (x86: CMPXCHG). Dəyəri müqayisə edir və eyni atomic əməliyyatda yeniləyir.
bool compare_and_swap(int *ptr, int expected, int new_value) {
if (*ptr == expected) {
*ptr = new_value;
return true;
}
return false;
}
Atomic Operations: CPU səviyyəsində atomic (kəsilməz) əməliyyatlar. C11/C++11-də <stdatomic.h> və <atomic>.
#include <atomic>
std::atomic<int> counter(0);
void increment() {
counter.fetch_add(1, std::memory_order_relaxed);
// Atomic, thread-safe
}
Qarşılıqlı İstisna problemləri
1. Deadlock (Ölü Kilid)
İki və ya daha çox proses bir-birinin tutduğu resursu gözləyir və heç biri irəliləyə bilmir.
Nümunə:
- Proses A: Lock1 tutur, Lock2 gözləyir
- Proses B: Lock2 tutur, Lock1 gözləyir
- Nəticə: Deadlock, hər iki proses sonsuz gözləyir
Deadlock-un 4 şərti (Coffman conditions):
- Mutual exclusion
- Hold and wait (tutur və gözləyir)
- No preemption (zorla almaq olmur)
- Circular wait (dövri gözləmə)
Deadlock prevention:
- Lock-ları həmişə eyni sıra ilə almaq
- Timeout mechanism
- Lock ordering
- Deadlock detection alqoritmləri
2. Livelock
Proseslər aktiv, lakin heç biri irəliləməyir. Deadlock-dan fərqli olaraq, proseslər state dəyişir, lakin nəticə əldə etmir.
Nümunə: İki adam korridorda qarşılaşır, hər ikisi eyni tərəfə çəkilir, yenidən qarşılaşır, davam edir...
3. Starvation (Aclıq)
Proses uzun müddət və ya sonsuz lock əldə edə bilmir. Digər proseslər daim prioritet alır.
Həll: Fair scheduling, aging (gözləmə müddəti artdıqca prioritet artır).
4. Priority Inversion (Prioritet inversiyası)
Aşağı prioritetli proses lock tutur, yüksək prioritetli proses gözləyir, orta prioritetli proses işləyir. Yüksək prioritetli proses aşağı prioritetlidən asılı olur.
Həll: Priority inheritance protocol — lock tutan proses müvəqqəti olaraq yüksək prioritet alır.
5. Convoying
Çoxlu yüksək prioritetli proseslər bir aşağı prioritetli prosesin buraxdığı lock-u gözləyir.
Performans və optimallaşdırma
Lock Granularity (Kilit dənəvərliyi):
- Coarse-grained lock: Böyük məlumat strukturuna bir lock. Sadə, lakin low concurrency.
- Fine-grained lock: Kiçik hissələrə ayrıca lock-lar. Yüksək concurrency, lakin mürəkkəb, overhead.
Lock-Free və Wait-Free alqoritmlər: CAS (Compare-and-Swap) kimi atomic əməliyyatlardan istifadə edərək lock olmadan thread-safe data structure-lər. Yüksək performans, lakin dizayn çətin.
Lock Elision və Transactional Memory: Hardware səviyyəsində lock-ların optimallaşdırılması. Intel TSX (Transactional Synchronization Extensions).
RCU (Read-Copy-Update): Linux kernel-də istifadə olunan mexanizm. Reader-lər lock almır, writer yeni versiya yaradır və pointer-i update edir.
Praktik istifadə və məsləhətlər
Lock-ları qısa müddətə tutun: Kritik bölgə minimal olmalıdır. Uzun əməliyyatları lock xaricində edin.
Lock ordering: Çoxlu lock istifadə edərkən həmişə eyni sıra ilə alın (deadlock prevention).
Try-lock ilə timeout: pthread_mutex_trylock(), std::mutex::try_lock(). Deadlock-dan çıxmaq üçün timeout.
Avoid nested locks: Mümkünsə nested (iç-içə) lock-lardan çəkinin.
Testing: Thread-safety testləri, race condition detector (Valgrind Helgrind, Thread Sanitizer).
Documentation: Hansı lock hansı məlumatı qoruyur, aydın sənədləşdirin.
Nəticədə, qarşılıqlı istisna konkurrent və paralel proqramlaşdırmanın fundamental konsepsiyasıdır və paylaşılan resurslara təhlükəsiz girişi təmin edir. Mutex, semaphore, monitor, read-write lock kimi müxtəlif mexanizmlər fərqli ssenarilərdə istifadə olunur. Deadlock, livelock, starvation kimi problemlərdən çəkinmək vacibdir. Düzgün mutual exclusion strategiyası thread-safe, etibarlı və performant proqramlar yaratmağa imkan verir. Multithreaded proqramlaşdırmada mutual exclusion-un başa düşülməsi və düzgün tətbiqi kritik bacarıqdır və software mühəndisliyinin mühüm hissəsidir.