在多线程或者多进程编程中,都需要同步机制。有名信号量可以实现进程之间的同步,线程之间同步机制选择比较多,常用的有三种,互斥锁,条件变量和无名信号量。另外,c++还提供了一些原子操作的变量atomic,也可以帮助线程之间同步。对于互斥锁和条件变量,linux系统提供了一套pthread mutex和pthread condition机制(POSIX Thread),c++标准库也提供了一套std的mutex和condition类,当然,标准库的底层实现也是基于系统提供的系统调用。使用的时候尽量使用std提供的类,接口会比较简单。
下面依次总结如下:
一.互斥锁:mutex
1.linux系统调用
a.静态分配的mutex
#include
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;//默认属性, 不需要destroy。
//both return 0 success, or a positive error number on error
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
//linux还提供了上面两个函数的变体,使用的比较少。
/*如果该mutex已经被lock,则会失败,并返回EBUSY错误*/
int pthread_mutex_trylock(pthread_mutex_t *mutex)
#include
/*如果此时mutex被lock,该mutex会等待指定的时间,如果指定的时间內不能获得该锁,则返回ETIMEDOUT错误*/
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex,
const struct timespec *restrict_abstime)
b.动态分配的mutex
对于动态分配于堆中的mutex,或者栈中分配的自动变量,或者经由静态分配且不使用默认属性的mutex,需用使用如下初始化接口:
#include
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_mutexattre_t *attr);
/*init 过的mutex使用完后需要destroy。destroy之后的mutex,还可以重新init使用*/
int pthread_mutex_destroy(pthread_mutex_t *mutex);
c.mutex的属性
I : PTHREAD_MUTEX_NORMAL//默认
II : PTHREAD_MUTEX_ERRORCHECK
error check类型会对三种情况进行检测,遇到这三种类型,返回错误。这种锁比较慢,一般用来调试使用,可以找出哪里的锁出了问题,产品中不建议使用。
三种情况为:同一线程对同一mutex加锁两次, 线程对不为自己所有的mutex解锁,线程对一尚未锁定的mutex解锁。
III : PTHREAD_MUTEX_RECURSIVE
同一线程可以多次lock,每次lock内部计数会+1。unlock的时候需要同样的次数才能解锁。
设置mutex属性实例:
pthread_mutex_t mtx;
pthread_mutexattr_t mtx_attr;
int s, type;
s = pthread_mutexattr_init(&mtx_attr);
if (s != 0) {
ERROR("pthread mutexattr init error");
}
s = pthread_mutexattr_settype(&mtx_attr, PTHREAD_MUTEX_ERRORCHECK);
if (s != 0) {
ERROR("pthread mutexattr settype error.");
s = pthread_mutex_init(&mtx, &mtx_attr);
if (s != 0)
ERROR("pthread mutex init error.");
s = pthread_mutexattr_destroy(&mtx_attr);
if (s != 0)
ERROR("pthread mutex attr destroy error");
/*do some thing*/
s = pthread_mutex_destroy(&mtx);
if (s != 0)
ERROR("pthread mutex destroy error");
2.std标准库提供的类(参考c++标准库P989)
c++ 标准库提供了以下的mutex class。
#include
class std::mutex
class std::recursive_mutex 允许同一线程多次锁定。解锁则需要最后一个unlock之后其他线程才能获取这个锁。
class std::timed_mutex//额外允许你传递一个时间段或者时间点,用来定义多长时间內它可以尝试捕捉一个lock。为此额外提供了两个接口try_lock_for(duration)和try_lock_until(tp)
class std::recursive_timed_mutex
操作:
mutex m;
m.~mutex()//销毁mutex
lock()
try_lock()//锁定成功返回true
unlock()
try_lock_for(duration)//尝试在时间段dur內锁定,如果锁定成功,返回true
try_lock_until(tp)//尝试在时间点tp之前锁定,如果锁定成功,返回true
同时,为了能自动unlock,标准库还提供了class std::lock_guard 模板类
值得注意的是,lock_guard 对象并不负责管理 Mutex 对象的生命周期,lock_guard 对象只是简化了 Mutex 对象的上锁和解锁操作,方便线程对互斥量上锁,即在某个 lock_guard 对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而 lock_guard 的生命周期结束之后,它所管理的锁对象会被解锁
std::mutex val_mutex;
std::lock_guard::mutex> lg(val_mutex);//lock and automatically unlock.
/*比如我们可以如下定义一个宏##的意思参考http://blog.csdn.net/hellokandy/article/details/50592971)*/
#define AUTO_MEM_LOCK(mutex) \
std::lock_guard __mutex_lock__##mutexlock(mutex)
try_lock()的示例:
std::mutex m;
while(!m.try_lock()) {
do_some_other_thing();
}
//adopt_lock意思是将已经锁定的mutex过继给lock_guard,确保lock被释放。
std::lock_guard lg(m,std::adopt_lock);
锁住多个lock
std::mutex m1;
std::mutex m2;
{
std::lock(m1,m2);//lock both mutexes.直到两个mutex都锁定才返回
std::lock_guard::mutex> lockm1(m1, std::adopt_lock);
std::lock_guard::mutex> lockm2(m2, std::adopt_lock);
}
unique_lock :
c++标准库还提供了class unique_lock<> 模板类。和lock_guard的功能非常相似,但其独有的优点为:
如果析构时,mutex仍被锁住,其析构函数自动解锁,如果没有被锁住,则析构函数不做任何事情。比lock_guard多了三个构造函数:std::try_to_lock std::chrono::seconds(1) std::defer_lock。所以一般推荐使用unique_lock,功能比较全。
//std::try_to_lock 表示企图锁定mutex,但是不希望阻塞。
std::unique_lock<std::mutex> lock(mutex, std::try_to_lock);
if(lock)//if lock was successful,检查关联的mutex是否被锁定
do_some_thing()
//传递一个时间段或时间点给构造函数,表示尝试在一个明确的时间周期內锁定(时间长度或者某个时间点)
std::unique_lock<std::mutex> lock(mutex, std::chrono::seconds(1));//1s內锁定
std::unique_lock<std::mutex> lock(mutex, std::chrono::milliseconds(1));//1ms內锁定
// std::defer_lock表示初始化这个lock object,但尚未打算锁住它。
std::unique_lock<std::mutex> lock(mutex, std::defer_lock);
do_some_thing();
lock.lock();//开始锁定
do_some_thing()
lock.unlock();//开锁
//unique_lock离开作用域也会自动unlock。另外,还提供了release()函数用来释放mutex或是将mutex拥有权转移给另一个lock。
二.条件变量:condition:
条件变量condition允许一个线程就某个共享变量的状态变化通知其他线程,并让其他线程在共享变量没有变化的时候等待。condition必须配合mutex使用。
条件变量不保存状态信息,signal时如果没有线程在等待,则会丢失该signal,如果signal之后再有线程wait,则需要等待下一个signal才能唤醒。
1。linux提供的系统调用
a.静态分配的条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
#include
/*All return 0 on success, or a positive error number on error*/
int pthread_cond_signal(pthread_cond_t *cond);//保证唤醒一个线程的wait
int pthread_cond_broadcast(pthread_cond_t *cond); //唤醒所有线程的wait
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)//等待条件变量的signal or broadcast。
/*如果超时没有被唤醒,则返回ETIMEOUT错误*/
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
b.动态分配的条件变量
对于动态分配于堆中的condition,或者栈中分配的自动变量,或者经由静态分配且不使用默认属性的condition,需用使用如下初始化接口:
#include
/*return 0 on success, or a positive error number on error*/
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);//静态分配的condition不需要destroy。
下面提供一个生产者-消费者实例:
//生产者
s = pthread_mutex_lock(&mtx);
if (s != 0)
ERROR("pthread mutex lock error");
avail ++;
s = pthread_mutex_unlock(&mtx);
if (s != 0)
ERROR("pthread mutex unlock error");
s = pthread_cond_signal(&cond);
if (s != 0)
ERROR("pthread cond signal error");
//消费者
for(;;)
{
s = pthread_mutex_lock(&mtx);
if (s != 0)
ERROR("pthread mutex lock error");
while(avail == 0) {
s = pthread_cond_wait(&cond, &mtx);//该函数内部会unlock,被唤醒后重新lock。
if (s != 0)
ERROR("pthread cond wait error");
}
while(avail > 0) {
/*do something*/
avail --;
}
s = pthread_mutex_unlock(&mtx);
if (s != 0)
ERROR("pthread mutex unlock error");
}
2。std标准库提供的类
#include
#include //使用condition的时候必须配合使用mutex
std::mutex ready_mutex;
std::condition_variable ready_cond;
//激发的线程调用,在mutex保护之外。
read_cond.notify_one();//notify one of the waiting threads
read_cond.notify_all();//notify all the waiting threads
//等待的线程
std::unique_lock<std::mutex> lk(ready_mtuex);//不能使用lock_guard.因为该lock不确定是否被锁定。
while(!ready_flag) {
read_cond.wait(lk);//该wait可能被假唤醒,所以被唤醒后需要检查等待的变量是否达到指定的条件。所以wait函数一般需要一个while循环。
}
还可以使用lambda表达式来达到同样的效果
std::unique_lock<std::mutex> lk(ready_mutex);
ready_cond.wait(lk, []{return ready_flag;});//每次被唤醒会检查ready_flag,直到返回true才真正返回。
condition的接口:
conditon_variable cv;
cv.notify_one();
cv.notify_all();
cv.wait(lk);
cv.wait(lk, pred)//直到等到pred结果为true才真正返回。
cv.wait_for(lk, duration);//返回枚举类std::cv_status::timeout和std::cv_status_no_timeout
cv.wait_for(lk, duration, pred);//同上面的wait
cv.wait_until(lk, timepoint);
cv.wait_until(lk, timepoint,pred);
notify_all_at_thread_exit(cv,lk);//用于线程exit时notify所有的wait。
三.atomic 变量
atomic提供一种比较简单的同步机制,因为即使对bool赋值,也不能保证是原子的,所以提供了atomic模板类来提供多种变量的一种atomic机制。因为为模板类,所以可以对bool, int type, ptr type使用。
#include
std::atomic ready_flag(false);
ready_flag.store(true);// 相当于 (ready_flag = true;)
ready_flag.load();//取当前值。
std::atomic int_flag;
++ int_flag;
int_flag = 5;
int_flag &= 0x01;
int_flag |= 0x01;
int_flag ^= 0x01;
另外,为了兼容c语言,还提供了一些c接口,比如atomic_bool等。可以参考《c++标准库》P1019.
四.信号量: semaphore:
信号量一般用于进程间通信和同步对共享资源的访问。linux系统提供了两种信号量机制,system v信号量和posix信号量,system v信号量比较复杂,现在一般使用posix信号量。标准库没有提供信号量的机制封装。信号量又分为有名信号量(基于文件)和未命名信号量(基于内存)。有名信号量可以实现进程之间的通信,而未命名信号量不能跨进程,除非在两个进程的共享内存中。posix信号量是一个整数,其值不能小于0。
1.命名信号量:
#include //defines o_* flags
#include //defines mode
#include
/*创建和打开semaphore,成功返回指针,失败返回SEM_FAILED。 oflag同文件的打开flag一样,如果为0,则访问一个既有信号量,如果指定了O_CREAT,并且与给定的name对应的信号量不存在,则会创建一个新信号量。如果同时指定了O_CREAT和O_EXCL,同时与给定的name的信号量已经存在,则返回错误。mode规定访问权限*/
sem_t *sem_open(const char *name, int oflag, .../*mode_t mode, unsigned int value*/);
/*return 0 on success, or -1 on error*/
int sem_close(sem_t *sem);//关闭信号量
int sem_unlink(const char *name);//删除信号量。return 0 on success, or -1 on error.
/*wait会递减(减1)sem引用的信号量的值。如果当前值大于0,则立即返回,如果等于0,则会wait。如果被信号处理器中断,则会返回失败,error_number为EINTR.*/
int sem_wait(sem_t *sem);
/*sem_wait的非阻塞版本,如果信号量value值等于0,不会阻塞,会返回EAGAIN错误。*/
int sem_try_wait(sem_t *sem);//return 0 on success, or -1 on error.
/*超时返回ETIMEDOUT错误,需要定义如下宏。*/
#define _XOPEN_SOURCE 600
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);//return 0 on success, or -1 on error.
/*发布一个信号量(增加1)*/
int sem_post(sem_t *sem);//return 0 on success, or -1 on error.
/*获取当前信号量的值*/
int set_getvalue(sem_t *sem, int *sval);
2.未命名信号量:
未命名的信号量也被称为基于内存的信号量。它的sem_post, sem_wait, sem_getvalue等函数和有名信号量一样,但额外提供了另外两个函数来初始化和销毁信号量
#include
/*使用value中指定的值来对sem指向的未命名信号量进行初始化。pshared如果为0,则在线程间共享,如果不等于0,则在进程间共享,此时,该sem必须位于两个进程的共享内存中。*/
int sem_init(sem_t *sem, int pshared, unsigned int value);//return 0 on success, or -1 on error.
sem_t sem;
sem_init(&sem, 0, 0);
int sem_destroy(sem_t *sem);//return 0 on success, or -1 on error.