目录
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 fullCount) to 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.
Semaphore | Lock | |
resources occupied | any 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 resource | A 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+房源