最近在学习多线程的网络编程,互斥量和条件变量是多线程编程中常用的线程同步方式。在编写自己的高并发服务器的过程中对互斥量和条件变量进行了封装,想要测试一下自己封装的类是否正确,能否通过自己封装的条件变量类顺利实现多线程顺序打印。
条件变量
先来简单的看一下条件变量的常用接口:
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
//对条件变量进行初始化,条件变量的属性参数由attr确定,如果使用默认参数,attr一般为NULL;
int pthread_cond_destroy(pthread_cond_t *cond);
//解除条件变量,不过并不对条件变量的内存;
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abtime);
//阻塞,等待被唤醒,该接口较复杂,后面会详细介绍。pthread_cond_timewait接口增加了定时功能,一旦时间超过abtime,则不再继续阻塞。
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);
//唤醒阻塞于该条件变量的线程。signal只唤醒一个线程,broadcast则唤醒所有阻塞于该条件变量的线程
条件变量一般需要配合互斥量使用,下面配合一段伪代码来解释其工作方式:
伪代码如下:
线程A: 线程B:
pthread_mutex_lock(); pthread_mutex_lock();
pthread_cond_wait(); pthread_cond_signal();
…//执行代码 pthread_mutex_unlock;
pthread_mutex_unlock();
其具体的工作流程如下:
步骤 | 线程A | 线程B |
---|---|---|
(1) | 对临界区进行加锁 | |
(2) | 调用pthread_cond_wait阻塞该线程,等待被唤醒 | |
(3) | pthread_cond_wait释放掉锁 | |
(4) | 获的互斥锁 | |
(5) | 调用pthread_cond_signal唤醒阻塞于该条件变量的线程 | |
(6) | (此时线程已经唤醒,但是需要重新获取互斥锁才能继续运行,所以仍然阻塞) | 释放互斥锁 |
(7) | pthread_cond_wait()重新获得互斥锁,继续运行 |
所以pthread_cond_wait需要先释放掉拥有的互斥量之后再休眠。被唤醒之后需要重新获取互斥量之后才能继续运行。
弄清楚了这个之后,我们来看一下条件变量的常用方式,给定一个场景:线程A和B从队列Q中取数据,线程C向队列Q中填充数据,当队列Q为空时,A和B需要阻塞,等到C填充,队列不为空时再继续运行。
线程A: 线程C:
pthread_mutex_lock(); pthread_mutex_lock();
while(Q.empty()) Q.push();
pthread_cond_wait(); pthread_cond_signal();
Q.pop();//执行其他操作
pthread_mutex_unlock(); pthread_mutex_unlock();
线程B和A相同。
问题:可以看到多出了一个while,最开始看到这种写法的时候我也很疑惑,当线程C向Q中填充数据后,对线程A进行唤醒,线程A为什么还要判断一次Q中是否为空。
解答:加入while的原因是为了防止虚假唤醒,比如在上述的场景中,线程A判断到Q中为空,阻塞于pthread_cond_wait();然后C向队列中填充了一个数据,进行唤醒(此时,A已经唤醒了,但是还在等待获取互斥锁),C释放互斥锁,注意,如果此时线程B将互斥锁获取了(那么A只能继续等待获取互斥锁),将Q中数据取走,释放互斥锁,此时A再获取互斥锁,对Q进行操作(其实此时Q是一个空队列),可能会引发错误,所以需要再次判断是否满足条件,才能向下运行。
实现顺序打印
互斥量的简单封装:
class Mutex
{
public:
Mutex() { pthread_mutex_init(&_mutex, NULL); }
~Mutex()
{
pthread_mutex_lock(&_mutex);
pthread_mutex_destroy(&_mutex);
}
void lock()
{
pthread_mutex_lock(&_mutex);
}
void unlock()
{
pthread_mutex_unlock(&_mutex);
}
pthread_mutex_t* get() { return &_mutex; }
private:
pthread_mutex_t _mutex;
};
class MutexGuard//对类Mutex的一个RAII封装,在退出临界区时可以自动解锁。
{
public:
explicit MutexGuard(Mutex& mutex) :_mutex(mutex)
{
mutex.lock();
}
~MutexGuard()
{
_mutex.unlock();
}
private:
Mutex& _mutex;//注意,持有引用
};
条件变量封装
class Condition
{
public:
Condition(Mutex& mutex) :_mutex(mutex)
{
pthread_cond_init(&_con,NULL);
}
~Condition()
{
pthread_cond_destroy(&_con);
}
void wait()
{
pthread_cond_wait(&_con, _mutex.get());
}
void waitForSeconds(int seconds)
{
timespec timeout;
clock_gettime(CLOCK_REALTIME, &timeout);
timeout.tv_sec += static_cast<time_t>(seconds);
pthread_cond_timedwait(&_con, _mutex.get(), &timeout);
}
void notify()
{
pthread_cond_signal(&_con);
}
void notifyAll()
{
pthread_cond_broadcast(&_con);
}
private:
Mutex& _mutex;
pthread_cond_t _con;
};
调用上述类实现多线程顺序打印
static int a = 0;
Mutex mutex;
Condition cond(mutex);
void* ConditionTest2(void* arg)
{
int printNum = *(int*)arg;
for (int i = 0; i < 5; i++)
{
MutexGuard mutexGuard(mutex);
while (a % 3 != printNum)
cond.wait();
std::cout << printNum << std::endl;
a += 1;
cond.notifyAll();
}
}
int main()
{
int a = 0, b = 1, c = 2;
pthread_t pthreadID;
pthread_create(&pthreadID, NULL, ConditionTest2, &a);
pthread_create(&pthreadID, NULL, ConditionTest2, &c);
pthread_create(&pthreadID, NULL, ConditionTest2, &b);
sleep(30);
return 0;
}
运行结果如下:
成功实现多线程顺序打印