Lock vs Semaphore vs Condition Variable vs Monitor

本文围绕多线程编程中的同步与通信展开。介绍了临界区,需互斥访问;阐述了锁、信号量、条件变量和监视器等机制,以生产者 - 消费者问题为例说明其应用,指出锁注重资源所有权,信号量侧重线程通信,条件变量可避免忙等待,监视器是 Java 内置同步方法。

目录

1. Critical Section(CS)

2. Lock 

3. Semaphore 

4. Condition variable

5. Monitor

6. Summary


In mutil-threads (concurrent) programing, two or more threads have access to a shared memory, for avoiding confusing results, these threads should access the shared memory in proper order, this is also called sychronization. But what is more, in order to make sychronization more efficient, these threads may communicate with each other. This article is mainly about sychronization and communication.

1. Critical Section(CS)

Critical section is a piece of codes of a program that should not be accessed by multiple threads at the same time. It should be accessed in mutual exclusion. At most one thread can enter in CS at a time. 

2. Lock 

Lock provides a means to achieve mutual exclusion. It is guaranteed that only one thread can enter in critical section at a time. Let us have a look at the following pesudo codes.

Lock num_mutex; // num_mutex is a lock

######################################


Thread1:

// acquire lock
mutex_lock(&num_mutex);

// ******** critical section *******
num++;
// ******** critical section *******

// release lock
mutex_unlock(&num_mutex); 


######################################


Thread2:

// acquire lock
mutex_lock(&num_mutex);

// ******** critical section *******
num++;
// ******** critical section *******

// release lock
mutex_unlock(&num_mutex);

As shown in above codes, num++ is not an atomic operation, so num++  should be excuted by only one thread at a time. When Thread1 and Thread2 are acquiring the same lock simultaneously, only one thread will get the lock finally, and then enter in CS,  the other thread will be waiting until the lock is released.

For classic producer-consumer problem, we can use Lock to solve it.

Lock count_mutex; // count_mutex is a lock


#############################################



Producer Thread

// acquire lock
mutex_lock(&count_mutex);

// if count is equal to MAX_SIZE, do not produce and then sleep to wait
while(count == MAX_SIZE){
    
    // release lock to give lock to consumer
    mutex_unlock(&count_mutex);

    Thread.sleep(3000); // sleep for 3s
    
    // awake to acquire lock again
    mutex_lock(&count_mutex);
}

// ***** critical section ****//

// produce 
produce();

// after producing, increment count by 1
count++;

// ***** critical section ****//

// release lock after producing
mutex_unlock(&count_mutex);



##############################################


Consumer Thread

// acquire lock
mutex_lock(&count_mutex);

// if count is equal to 0, do not consume and then sleep to wait
while(count == 0){
    
    // release lock to give lock to producer
    mutex_unlock(&count_mutex);

    Thread.sleep(3000); // sleep for 3s
    
    // awake to acquire lock again
    mutex_lock(&count_mutex);
}

// ***** critical section ****//

// consume
consume();

// after consuming, decrement count by 1
count--;

// ***** critical section ****//

// release lock after consuming
mutex_unlock(&count_mutex);


There are two threads: Producer and Consumer, Producer is producing until count is equal to MAX_SIZE, similarly Consumer is consuming until count is equal to 0,  (produce(); count++) and   (consume(); count--)  are critical section, either Producer or Consumer enters in the critical section, they must acquire the lock(count_mutex) before, and only one will acquire the lock at a time. But once one thread gets the lock and immediately checks whether the condition is satisfied, if not satisfied it will wait until the condition is satisfied. In this case, Producer and Consumer check whether count is satisfied,  if not they will enter a while loop: release lock and sleep for three seconds and then wake up to try to acquire the lock again. 

Solving with producer-consumer problem using lock seems not bad, but as we see in the case above, producer and consumer do not communicate with each other, they have to wait for some condition to become true by repeatedly checking, it will cause busy waiting. Busy waiting wastes cpus cycles and slows down runing threads, so we should try our best to avoid busy waiting.

