一、线程池
使用多线程时要注意传参传递堆空间指针变量;
平常定义的缓冲区就是一个简单的数据池;malloc的底层调用了系统调用来申请堆空间是有成本的,如:需要使用页表和MMU将虚拟地址和物理地址建立映射,期间会触发缺页中断;
使用池化技术,直接提前开辟好空间,用户层维护,就不需要交给操作系统提高效率,达到以空间换时间;
线程池就是池化技术的一个分支,提前创建好一批线程和一个任务队列,由主线程push任务,新线程们竞争pop任务,本质上就是一个CP模型;
1.1设计线程池
思路:对线程信息和任务信息创建类,然后用vector组织线程信息和用queue组织任务信息;
任务队列是一个通信场所,多个新线程是消费者,主线程是生产者;
注意:1.线程的执行函数参数和返回值是固定的,不可以写成普通成员函数,因为this指针传递实际上是传递了两个参数,必须是设计成静态成员函数;2.因为静态成员函数的内部要使用成员属性,所以可以使用第四个参数传递this;3.为了满足高并发,处理数据和生产数据不应该加锁;
c++
#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <pthread.h>
#include <queue>
#include <unistd.h>
struct threadifo
{
pthread_t tid;
std::string name;
};
template <class T>
class threadpool
{
static const int defaultnum = 10;
private:
void lock()
{
pthread_mutex_lock(&mutex_);
}
void unlock()
{
pthread_mutex_unlock(&mutex_);
}
void wait()
{
pthread_cond_wait(&cond_, &mutex_);
}
void signal()
{
pthread_cond_signal(&cond_);
}
public:
threadpool(int num = defaultnum) : threads_(defaultnum)
{
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
~threadpool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
static void *handlerTask(void *args)
{
threadpool *tp = static_cast<threadpool *>(args);
std::string name;
for (auto e : tp->threads_)
{
if (e.tid == pthread_self())
{
name = e.name;
}
}
while (true)
{
tp->lock();
while (tp->tasks_.empty())
{
tp->wait();
}
T t = tp->pop();
tp->unlock();
std::cout << name << " ";
t();
}
return nullptr;
}
void start()
{
int num = threads_.size();
for (int i = 0; i < num; i++)
{
threads_[i].name = "Thread-" + std::to_string(i + 1);
pthread_create(&(threads_[i].tid), nullptr, handlerTask, (void *)this);
}
}
void push(const T &value)
{
lock();
tasks_.push(value);
signal();
unlock();
}
T pop()
{
T t = tasks_.front();
tasks_.pop();
return t;
}
private:
std::vector<threadifo> threads_;
std::queue<T> tasks_;
pthread_mutex_t mutex_;
pthread_cond_t cond_;
};
二、线程的封装
使用包装器设置回调方法,传递任意函数,而不仅限于void*(void*);除了传递函数名,也可以传递函数参数;
c++
#pragma once
#include <pthread.h>
#include <iostream>
#include <ctime>
#include <string>
#include <functional>
using callback_t = std::function<void(int, int)>;
class thread
{
static int num;
private:
static void *routine(void *args)
{
thread *td = static_cast<thread *>(args);
td->func_(td->args1_, td->args2_);
return nullptr;
}
public:
thread(callback_t func, int agrs1, int args2) : tid_(0), starttime_(0), isrunning_(false), func_(func), args1_(agrs1), args2_(args2) {}
void run()
{
name_ = "Thread-" + std::to_string(num++);
starttime_ = time(nullptr);
isrunning_ = true;
pthread_create(&tid_, nullptr, routine, this);
}
void join()
{
pthread_join(tid_, nullptr);
isrunning_ = false;
}
bool isrunning()
{
return isrunning_;
}
uint64_t getstarttime()
{
return starttime_;
}
std::string getname()
{
return name_;
}
~thread() {}
private:
pthread_t tid_;
std::string name_;
uint64_t starttime_; // 启动时间戳
bool isrunning_;
callback_t func_;
int args1_;
int args2_;
};
int thread::num = 1;
三、STL,智能指针和线程安全
STL中的容器并不是线程安全的;大部分智能指针是线程安全的;
四、线程安全的单例模式
延时加载可以最大程度的提高启动速度;全局变量或者对象在程序加载时就已经创建好了并且生命周期是随进程的;
单例模式就是指整个类最终只能实例化一个对象;
有两种实现模式:1.饿汉模式;2.懒汉模式;
懒汉模式通过延时加载的方式,优化了服务器的启动速度;
将线程池改成懒汉模式:
注意:1.多线程中单例模式是由线程安全问题的,需要对静态变量加锁,并且需要使用的是全局锁,而互斥锁的全局或者静态初始化方式恰好满足这样的场景;2.但是又会引发新的问题,比如实际上只有第一次使用单例才需要进行临界资源的判断,如果加锁就导致每次线程都要串行执行去加锁解锁访问临界资源,但是实际上临界区的代码只会执行一次,之后一直做无效的事情,这样就降低了程序执行的效率;可以再套一层判断,可能会进来一批,但是内部加锁了,最多只能有一个访问,当new成功之后,即使其他线程竞争所成功也立刻判断不满足条件解锁,后面的线程就再也不会进入加锁逻辑,这样既保证了线程安全又可以提高效率;
总结:对于单例模式,1.加锁判断临界资源;2.在外部套一层判断;
c++
template <class T>
class threadpool
{
public:
static threadpool<T> *Getinstance(int num = defaultnum)
{
if (nullptr == tp_)
{
pthread_mutex_lock(&lock_);
if (tp_ == nullptr)
{
tp_ = new threadpool<T>(num);
}
pthread_mutex_unlock(&lock_);
}
return tp_;
}
private:
threadpool(int num = defaultnum) : threads_(defaultnum)
{
pthread_mutex_init(&mutex_, nullptr);
pthread_cond_init(&cond_, nullptr);
}
~threadpool()
{
pthread_mutex_destroy(&mutex_);
pthread_cond_destroy(&cond_);
}
threadpool(const threadpool &args) = delete;
threadpool &operator=(const threadpool &args) = delete;
};
template <class T>
threadpool<T> *threadpool<T>::tp_ = nullptr;
五、常见的各种锁
悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。如:互斥锁和信号量;
乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。 即允许其他线程修改但是当前线程会进行数据的修正;
CAS(compare and swap)操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。原子的;
5.1自旋锁
自旋锁不挂起而是周而复始不断地申请锁,成功进入临界区,失败返回并且立刻重新申请锁;
实现自旋锁:
c++
while(true){
int ret = pthread_mutex_try_lock(&mutex);
if(ret == 0)//成功退出循环,失败继续循环
{
break;
}
}
自旋锁接口:
c++
#include <pthread.h>
int pthread_spin_lock(pthread_spinlock_t *lock);//底层加了循环,申请失败了上层感觉就是阻塞
int pthread_spin_unlock(pthread_spinlock_t *lock);
int pthread_spin_trylock(pthread_spinlock_t *lock);//和互斥锁的try_lock是一样的
int pthread_spin_destroy(pthread_spinlock_t *lock);
int pthread_spin_init(pthread_spinlock_t *lock, int
pshared);
//第二个参数是是否共享,默认为0;
选择挂起等待锁还是自旋锁完全取决于临界区执行时间的长短;
当考虑线程在临界区的时间时,如果执行时间短,则其他进程选择自选方式,如果执行时间长,其它线程选择挂起等待;因为挂起等待也是有时间成本的,执行时间短却选择了挂起等待方式,就会导致频繁地进行挂起和唤醒操作,而自选方式频繁地竞争,但是时间短可能比起挂起等待耗费的时间还要少,效率更高;执行时间长选择挂起等待则较为合适;
六、读者写者问题
6.1读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。
使用读写锁解决问题;分析是要注意三种关系,两种角色,一个通信场所;
读者之间:共享;读者不会修改数据;
写者之间:互斥加竞争;
读写者之间:互斥加同步;
与CP模型相比实现时不要对读者之间进行同步与互斥;
使用读写锁其实就可以实现读者写者问题,接口如下:
对于读写锁,锁处于无锁时,读写锁请求都可以申请;锁处于读锁时,读锁可以申请,写锁一定失败;锁处于写锁时,读写锁都不可以申请;
c++
//默认读者优先可以修改
#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
6.2模型理解
一般时读者多写者少,表现出来就是读者竞争能力强,写者会存在饥饿问题;但是此现象在读者写者中是正常的;所以可以设计同步策略如:读者优先(读者读完了,写者才进入)、写者优先(当写者到了,先写者执行);
用互斥锁模拟读写锁,读者之间可以看到读者计数器和两个互斥锁分别用于读写互斥,读者由于修改读者计数器所以要加锁访问,当计数器为1时,读者申请写者锁;当前读者线程申请写者互斥锁成功,如果写者在临界区,调度回来时就会阻塞,不在就无法访问临界区也阻塞;此时就可以读者读取共享,结束后判断计数器共享资源需要加锁,当最后一个读者线程时,再将写者锁释放,这样写者才能执行,实现读者优先;
c++
//读者优先
int rcount=0;
mutex_t rlock,wlock;
lock(&rlock);
rcount++;
if(rcount==1)
lock(&wlock);
unlock(&rlock);
//共享读取
lock(&rlock);
rcount--;
if(rcount==1)
unlock(&wlock);
unnlock(&rlock);
//写者
lock(&wlock);
//写入
unlock(&wlock);