通过一个多线程的关于生产者与消费者的例子,熟悉和掌握pthread最常用的一些函数。
下面代码演示了10个线程做为生产者,不间断的往vector中插入数据。一个线程做为消费者,不间断的读取vector中的数据并删除。当处理PRODUCE_NUM_MAX个数据时停止。临界区资源使用pthread_mutex_t进行保护。资源不可用时为了避免使用轮询监测资源是否可用,可使用pthread_cond_t条件变量进行休眠等待,直到条件满足时被唤醒。
#include <stdio.h>
#include <pthread.h>
#include <mutex>
#include <vector>
#include "utility.h"
#define PRODUCE_NUM_MAX 10000
#define PRODUCE_BUFF_SIZE 40
static pthread_once_t s_pot = PTHREAD_ONCE_INIT;
static pthread_key_t s_pk;
struct MsgVec
{
pthread_mutex_t m_mutex; //一个mutex可以对应多个cond,但是一个cond只能对应一个mutex.
pthread_cond_t *m_pwcond; //写条件变量,动态申请的必须要调用destroy.
pthread_cond_t m_rcond; //读条件变量.
int m_maxid;
vector<int> m_vec;
MsgVec()
{
m_maxid = 0;
m_mutex = PTHREAD_MUTEX_INITIALIZER;
m_pwcond = (pthread_cond_t*)malloc(sizeof(pthread_cond_t));
m_rcond = PTHREAD_COND_INITIALIZER;
pthread_cond_init(m_pwcond, NULL);
}
~MsgVec()
{
if(m_pwcond)
{
pthread_cond_destroy(m_pwcond);
free(m_pwcond);
m_pwcond = NULL;
}
cout << "~Msg()" << endl;
}
};
struct Pthread_info
{
pthread_t m_pid;
long long m_starttime = 0; //millisecond
long long m_endtime = 0;
long long m_cnt = 0;
int m_fakewake = 0; //假唤醒次数。唤醒后条件不满足。
};
static pthread_mutex_t s_printmutex = PTHREAD_MUTEX_INITIALIZER;
void FreeKeyValue(void* data)
{
pthread_mutex_lock(&s_printmutex);
Pthread_info* pInfo = (Pthread_info*)data;
cout << "pthread:" << pInfo->m_pid << " produce cnt:" << pInfo->m_cnt << " fake wake cnt:" <<pInfo->m_fakewake << " use time:" << pInfo->m_endtime - pInfo->m_starttime << endl;
pthread_mutex_unlock(&s_printmutex);
delete pInfo;
}
//创建线程私有数据key和对应的资源清理函数
void CreatePthreadKey()
{
pthread_key_create(&s_pk, FreeKeyValue);
}
void* Produce(void* pData)
{
pthread_detach(pthread_self()); //使自身线程分离,这样主线程不需要join回收资源.
struct timespec tp;
clock_gettime(_CLOCK_REALTIME, &tp);
pthread_once(&s_pot, CreatePthreadKey); //所有线程只会调用一次.
Pthread_info* pThreaddata = NULL;
if(pthread_getspecific(s_pk) == NULL)// 各线程第一次pthread_getspecific反回NULL,然后设置自己的私有数据.
{
pThreaddata = new Pthread_info;
pthread_setspecific(s_pk, pThreaddata);//设置线程私有数据
pThreaddata->m_pid = pthread_self();
pThreaddata->m_starttime = tp.tv_sec * 1000000LL + tp.tv_nsec / 1000ll;
}
MsgVec* pVec = (MsgVec*)pData;
for(;;)
{
pthread_mutex_lock(&pVec->m_mutex);
while (pVec->m_vec.size() >= PRODUCE_BUFF_SIZE && pVec->m_maxid <PRODUCE_NUM_MAX)
{
pthread_cond_wait(pVec->m_pwcond, &pVec->m_mutex);
if(pVec->m_vec.size() >= PRODUCE_BUFF_SIZE)
pThreaddata->m_fakewake++;
}
if(pVec->m_maxid >= PRODUCE_NUM_MAX)
{
pthread_mutex_unlock(&pVec->m_mutex);
break;
}
for(int i = 0; i < 10; ++i)
pVec->m_vec.push_back(pVec->m_maxid++);
pThreaddata->m_cnt += 10;
pthread_mutex_unlock(&pVec->m_mutex);
//pthread_cond_signal,pthread_cond_broadcast可以在lock和unlock直接,也可以在unlock之外
//能使用pthread_cond_signal的地方,替换为pthread_cond_broadcast总不会错
//如果难以决定用哪个唤醒时,就使用broadcast方式。
pthread_cond_signal(&pVec->m_rcond);
}
clock_gettime(_CLOCK_REALTIME, &tp);
pThreaddata->m_endtime = tp.tv_sec * 1000000LL + tp.tv_nsec / 1000ll;
pthread_exit(NULL);
}
void* Consume(void* pData)
{
MsgVec* pVec = (MsgVec*)pData;
long long consumecnt = 0;
bool usebroadcast = true;
for(;;)
{
pthread_mutex_lock(&pVec->m_mutex);
while (pVec->m_vec.empty())
pthread_cond_wait(&pVec->m_rcond, &pVec->m_mutex);
if(pVec->m_vec.size() > PRODUCE_BUFF_SIZE || pVec->m_vec.size()%10 != 0)
cout << "vec error. size:" << pVec->m_vec.size() << endl;
consumecnt += pVec->m_vec.size();
pVec->m_vec.erase(pVec->m_vec.begin(), pVec->m_vec.end());
if(pVec->m_maxid == PRODUCE_NUM_MAX)
{
//如果不是broadcast模式,只会有一个producer退出,
//为确保所有producert退出,这里需要唤醒所有producert.
if(!usebroadcast)
pthread_cond_broadcast(pVec->m_pwcond);
pthread_mutex_unlock(&pVec->m_mutex);
break;
}
else
{
if(usebroadcast)
pthread_cond_broadcast(pVec->m_pwcond);
else
pthread_cond_signal(pVec->m_pwcond);
pthread_mutex_unlock(&pVec->m_mutex);
}
}
void* pRet = (void*)consumecnt;
//1.如果有多个信息要返回,可以把这些信息放在一个结构中,返回结构的地址。
//2.不要返回栈上变量的地址,因为线程退出后栈销毁,再访问的话就会出错。
pthread_exit(pRet);
}
int main()
{
MsgVec vec;
pthread_t pt_producer[10], pt_consumer;
for(int i = 0; i < arraysize(pt_producer); ++i)
pthread_create(&pt_producer[i], NULL, Produce, &vec);
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setstacksize(&attr, 4*1024*1024);
pthread_create(&pt_consumer, &attr, Consume, &vec);
pthread_setconcurrency(arraysize(pt_producer) + 1);
void* ret;
pthread_join(pt_consumer, (void**)&ret);//用ret接收pt_consumer线程返回的信息的地址。
pthread_mutex_lock(&s_printmutex);
cout << "finish consume cnt:" << (long long)ret << endl;
pthread_mutex_unlock(&s_printmutex);
pthread_exit(0); //多线程编程时,主线程退出最好用pthread_exit。注意return和pthread_exit的区别。
}
讲解
1.代码11-37行,声明结构体MsgVec,里面的m_vec是生产者线程和消费者线程需要并发读写的容器。 m_mutex是保护m_vec的互斥锁,m_pwcond是是否可写的条件变量,m_rcond是是否可读的条件变量。pthread_mutex_t,pthread_cond_t,pthread_attr_t等使用前需要先初始化。初始化可以使用XXX_INITIALIZER进行赋值或者使用xxx_init()函数,如果是动态申请的变量就得使用xxx_init()函数并且最后释放需要调用xxx_destroy()函数。如果不是动态申请的xxx_destroy()调用不是必须的。
2.代码152-153行,创建10个生产者线程。传递vec的指针做为生产者线程启动函数Produce的参数。pthread_create函数的pthread_attr_t*参数指定为NULL,表示使用默认属性进行创建线程。
3.代码155-158行,创建1个消费者线程。传递vec的指针做为消费者线程启动函数Consume的参数。演示了通过pthread_attr_t指定线程的栈大小为4M。
4.代码159行,pthread_setconcurrency函数,通知系统其所需的并发级别。pthread_setconcurrency函数设定的并发度只是对系统的一个提示,系统并不保证请求的并发度一定会采用。
5.代码160-161行,通过ret接收pt_consumer线程返回的数据地址。Consume函数把数据元素个数强转为地址进行返回。如果有多个信息需要返回,那就把需要返回的信息声明到一个结构体中,然后Consume函数中动态申请这个结构体最后把该结构体的地址进行返回。注意不要返回栈上地址,因为线程结束后,栈被销毁,再访问销毁的栈地址是不合法的。
6.代码163-165行,打印消费者线程消费的元素个数。使用s_printmutex对输出进行保护,使本文中的各输出信息不会出现串行。
7.代码65行,线程通过调用pthread_detach使自己分离,从而其他线程不再需要进行pthread_join进行该线程结束时的资源清理。使线程分离可以调用pthread_detach或者创建线程时指定分离属性。可以通过调用pthread_attr_setdetachstate把分离属性设置到属性变量中。
8.代码68行,通过pthread_once创建生产者线程的私有数据key和对应的资源清理函数。pthread_once能确保所有线程调用但是只执行1次。一般用于初始化操作。
9.代码48-55行,函数FreeKeyValue是pthread_once初始化时设置的线程私有数据清理函数,程序结束时会打印出10条生产者线程的生产数据信息,并正确释放之前动态申请的Pthread_info内存。
10.代码69-77行,通过pthread_getspecific和pthread_setspecific获取和设置线程的私有数据。10条生产者线程第一次调用pthread_getspecific都会反回NULL,然后设置各自的私有数据。
11.代码79-101行,生产者线程不间断的测试生产条件,如果生产条件满足就往vector中插入10个数据,如果不满足生产条件就在pVec->m_pwcond这个条件变量上进行等待。
pthread_cond_wait用于阻塞当前线程,等待别的线程使用pthread_cond_signal()
或pthread_cond_broadcast
来唤醒它 pthread_cond_wait()
必须与pthread_mutex
配套使用。pthread_cond_wait()
函数一进入wait
状态就会自动release mutex
。当其他线程通过pthread_cond_signal()
或pthread_cond_broadcast
,把该线程唤醒,使pthread_cond_wait()
通过(返回)时,该线程又自动获得该mutex
。pthread_cond_wait常与while条件监测搭配使用,使用while是避免[假唤醒].下面代码展示了常用的使用方法。
pthread_mutex_lock while(条件) pthread_cond_wait
.....操作临界区资源.....
pthread_cond_signal(1)可放在mutex锁内 pthread_mutex_unlock pthread_cond_signal(2)也可以放在mutex锁外
代码82-87行,进行while生产条件监测,如果线程被唤醒后不满足条件就累计pThreaddata->m_fakewake++的[假唤醒]次数。如果消费者是使用pthread_cond_broadcast来唤醒的那生产者线程将产生大量的假唤醒次数。因为pthread_cond_broadcast会唤醒所有消费者线程,当第PRODUCE_BUFF_SIZE/10个线程插入数据后,第PRODUCE_BUFF_SIZE/10+1个线程通过竞争取得pVec->m_mutex锁后退出pthread_cond_wait函数,此时pVec->m_vec.size() 已经等于 PRODUCE_BUFF_SIZE了,所以此次唤醒是[假唤醒]。pthread_cond_signal可以放在mutex锁内也可以放在mutex锁外。假设在线程A的mutex锁内调用
pthread_cond_signal,唤醒了等待线程B。在线程A退出mutex锁前,操作系统调度到线程B执行,线程B此时在pthread_cond_wait内被唤醒,退出pthread_cond_wait时需要竞争再次锁上mutex,因为线程A还没释放所有又会导致线程B再被等待。从而会浪费一次线程调度切换。
12.代码113-138行,消费者线程不断进行消费条件监测,如果满足条件就清空vector的数据。如果条件不满足就等待在
pVec->m_rcond上。能使用pthread_cond_signal的地方,替换为pthread_cond_broadcast总不会错,如果难以决定用哪个唤醒时,就使用broadcast方式。一个mutex可以对应多个条件,但是一个cond只能对应一个mutex。本例子演示了一个mutex对应了一个读条件和一个写条件。
13.代码166行,主线程使用pthread_exit结束,而不是return 0;这样能避免子线程所有输出未完成时,进程退出。pthread_exit和return的区别可见博文线程使用pthread_exit和return结束的区别_yadoufeng的博客-CSDN博客
运行结果
pthread:0x700001abf000 produce cnt:1020 fake wake cnt:99 use time:11510
pthread:0x700001dd1000 produce cnt:1140 fake wake cnt:108 use time:11270
pthread:0x700001bc5000 produce cnt:1080 fake wake cnt:85 use time:11483
pthread:0x700001ccb000 produce cnt:890 fake wake cnt:110 use time:11531
pthread:0x700001d4e000 produce cnt:850 fake wake cnt:96 use time:11457
pthread:0x700001ed7000 produce cnt:650 fake wake cnt:88 use time:11101
pthread:0x700001b42000 produce cnt:1390 fake wake cnt:63 use time:11526
pthread:0x700001e54000 produce cnt:990 fake wake cnt:96 use time:11162
pthread:0x700001c48000 produce cnt:1170 fake wake cnt:90 use time:11522
pthread:0x700001f5a000 produce cnt:820 fake wake cnt:91 use time:11022
finish consume cnt:10000
Program ended with exit code: 0