3. Semaphore 

A semaphore is usually termed as a set of permit that controls access to a common resource by multiple threads. A semaphore is initialized with a integer value N( N>0 ), N means the maximum number of threads that can share a common resource concurrently.  Semaphore has two operations: wait(or P) and signal(or V), wait decrements the value of semaphore by 1 and causes the calling thread blocked(added to a waiting queue) if the new value is negative, signal increments the value of semaphore by 1 and wakes up one thread in waiting queue, the awaked thread is ready for being scheduled again.

An example for producer-consumer problem with Semaphore as belows:

// emptyCount, mutex and fullCount are a Semaphore
Semaphore emptyCount = N, mutex = 1, fullCount = 0; 


####################################################


Producer Thread

// decrement emptyCount by 1 to get permit to produce, 
// producer will be put in waiting queue if new value is negative 
sem_wait(&emptyCount);

sem_wait(&mutex);

// ******** critical section *********

produce();

// ******** critical section *********

sem_signal(&mutex);

// successfully produce, increment fullCount by 1 
// and causes the waiting consumer resume consuming 
sem_signal(&fullCount);



################################################################


Consumer Thread

// decrement fullCount by 1 to get permit to consume
// consumer will be put in waiting queue if new value is negative 
sem_wait(&fullCount);

sem_wait(&mutex);

// ******** critical section *********

consume();

// ******** critical section *********

sem_signal(&mutex);

// successfully consume, increment emptyCount by 1 
// and causes the waiting producer resume producing 
sem_signal(&emptyCount);


