目录
一、问题引入
我们再次看看上次讲到的多线程抢票的代码:这次我们让一个线程抢完票之后不去做任何事。
#include <iostream>
#include <unistd.h>
#include <cstring>
#include <time.h>
#include <pthread.h>
using namespace std;
#define THREAD_NUM 5
class threaddata
{
public:
threaddata(const string &s, pthread_mutex_t *m)
: name(s), mtx(m)
{}
public:
string name;
pthread_mutex_t *mtx;
};
int ticket = 100;
void *getticket(void *arg)
{
threaddata *td = (threaddata *)arg;
while (true)
{
pthread_mutex_lock(td->mtx);
if (ticket > 0)
{
usleep(rand() % 10000);
cout << td->name << ":"
<< " " << ticket << endl;
ticket--;
pthread_mutex_unlock(td->mtx);
}
else
{
pthread_mutex_unlock(td->mtx);
break;
}
}
delete td;
return nullptr;
}
int main()
{
pthread_mutex_t mtx;
pthread_mutex_init(&mtx, nullptr);
pthread_t t[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i++)
{
string name = "thread ";
name += to_string(i + 1);
threaddata *td = new threaddata(name, &mtx);
pthread_create(t + i, nullptr, getticket, (void *)td);
}
for (int i = 0; i < THREAD_NUM; i++)
pthread_join(t[i], nullptr);
pthread_mutex_destroy(&mtx);
return 0;
}
运行结果:
我们这就发现了一个问题,对于抢票系统,我们看到的是只有一个线程5在一直连续抢票,没有其他的线程。这很不合理。
这是因为如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后什么也不做,所以在我们看来这个线程就一直在申请锁和释放锁,那么它就可以一直抢票。这就可能导致其他线程长时间竞争不到锁,造成了其他线程的饥饿问题(无法抢票)。虽然,你是拿到锁后再去访问临界资源,并且最后还释放了锁,由于竞争能力太强,可以一直拿到锁,这没有错,但这不合理。
为了解决这个问题,我们增加一个限制:当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后。这样,我们就有了线程同步:我们在保证数据安全的情况下让这些线程按照一定的顺序进行临界资源的访问,这就是线程同步。
竞态条件:因为时序问题,而导致程序异常,我们称为竞态条件。
二、实现线程同步的方案——条件变量
2.1、常用接口:
1、条件变量的定义和初始化
NAME
pthread_cond_destroy, pthread_cond_init - destroy and initialize condition variables
SYNOPSIS
#include <pthread.h>
//销毁
int pthread_cond_destroy(pthread_cond_t *cond);
//初始化
int pthread_cond_init(pthread_cond_t *restrict cond,
const pthread_condattr_t *restrict attr);
//全局和静态变量初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
2、线程等待临界资源:
pthread_cond_wait 功能:一就是让线程在特定的条件变量下等待,二就是让线程在等待时释放对应的互斥锁。当线程被唤醒时,该函数会帮助我们线程获取锁。
#include <pthread.h>
//特定时间阻塞等待
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex,
const struct timespec *restrict abstime);
//等待
int pthread_cond_wait(pthread_cond_t *restrict cond,
pthread_mutex_t *restrict mutex);
3、唤醒线程去访问临界资源
#include <pthread.h>
// 唤醒所有等待的线程
int pthread_cond_broadcast(pthread_cond_t *cond);
// 唤醒一个线程
int pthread_cond_signal(pthread_cond_t *cond);
注:
1、条件变量通常需要配合互斥锁一起使用。
2、条件变量的使用:一个线程等待条件变量的条件成立而被挂起;另一个线程使条件成立 后唤醒等待的线程。
3、等待的时候往往是在临界区内等待的。(加锁与解锁之间的区域进行等待)
4、线程被唤醒,是在之前进行等待的地方被唤醒。
2.2、使用示例
有了线程同步,我们就可以改进我们之前的抢票系统的代码:
#include <iostream>
#include <string>
#include <time.h>
#include <unistd.h>
#include <pthread.h>
using namespace std;
#define THREADNUM 3
typedef void *(*func)(void *argc);
class threaddata
{
public:
threaddata(pthread_mutex_t *mtx, pthread_cond_t *cond, const string &name)
: mtx_(mtx), cond_(cond), name_(name)
{}
public:
pthread_mutex_t *mtx_;
pthread_cond_t *cond_;
string name_;
};
int ticket = 100;
void *getticket(void *arg)
{
threaddata *td = (threaddata *)arg;
while (true)
{
pthread_mutex_lock(td->mtx_);
pthread_cond_wait(td->cond_, td->mtx_); // 在加锁和解锁之间进行等待
if (ticket > 0)
{
usleep(rand() % 10000);
cout << td->name_ << ":"
<< " " << ticket << endl;
ticket--;
pthread_mutex_unlock(td->mtx_);
}
else
{
pthread_mutex_unlock(td->mtx_);
break;
}
}
delete td;
return nullptr;
}
void *fun1(void *arg)
{
threaddata *td = (threaddata *)arg;
while (true)
{
getticket((void *)td);
sleep(1);
}
}
void *fun2(void *arg)
{
threaddata *td = (threaddata *)arg;
while (true)
{
getticket((void *)td);
sleep(1);
}
}
void *fun3(void *arg)
{
threaddata *td = (threaddata *)arg;
while (true)
{
getticket((void *)td);
sleep(1);
}
}
int main()
{
srand((unsigned long)time(nullptr) ^ getpid() ^ 433);
pthread_mutex_t mtx;
pthread_cond_t cond;
pthread_mutex_init(&mtx, nullptr);
pthread_cond_init(&cond, nullptr);
pthread_t t[THREADNUM];
func fun[THREADNUM] = {fun1, fun2, fun3};
for (int i = 0; i < THREADNUM; i++)
{
string name = "thread ";
name += to_string(i + 1);
threaddata *td = new threaddata(&mtx, &cond, name);
pthread_create(t + i, nullptr, fun[i], (void *)td);
}
sleep(5);
while (true)
{
pthread_cond_signal(&cond);
sleep(1);
}
for (int i = 0; i < THREADNUM; i++)
pthread_join(t[i], nullptr);
pthread_mutex_destroy(&mtx);
pthread_cond_destroy(&cond);
return 0;
}
如果我们每次都想将在该条件变量下等待的所有线程进行唤醒,可以将代码中的pthread_cond_signal函数改为pthread_cond_broadcast函数。
此时我们会发现唤醒这三个线程时具有明显的顺序性,因为这些线程启动时默认都会在该条件变量下去等待,而我们每次都唤醒的是在当前条件变量下等待的头部线程,当该线程执行完代码后会继续排到等待队列的尾部进行等待。