一 线程池实现
线程池,内存池,进程池都是在用空间换时间,因为我们进程在运行时可能受到大量请求,这个时候要创建线程去运行,如果是来一个任务才创建线程,此时完成任务的时间就要算上创建线程,可是如果我们在没任务时候提前创建好线程,就能比较快的响应请求,今天我们就聊聊线程池的小实现。
其实还是一种变形的生产消费模型。客户端发任务到消息队列,线程池从消息队列拿任务去执行,显然超市就是任务队列,客户端是生产者,线程池是消费者,应该算是单生产多消费的模型。
大致代码文件如下。
1 main.cc
可以先往队列中push一个数字,看看程序跑起来有没有问题,没问题再将数字改成一个Task类。
2 线程池实现
一个vp_用于保存多线程的id,tasks是个任务队列,多线程并发访问从中获得任务。
任务队列我们就设计成阻塞队列。线程同步解析-优快云博客 这篇博客中曾提及阻塞队列和环形队列,它们在访问队列的设计时不一样的,这就导致实现同步的方式不一样,例如环形队列的空间是固定开辟好的,不方便判空和判满,如果用条件变量来实现同步,判空判满会很麻烦,大家可以下去试试。
我们阻塞队列实现同步用条件变量,而且是两个条件变量,生产者和消费者不可以共用一个条件变量的,这个在上面那篇博客中也提过。我试想了一下用信号量不太行,因为我们在阻塞队列是直接Push和pop的,如果pop将队列变为空,此时空间信号量和数据信号量都归零了,所以如果要用信号量就得像环形队列那样访问队列。
阻塞队列中生产者和消费者无法并发访问任务队列,所以只有一把锁,因为我们是复用了stl容器的push和pop,这个是线程不安全的,必须要加锁使用,环形队列没有复用stl的接口而且配合信号量,所以才可以用两把锁,可以让push和pop并发。下面是一些基本函数封装
#define NUM 5
template<class T>
class threadPool
{
public:
threadPool(int size = NUM)
:vp_(size)
{
pthread_mutex_init(&mutex_,nullptr);
pthread_cond_init(&Consumer,nullptr);
pthread_cond_init(&Productor,nullptr);
}
~threadPool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&Consumer);
pthread_cond_destroy(&Productor);
}
void Lock()
{
pthread_mutex_lock(&mutex_);
}
void Unlock()
{
pthread_mutex_unlock(&mutex_);
}
bool Full()
{
return tasks_.size() == NUM;
}
bool Empty()
{
return tasks_.size() == 0;
}
std::queue<T> tasks_; 任务队列
std::vector<pthread_t> vp_; 线程池,不能存指针,内部要解引用访问的
pthread_mutex_t mutex_;
pthread_cond_t Consumer;
pthread_cond_t Productor;
};
线程执行函数
static void* threadRun(void* arg) 这个函数必须为静态的,否则参数会不匹配
{
线程分离,懒得join了
pthread_detach(pthread_self());
threadPool<T>* tp = static_cast<threadPool<T>*> (arg);
取出一个数据进行读取或者取一个任务来执行
T data;
tp->pop(data);
std::cout<<data.getformat()<<std::endl;
执行任务
data();
std::cout<<data.getRformat()<<"id: "<<pthread_self()<<std::endl;
}
void start()
{
for(int i = 0; i < NUM; i++) 创建多线程
{
pthread_create(vp_[i],nullptr,threadRun,this);
为了threadRun可以访问类内成员,只能把this传入了
}
}
push和pop函数介绍,push函数由于我们是单个生产者,可以加锁也可以不加。
void push(const T&data)
{
Lock();
while(tp->Full())
{
pthread_cond_wait(&tp->Productor,&tp->mutex_);
}
tasks_.push(data);
pthread_cond_signal(&Consumer); 唤醒消费者
Unlock();
}
防止多个线程并发pop,要加锁控制
void pop(T& data)
{
Lock();
//检查是否有任务
while(tp->Empty())
{
pthread_cond_wait(&tp->Consumer,&tp->mutex_);
}
data = tasks_.front();
pthread_cond_signal(&Productor);
Unlock();
}
这就是所有的代码了,不过还可以改进,因为我之前写过对锁和创建线程的原生线程库接口进行封装,可以添加到这里。
二 线程池优化
class Mutex
{
public:
Mutex(pthread_mutex_t* lock)
:lock_(lock)
{
;
}
~Mutex()
{
;
}
void Lock()
{
pthread_mutex_lock(lock_);//加锁
}
void Unlock()
{
pthread_mutex_unlock(lock_);//解锁
}
private:
pthread_mutex_t* lock_;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t* lock)
:mt_(lock)
{
mt_.Lock();
}
~LockGuard()
{
mt_.Unlock();
}
private:
Mutex mt_;
};
class Thread
{
public:
typedef enum
{
NEW = 1,
RUNING,
EXIT
}status;
typedef void* (*fun_t)(void*);
Thread()
{
;
}
Thread(int num, fun_t fun, void* arg)
:id_(0),fun_(fun),arg_(arg),status_(NEW)
{
name_ = "thread->" + std::to_string(num);
}
~Thread()
{
;
}
static void * threadRun(void*arg)
{
Thread* th = (Thread*) arg;
th->fun_(th->arg_);
return nullptr;
}
void Run()
{
int n = pthread_create(&id_,nullptr,threadRun,(void*)this);
if(n != 0)//成功返回0,不成功返回错误码
exit(4);
status_ = RUNING;
}
void join()
{
pthread_join(id_,nullptr);
status_ = EXIT;
}
std::string getname()
{
return name_;
}
int getstatus()
{
return status_;
}
pthread_t getid()
{
if(status_ == RUNING)
return id_;
else
{
std::cout<<name_<<" not create ";
return 1;
}
}
pthread_t id_;
std::string name_;//线程名
status status_;//线程状态
fun_t fun_;//线程执行函数
void* arg_;//线程参数
};
template <class T>
class threadPool
{
public:
threadPool(int size = NUM) // vp_存的自定义类型要有默认构造,不然这里初始化会找不到默认构造!
: vp_(size)
{
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&Consumer, nullptr);
pthread_cond_init(&Productor, nullptr);
}
void init()
{
for (int i = 0; i < NUM; i++) 复用Thread类,将线程执行函数和this指针传入
用来给Thread类内访问threadPool的静态成员函数threadRun。
{
vp_[i] = (Thread(i, threadRun, this));
}
}
void Lock()
{
pthread_mutex_lock(&mutex_);
}
void Unlock()
{
pthread_mutex_unlock(&mutex_);
}
static void *threadRun(void *arg)
{
threadPool<T> *tp = static_cast<threadPool<T> *>(arg);
while (true)
{
T data;
tp->pop(data);
std::cout << data.getformat() << std::endl;
data();
std::cout << data.getRformat() << "id: " << pthread_self() << std::endl;
}
}
bool Full()
{
return tasks_.size() == NUM;
}
bool Empty()
{
return tasks_.size() == 0;
}
void start()
{
for (auto &e : vp_) 复用Thread Run方法创建线程
{
e.Run();
}
}
~threadPool()
{
for (auto &e : vp_)复用Thread join方法回收线程
{
e.join();
}
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&Consumer);
pthread_cond_destroy(&Productor);
}
void push(const T &data)
{
// Lock();
{
LockGuard lg(&mutex_); 复用LockGuard类进行加锁
while (Full())
{
pthread_cond_wait(&Productor, &mutex_);
}
tasks_.push(data);
}
pthread_cond_signal(&Consumer);
// Unlock();
}
void pop(T &data)
{
// Lock();
{
LockGuard lg(&mutex_);
// 检查是否有任务
while (Empty())
{
pthread_cond_wait(&Consumer, &mutex_);
}
data = tasks_.front();
tasks_.pop();
}
pthread_cond_signal(&Productor);
// Unlock();
}
std::queue<T> tasks_;
std::vector<Thread> vp_;
pthread_mutex_t mutex_;
pthread_cond_t Consumer;
pthread_cond_t Productor;
};
三 单例线程池
因为我们想让程序中只有一个线程池,先了解了解懒汉,饿汉模式先,懒汉模式是等你要使用时再创建,而饿汉模式则是立刻创建,使用时直接用。我们下面就实现一个懒汉模式。
template <class T>
class threadPool
{
private:
threadPool(int size = NUM)
: vp_(size)
{
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&Consumer, nullptr);
pthread_cond_init(&Productor, nullptr);
}
void init()
{
for (int i = 0; i < NUM; i++)
{
vp_[i] = (Thread(i, threadRun, this));
}
}
~threadPool()
{
for (auto &e : vp_) // 复用Thread join方法回收线程
{
e.join();
}
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&Consumer);
pthread_cond_destroy(&Productor);
}
static threadPool<T>* getthreadPool()
{
if(nullptr == tp_)
{
LockGuard lg(&Poolmutex_);
if (tp_ == nullptr)
{
tp_ = new threadPool<T>;
tp_->init();
tp_->start();
}
}
return tp_;
}
std::queue<T> tasks_;
std::vector<Thread> vp_;
pthread_mutex_t mutex_;
pthread_cond_t Consumer;
pthread_cond_t Productor;
static threadPool<T>* tp_;
static pthread_mutex_t Poolmutex_;
};
template<class T>
threadPool<T>*threadPool<T>::tp_ = nullptr;
template<class T>
pthread_mutex_t threadPool<T>::Poolmutex_ = PTHREAD_MUTEX_INITIALIZER;
构造函数,拷贝,赋值私有,这样就不能new,不能创建静态对象,也不能在栈上创建对象了,析构可以私有,那就得再实现一个静态函数Destroy,内部封装delete tp_,就可以释放资源了。
让外部获取该对象,显然这里的if判断会有线程安全的问题,等会解决。
然后还要初始化和start一下。
加锁保护,那就要搞一把静态锁了,不然静态成员函数怎么访问呢?创建对象?我们现在可是在搞单例啊。
但是没必要都加锁,当对象已经创建好了,直接返回就好了,就没必要获取锁了,可以外层再套个if,这个if不用担心线程安全。
四 常见锁介绍
悲观锁,一旦可能出现线程安全就加锁,总是悲观地认为其它线程会修改自己的数据,所以我们上面实现的基本上都是悲观锁,乐观锁,认为其它线程不会修改数据,不加锁,但是在更新数据的时候判断数据有没有被修改,如何判断呢,1 采用版本号,也就是读取数据的时候会伴随着读取它的版本号,如果修改了数据内容,就要更改版本号,这样如果有人拿着1.0版本的数据算出来的结果,要写入时发现数据的版本号已经变成2.0了,就不能写了,此时会重新读取数据,重新计算,然后再写入。2 采用CAS,也就是会将先前取得的数据和内存数据做对比,相等则用新数据更新,不相等则不断重试。
自旋锁:自旋锁就是申请不到,不是去阻塞,而是继续申请,因为有可能锁很快就可以给你了,但是你却非要去阻塞,为了效率就设计出不让你去阻塞,而是让你继续申请的锁。同理如果是直接去阻塞的说明这个场景下并不会很快申请到锁,
函数如下图,trylock函数是非阻塞式,可是lock不才是非阻塞式一直申请吗,因为我们感知不到轮询这个过程,我们只知道线程一直卡在lock函数处,我们以为它在阻塞,实际上是在轮询,非阻塞就是不轮询,直接返回。
读写锁
读加锁
写加锁
读者写者问题分析,首先读者和读者是无关系的,因为读者不修改数据,真的只是读,但是cp问题中的的读者是要拿走数据的,这两种读不是一个概念,还有读和写也是互斥和同步关系,写和写则是互斥。至于如何使用这些函数,很简单,读的时候就用读加锁,写的时候就用写加锁呗。