In the above codes, Producer and Consumer communicate with each other by operating Semaphore (emptyCount and fullCountto avoid busy waiting. Note that mutex is also a Semaphore, mutex is initialized with 1, in this case it  is used as a lock to protect the critical section.  Some articles regard a semaphore with initial value 1 as a lock, but there are some significant differences between lock and semaphore.

SemaphoreLock
resources occupiedany thread can release the resource(any thread can call signal() )

the resource can only be released by the thread that owns the lock

acquire/release

a thread don't need to release resource after successfully acquiring the resourceA thread must release the resource aflter acquiring the resource, otherwise it will cause dead-lock.

Because of these differences between Semaphore and Lock, Lock pays more attention to resources and the owership of resources while Semaphore focuses on communication between threads.

4. Condition variable

Condition variable takes advantages of Semaphore, it can be seen as a container of waiting threads like a semaphore, differently it has to be used with a mutex(lock). Condition variable has three operations: wait(), signal() and broadcast()wait() causes the current thread to release the lock and wait until it is awaked, note that the current thread must own the lock before. signal() wakes up one waiting thread and the awaked thread is ready for acquiring the lock again and resuming executing( if more than one threads are acquiring the lock, the awaked thread and other threads compete to acquire the lock fairly). broadcast() is similar to signal() but it will wake up all waiting threads. These operations are illustrated in the picture below :

Then let us use Condition variable to modify the above producer-consumer example with Lock.

// condi_produce and condi_consume are a Condition variable
Condition condi_produce, condi_consume;


#########################################################


Producer Thread

// acquire lock
mutex_lock(&count_mutex);

// if count is equal to MAX_SIZE, do not produce and then wait
while(count == MAX_SIZE){
    
    // release lock(count_mutex) and wait on condi_produce
    condition_wait(&condi_produce, &count_mutex);
}

// ***** critical section ****//

// produce 
produce();

// after producing, increment count by 1
count++;

// ***** critical section ****//

mutex_unlock(&count_mutex);

// notify Consumer to resume consuming
condition_signal(&condi_consume);



##############################################


Consumer Thread

// acquire lock
mutex_lock(&count_mutex);

// if count is equal to 0, do not consume and then wait
while(count == 0){
    
    // release lock(count_mutex) and wait on condi_consume 
    condition_wait(&condi_produce, &condi_consume);
}

// ***** critical section ****//

// consume
consume();

// after consuming, decrement count by 1
count--;

// ***** critical section ****//

mutex_unlock(&count_mutex);

// notify Producer to resume producing
condition_signal(&condi_produce);


With Conditional Variable, it provides a means in which Producer and Consumer communicate with each other to notify each other to resume executing instead of busy waiting. So Condition variable is definitely a good partner of Lock.

5. Monitor

Java provides a efficient built-in synchronization method called Monitor.  Monitor is similar to Lock(Mutex) + Condition variable. Every object in java has its own monitor,  but we can not get a object of Monitor directly, instead Java provides Synchronized, Object.wait(),  Object.notify() and Object.notifyAll() to achieve synchronization based on Monitor. Let us find out how to solve producer-consumer problem with Monitor.

//sync_object is a object and asscociated with a monitor
Object sync_object;


################################################# 


Producer Thread

// acquire lock on sync_object, 
synchronized(sync_object){

    // check whether count is equal to MAX_SIZE
    while(count == MAX_SIZE){
        
        // release lock, and wait 
        sync_object.wait();
    }

    // produce 
    produce();

    // after producing, increment count by 1
    count++;

    // notify Consumer to resume consuming
    sync_object.notify();
}



##############################################


Consumer Thread

// acquire lock on sync_object, 
synchronized(sync_object){

    // check whether count is equal to 0
    while(count = 0){
        
        // release lock, and wait 
        sync_object.wait();
    }

    // consume
    consume();

    // after consuming, decrement count by 1
    count--;

    // notify Producer to resume producing
    sync_object.notify();
}


As shown in the example, Synchronized acquires the monitor lock of sync_object, wait() releases the monitor lock and puts the current thread into waiting queue, notify() wakes up one waiting thread.

6. Summary

Concurrent programing not only needs synchronization but also needs to achieve synchronization more efficiently.

👉👉👉 自己搭建的租房网站:全网租房助手,m.kuairent.com,每天新增 500+房源

哲学家就餐问题的设计与实现。 【问题描述】+ 哲学家进餐问题是操作系统中经典的同步问题,由 Di ikstza提出。五位哲学家围坐圆 桌,每人面前有一盘面,左右各有一把共享叉子。哲学家交替进行思考和进餐:进餐需后时 拿起左右两把又子。问题在于并发执行时,可能所有哲学家同时掌起左叉,等待右叉,导致 死锁一 一所有进程阻塞,无法推进。这模拟了多进程竞争有限资源(如工/0设备)时的冲突, 需设计机制避免僵局。 【设计要求】+ 避免死锁,确保系统安全; 防止饥饿,保证所有哲学家最终能进餐; 支持并发性,允许多个哲宁家同时进餐以提高效率;。 并实现公平性,避免某些哲学家长期等待。 设计应简单高效,减少额外开销,且易于扩展。 常见约束包括:哲学家必须原子性地获取两把又子,资源(叉子〉不可抢占,进程行为 非确定性。 『数据结构】+ 王要使用信号量(semaphore)管理资源:每个叉子对应一个二元信号量(初始值 1), 表示可用性(如 semaphore fork[5] =(1,1,1,1,1))。哲学家进程通过 rait (forkGJ)和 signal (fork[了)操作获取和释放叉子。辅助结构包括互斥锁(mutez)用于保护临界区(如 限制同时尝试进餐的哲学家数),或整型数组跟踪又子状态。更高级方案可能引入管程 (monitor)封装操作。。 【分折与实现】。 分析:死锁风险源于循环等待(所有哲学家持有一叉等待另一叉)。解决震破坏死锁条 件,如引入资源有序获取《强制哲学家先拿编号较小叉子〉或限制并发数(如只允许多至4 人同时尝试〉。实现时,用信号量构建算法:例如,设蛋一个互斥信号量控制进餐人数,哲 学家按顺序申请叉子。代码中每个哲学家进程循环执行:思考、申请叉子《有序 rait)、进 餐、程放买子(signal)。此方案确保安全性、活跃性和公平性,时间复杂度0(1) per 操作。c++
06-17
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值