现实应用场景中经常会用到单生产者单消费者或多生产者多消费者模型,例如一个写线程,接收到数据写入缓冲区,另一个线程读出并处理。为了尽可能减少锁所占用的时间,可使用gcc的一些原子操作来代替pthread_mutex_t或pthread_spinlock_t。
核心思想:预分配一段连续的内存,用min_seq_表示读的位置,max_seq_表示写的位置,当写满或者读完时返回错误;如果不同步有可能发生的错误是:读线程读取max_seq_时,刚好写线程在修改max_seq_,min_seq_同上,故使用__sync_add_and_fetch 函数自增0或者1来读取或者修改变量。
注意事项:分配的内存大小一定要足够大,否则可能会造成数据的丢失。
就好比一件仓库,有人不停往里存放东西,有人不停往外取,当仓库较小时,空间满时,可能会出现存放失败,那么这件商品就丢失了。
实现如下:
#ifndef SPSC_H
#define SPSC_H
//single producer single consumer
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
//if Data is fixed length, you can use Data directly; else you must use Data*
template<typename Data>
class Spsc
{
public:
Spsc():buf_(NULL), size_(0), max_seq_(0), min_seq_(0){}
Spsc(int size):buf_(NULL), size_(size), max_seq_(0), min_seq_(0){}
~Spsc()
{
if(NULL != buf_)
{
free(buf_);
buf_ = NULL;
}
}
bool Init();
bool Init(int size);
bool Push(Data& data);
//note:if data is pointer type, don't forget delete it
bool Pop(Data& data);
private:
char* buf_;
int size_;
uint64_t max_seq_;//push seq
uint64_t min_seq_;//pop seq
};
template<typename Data>
bool Spsc<Data>::Init()
{
if(size_ <= 0)
return false;
buf_ = (char*)malloc(size_*sizeof(Data));
return NULL != buf_;
}
template<typename Data>
bool Spsc<Data>::Init(int size)
{
size_ = size;
return Init();
}
template<typename Data>
bool Spsc<Data>::Push(Data& data)
{
uint64_t min_seq_tmp = __sync_add_and_fetch (&min_seq_, 0);
if((int)(max_seq_ - min_seq_tmp) >= size_)
return false;
int pos = max_seq_ % size_;
memcpy(buf_+pos*sizeof(Data), &data, sizeof(Data));
__sync_add_and_fetch(&max_seq_, 1);
return true;
}
template<typename Data>
bool Spsc<Data>::Pop(Data& data)
{
uint64_t max_seq_tmp = __sync_add_and_fetch (&max_seq_, 0);
if(min_seq_ >= max_seq_tmp)
return false;
int pos = min_seq_ % size_;
memcpy(&data, buf_+pos*sizeof(Data), sizeof(Data));
__sync_add_and_fetch(&min_seq_, 1);
return true;
}
#endif // SPSC_H
main.cpp如下:
#include "spsc.h"
#include <pthread.h>
#include <iostream>
#include <sys/time.h>
#include <unistd.h>
Spsc<uint64_t> spsc_test;
void* WriteThread(void* )
{
uint64_t value = 10000;
int wait_value = 0;
timeval start_tv, end_tv;
gettimeofday(&start_tv, NULL);
while(1)
{
gettimeofday(&end_tv, NULL);
if((end_tv.tv_sec-start_tv.tv_sec)*1000*1000+(end_tv.tv_usec-start_tv.tv_usec) >= 100*1000*1000)
{
std::cout << "last value:" << value << ", wait value:" << wait_value << std::endl;
return NULL;
}
if(spsc_test.Push(value))
{
++value;
}
else
{
timeval tv{0, 1};
select(0, NULL, NULL, NULL, &tv);
++wait_value;
}
}
return NULL;
}
void* ReadThread(void* )
{
uint64_t last_value = 9999;
uint64_t cur_value;
int wait_value = 0;
timeval start_tv, end_tv;
gettimeofday(&start_tv, NULL);
while(1)
{
gettimeofday(&end_tv, NULL);
if((end_tv.tv_sec-start_tv.tv_sec)*1000*1000+(end_tv.tv_usec-start_tv.tv_usec) >= 100*1000*1000)
{
std::cout << "last value:" << cur_value << ", wait value:" << wait_value << std::endl;
return NULL;
}
if(spsc_test.Pop(cur_value))
{
if(cur_value - last_value == 1)
{
last_value = cur_value;
}
else
{
std::cout << "an error happened at Pop" << std::endl;
}
}
else
{
timeval tv{0, 1};
select(0, NULL, NULL, NULL, &tv);
++wait_value;
}
}
return NULL;
}
int main(void)
{
if(!spsc_test.Init(100000))
{
std::cout << "Init failed" << std::endl;
}
pthread_t write_thread, read_thread;
pthread_create(&write_thread, NULL, WriteThread, (void *)&spsc_test);
pthread_create(&read_thread, NULL, ReadThread, (void *)&spsc_test);
pthread_join(write_thread, NULL);
pthread_join(read_thread, NULL);
sleep(1);
return 0;
}
运行100s,平均1us value增加的值,也就是运行的次数分别是:
原子操作:10次
使用自旋锁时:6-7次
不加锁时:16次,当然这种情况很有可能出现异常。