为了避免多个线程同事读写同一个数据而产生不可预料的后果,我们要将各个线程对同一数据的访问同步。所谓同步,即指在一个线程访问数据未结束的时候,其他线程不得不对同一个数据进行访问。如此,对数据的访问被原子化了。
同步的最常见的方法是使用锁(Lock).锁是一种非强制机制,每一个线程在访问数据或资源之前首先试图获取锁,并在访问结束之后释放锁。在锁已经被占用的时候试图获取锁时,线程会等待,直到锁重新可用。
- 二元信号量是最简单的一种锁,他只有两种状态:占用与非占用。它适合只能被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一试图获取该二元信号量的线程会获得该锁,并将二元信号量置为占用状态,伺候其他的所有试图获取该二元信号量的线程会等待,直到该锁被释放。
对于允许多个线程并发访问的资源,多元信号量简称信号量,它是一个很好的选择。一个初始值为N的信号量允许N个线程并发访问。线程访问资源的时候首先获取信号量,进行如下操作:- 将信号量 的值减一
- 如果信号量的值小于0,则进入等待状态,否则继续执行。访问完资源之后,线程释放信号量,进行如下操作。
- 将信号量加1.
- 如果信号量的值小于1,唤醒一个等待中的线程。
常用的POSIX信号量函数
#include <semaphore.h>
sem_init函数用于初始化一个未命名的信号量,pshared参数指定信号量的类型。如果其值为0,就表示这个信号量是当前信号量的局部信号量,否则该信号量就可以在多个进程之间共享。value参数指定信号量的初始值。此外,初始化一个已经被初始化的信号量将导致不可预期的后果
int sem_init(sem_t *sem, int pshared, unsigned int value);
‘
sem_destroy函数用于销毁信号量,已释放其占用的内核资源。如果销毁一个正在被其他线程等待的信号量,则将导致不可预期的后果。
int sem_destroy(sem_t *sem);
//以原子操作的方式将信号量的值减一,如果信号量的值为0,则sem_wait将被阻塞,直到这个信号为非0值
int sem_wait(sem_t *sem);
相当与sem_wait的非阻塞版本。
int sem_trywait(sem_t *sem);
sem_post函数以原子操作的方式将信号量的值加1.当信号量的值大于0时,其他正在调用sem_wait等待信号的线程将被唤醒。
int sem_post(sem_t *sem);
#include <iostream>
#include <pthread.h>
#include <exception>
#include <semaphore.h>
#include <unistd.h>
using namespace std;
class sem {
public:
sem()
{
if (sem_init( &m_sem, 0, 1 ) != 0) {
throw std::exception();
}
}
~sem() { sem_destroy(&m_sem); }
bool wait() { return sem_wait(&m_sem) == 0; }
bool post() { return sem_post(&m_sem) == 0; }
private:
sem_t m_sem;
};
void *thread_fun(void *arg)
{
printf("child thread\n");
sem sem_t;
sem_t.wait();
printf("main thread 5 seconds ,post after\n");
sem_t.post();
}
int main()
{
pthread_t id;
sem sem_t;
pthread_create(&id, NULL, thread_fun, NULL);
printf("main thread\n");
sem_t.wait();
sleep(2);
sem_t.post();
}
- 互斥量和二元信号量很类似,资源仅同时允许一个线程访问,但和信号量不同的是,信号量在整个系统可以被任意线程获取并释放,也就是说,同一个信号量可以被系统中的一个线程之后由另一个线程释放。而互斥量则要求哪个线程获取了互斥量,哪个线程就要负责释放这个锁,其他线程越俎代庖去释放互斥量是无效的。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <exception>
int a = 0;
class locker {
public:
locker()
{
if (pthread_mutex_init(&m_mutex, NULL) != 0) {
throw std::exception();
}
}
~locker() { pthread_mutex_destroy(&m_mutex); }
bool lock() { return pthread_mutex_lock(&m_mutex) == 0; }
bool unlock() { return pthread_mutex_unlock(&m_mutex) == 0; }
private:
pthread_mutex_t m_mutex;
};
locker mutex_t;
void *thread_fun(void *arg)
{
printf("thread_fun1 %d\n",a);
mutex_t.lock();
//printf("thread_fun1 %d\n",a);
a--;
mutex_t.unlock();
printf("thread_fun2 %d\n",a);
}
int main()
{
pthread_t id;
pthread_create(&id, NULL, thread_fun, NULL);
printf("%d\n",a);
mutex_t.lock();
a++;
//sleep(5); //主线程加锁,子线程会等待主线程解锁后加锁。
mutex_t.unlock();
printf("%d\n",a);
pthread_join(id, NULL);
}
- 临界区是比互斥量更加严格的同步手段。在术语中,把临界区的获取称为进入临界区,而把锁的释放称为离开临界区。临界区和互斥量与信号量的区别在于,互斥量和信号量在系统中的任何进程里都是可见的,也就是说,一个进程创建了一个互斥量或信号量,另一个进程试图去获取该锁是合法的。然而,临界区的作用范围仅限于本进程,其他的进程无法获取该锁。除此之外,临界区具有和互斥量相同的性质。
- 读写锁致力于一种更加特定的场合的同步。对于一段数据,多个线程同时读取总是没有问题的,但假设操作都不是原子型,只要有任何一个线程试图对这个数据进行修改,就必须使用同步手段来避免出错。如果我们使用上述信号量,互斥量或临界区中的任何一种来进行同步,尽管可以保证程序正确,但对于读取频繁,而仅仅偶尔写入的情况,会显得非常低效。读写锁可以避免这个问题。对于同一个锁,读写锁有两种获取方式,共享的或独占的。当锁处于自由的状态时,试图以任何一种方式获取锁都能成功,并将锁置于对应的状态。如果锁处于共享状态,其他线程以共享的方式获取仍然会成功,此时这个锁分配给了多个线程。然而,如果其他线程试图以独占的方式获取已经处于共享状态的锁,那么他将必须等待锁被所有线程释放。相应的,处于独占状态的锁将阻止任何其他线程获取该锁,不论他们试图以哪种方式获取。读写锁的行为可以总结如下图
读写锁 | 以共享方式获取 | 以独占方式获取 |
---|---|---|
自由 | 成功 | 成功 |
共享 | 成功 | 等待 |
独占 | 等待 | 等待 |
- 条件变量作为一种同步手段,作用类似与一个栅栏。对于条件变量,线程可以有两种操作,首先线程可以等待条件变量,一个条件变量可以被多个线程等待。其次,线程可以唤醒条件变量,此时某个或所有等待此条件变量的线程都会被唤醒并继续支持。也就是说,使用条件变量可以让许多线程一起等待某个事件的发生,当事件发生时(条件变量被唤醒),所有的线程可以一起恢复执行。
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <exception>
using namespace std;
class cond {
public:
cond() {
if (pthread_mutex_init(&m_mutex, NULL) != 0) {
throw std::exception();
}
if (pthread_cond_init(&m_cond, NULL) != 0) {
pthread_mutex_destroy(&m_mutex);
throw std::exception();
}
}
~cond()
{
pthread_mutex_destroy(&m_mutex);
pthread_cond_destroy(&m_cond);
}
bool wait()
{
int ret = 0;
pthread_mutex_lock(&m_mutex);
ret = pthread_cond_wait(&m_cond, &m_mutex);
pthread_mutex_unlock(&m_mutex);
return ret = 0;
}
bool signal() {
return pthread_cond_signal(&m_cond) == 0;
}
private:
pthread_mutex_t m_mutex;
pthread_cond_t m_cond;
};
cond cond_t;
void *thread_fun(void *arg)
{
cond_t.wait();
printf("child thread is signal\n");
}
int main()
{
pthread_t id;
pthread_create(&id, NULL, thread_fun, NULL);
int a = 1;
if (a) {
sleep(1);
cond_t.signal();
printf("signal\n");
}
pthread_join(id, NULL);
}