1.线程概念及其用途和优缺点
2.互斥量(锁)
3.条件变量
4.POSIX信号量
5.生产者消费者模型
6.线程池
7.线程安全的单例模式
1.线程概念及其用途和优缺点
概念:线程是比进程更轻量化的一种执行流,是在进程内部执行的一种执行流。线程是CPU调度的基本单位,进程是承担系统资源的基本实体。注意,在Linux中,没有真正意义上的线程,都是通过使用轻量化进程模拟出来的。
优点:创建一个新线程的代价要比创建一个新进程小得多;
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多;
线程占用的资源要比进程少很多;
能充分利用多处理器的可并行数量;
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务;
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现;
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
缺点:性能损失, 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型 线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的 同步和调度开销,而可用的资源不变。
健壮性降低, 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了 不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制, 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高, 编写与调试一个多线程程序比单线程程序困难得多。
用途:合理的使用多线程,能提高CPU密集型程序的执行效率 ;合理的使用多线程,能提高IO密集型程序的用户体验。
线程函数:
创建线程:int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void * (*start_routine)(void*), void *arg);
参数: thread:返回线程ID ;attr:设置线程的属性,attr为NULL表示使用默认属性 ;start_routine:是个函数地址,线程启动后要执行的函数; arg:传给线程启动函数的参数; 返回值:成功返回0;失败返回错误码。
获取线程自身ID:pthread_t pthread_self(void);
线程终止:void pthread_exit(void *value_ptr);
参数: value_ptr:value_ptr不要指向一个局部变量。 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。
取消一个执行中的线程:int pthread_cancel(pthread_t thread);
参数 thread:线程ID; 返回值:成功返回0;失败返回错误码。
等待线程结束:int pthread_join(pthread_t thread, void **value_ptr);
参数: thread:线程ID; value_ptr:它指向一个指针,后者指向线程的返回值; 返回值:成功返回0;失败返回错误码。
分离线程:int pthread_detach(pthread_t thread);
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放 资源,从而造成系统泄漏。 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
#include<cstdlib>int ticket=10000;
void*geticket();
void*Function(void*arg)
{geticket();
}void*geticket()
{while(true){if(ticket>0){usleep(1000);cout<<pthread_self()<<": get a ticket: "<<ticket<<endl;ticket--;}else{break;}}
}int main()
{pthread_t id1,id2;pthread_create(&id1,nullptr,Function,(void*)"thread1");pthread_join(id1,nullptr);pthread_detach(id1);return 0;
}
2.互斥量(锁)
临界资源:多线程执行流共享的资源就叫做临界资源。
临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。
互斥量mutex :大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个 线程,其他线程无法获得这种变量。 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之 间的交互。 多个线程并发的操作共享变量,会带来一些问题。
例如在上面的代码中,若是使用多个线程进行geticket就会出现ticket出现负数的情况,为防止出现这种情况,我们需要一把锁即互斥量。
互斥量具有以下作用:当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临 界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。
互斥量函数:
初始化互斥量:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 静态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 动态分配
参数: mutex:要初始化的互斥量; attr:NULL
销毁互斥量:int pthread_mutex_destroy(pthread_mutex_t *mutex);
静态初始化的互斥量不需要销毁;不要销毁一个已经加锁的互斥量。
互斥量加锁和解锁:int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号。
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量, 那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
#include<pthread.h>
#include<unistd.h>
#include<cstdio>
#include<cstdlib>pthread_mutex_t mutex=PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
int ticket=10000;
void*geticket();
void*Function(void*arg)
{geticket();
}void*geticket()
{while(true){pthread_mutex_lock(&mutex);if(ticket>0){usleep(1000);cout<<pthread_self()<<": get a ticket: "<<ticket<<endl;ticket--;pthread_mutex_unlock(&mutex);}else{break;}}
}int main()
{pthread_t id1,id2;pthread_create(&id1,nullptr,Function,(void*)"thread1");pthread_create(&id2,nullptr,Function,(void*)"thread2");pthread_join(id1,nullptr);pthread_join(id2,nullptr);pthread_detach(id1);pthread_detach(id2);pthread_mutex_destroy(&mutex);return 0;
}
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。
常见的线程安全的情况:每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的; 类或者接口对于线程来说都是原子操作; 多个线程之间的切换不会导致该接口的执行结果存在二义性。
可重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们 称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数。
常见的可重入情况:不使用全局变量或静态变量; 不使用用malloc或者new开辟出的空间; 不调用不可重入函数; 不返回静态或全局数据,所有数据都有函数的调用者提供; 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。
线程安全与否,描述的是线程的状态。可/不可重入,描述的是函数的特点。
可重入函数是线程安全函数的一种,线程安全函数不一定是可重入函数。
死锁:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资 源而处于的一种永久等待状态。
死锁的四个必要条件:互斥条件:一个资源每次只能被一个执行流使用。
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。
避免死锁及其算法:破坏死锁的四个必要条件; 加锁顺序一致; 避免锁未释放的场景; 资源一次性分配;死锁检测算法; 银行家算法。
3.条件变量
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问 题,叫做同步。
条件变量:当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情 况就需要用到条件变量。
条件变量函数:
初始化条件变量:int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数: cond:要初始化的条件变量; attr:NULL
销毁条件变量:int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满足:int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
参数: cond:要在这个条件变量上等待; mutex:互斥量。
唤醒等待:int pthread_cond_broadcast(pthread_cond_t *cond);唤醒全部
int pthread_cond_signal(pthread_cond_t *cond);随机唤醒一个
wait函数执行后,首先会释放mutex锁,并使调用线程进入阻塞状态和等待cond被触发,这个mutex锁会被其他线程锁住,等到signal函数被调用后,会发送信号给等待cond的线程,mutex被再次释放。
注意:条件变量一般与互斥量一起使用,因为条件变量可以通过允许线程阻塞和等待另一个线程发送信号来弥补互斥量的不足。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
void *r1( void *arg )
{ while ( 1 ){ pthread_cond_wait(&cond, &mutex); printf("活动\n"); }
}
void *r2(void *arg )
{ while ( 1 ) { pthread_cond_signal(&cond); sleep(1); }
}
int main( void )
{ pthread_t t1, t2; pthread_cond_init(&cond, NULL); pthread_mutex_init(&mutex, NULL); pthread_create(&t1, NULL, r1, NULL); pthread_create(&t2, NULL, r2, NULL); pthread_join(t1, NULL); pthread_join(t2, NULL); pthread_mutex_destroy(&mutex); pthread_cond_destroy(&cond);
}
4.POSIX信号量
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
注意:信号量本质是一把计数器,申请信号就是预约资源。
POSIX函数:
初始化信号量:#include<semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数: pshared:0表示线程间共享,非零表示进程间共享; value:信号量初始值
销毁信号量:int sem_destroy(sem_t *sem);
等待信号量并会将信号量的值减1: int sem_wait(sem_t *sem); //P()
发布信号量,表示资源使用完毕,信号量加1:int sem_post(sem_t *sem);//V()
P()V()操作是原子的。
5.生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者 要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队 列就是用来给生产者和消费者解耦的。
优点:解耦 ;支持并发,提高效率; 支持忙闲不均。
“三种关系,两个角色,一个场所”:
三种关系:生产者之间的关系是互斥关系;消费者之间的关系是互斥关系;生产者和消费者之间的关系是互斥和同步关系。
两个角色:生产者和消费者。
一个场所:内存空间。
生产者消费者模型一般有两种:其一是基于BlockingQueue的生产者消费者模型,其一是基于环形队列(RingQueue)的生产者消费者模型。二者的主要区别是前者是通过条件变量和互斥量进行构建的,后者是通过互斥量和POSIX信号量进行构建的。
//生产消费模型:普通版本
#include <iostream>
#include <queue>
#include <pthread.h>
#include <thread>
#include <mutex>
#include "Task.hpp"using namespace std;
template<class T>
class BlockQueue
{
public:BlockQueue(int cap=5):_cap(cap){pthread_mutex_init(&lock,nullptr);pthread_cond_init(&full,nullptr);pthread_cond_init(&empty,nullptr);}~BlockQueue(){pthread_mutex_destroy(&lock);pthread_cond_destroy(&full);pthread_cond_destroy(&empty);}bool Isfull(){return q.size()==_cap;}bool Isempty(){return q.size()==0;}void Push(const T&in)//生产者的{pthread_mutex_lock(&lock);while(Isfull()){pthread_cond_wait(&full,&lock);}q.push(in);pthread_cond_signal(&empty);pthread_mutex_unlock(&lock);}void Pop(T*out)//消费者的{pthread_mutex_lock(&lock);while(Isempty()){pthread_cond_wait(&empty,&lock);}*out=q.front();q.pop();pthread_cond_signal(&full);pthread_mutex_unlock(&lock);}
private:queue<T> q;pthread_mutex_t lock;pthread_cond_t full;pthread_cond_t empty;int _cap;
};
//生产消费模型:环形队列
#pragma once#include <iostream>
#include <vector>
#include <semaphore.h>
#include <mutex>template<class T>
class RingQueue
{
private:void P(sem_t&sem){sem_wait(&sem);}void V(sem_t&sem){sem_post(&sem);}
public:RingQueue(int _size=10):v(_size),size(_size),c_step(0),p_step(0){pthread_mutex_init(&c_lock,nullptr);pthread_mutex_init(&p_lock,nullptr);sem_init(&c_sem,0,0);sem_init(&p_sem,0,size);}~RingQueue(){pthread_mutex_destroy(&c_lock);pthread_mutex_destroy(&p_lock);sem_destroy(&c_sem);sem_destroy(&p_sem);}void Push(const T&in){P(p_sem);{pthread_mutex_lock(&p_lock);v[p_step]=in;p_step++;p_step%=size;}V(c_sem);pthread_mutex_unlock(&p_lock);}void Pop(T*out){P(c_sem);{pthread_mutex_lock(&c_lock);*out=v[c_step];c_step++;c_step%=size;}V(p_sem);pthread_mutex_unlock(&c_lock);}
private:vector<T> v;int size;int c_step;int p_step;sem_t c_sem;sem_t p_sem;pthread_mutex_t c_lock;pthread_mutex_t p_lock;
};
6.线程池
一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着 监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利 用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。
线程池的应用场景: 1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技 术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个 Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情 况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限, 出现错误.
线程池实现: 1. 创建固定数量线程池,循环从任务队列中获取任务对象;2. 获取到任务对象后,执行任务对象中的任务接口
//PthreadPool.h
class Date
{
public:Date(const string &name) : threadname(name){}~Date(){}public:string threadname;
};template <class T>
class PthreadPool
{
private:PthreadPool(int thread_num = 5) : _thread_num(thread_num){pthread_mutex_init(&_mutex, nullptr);pthread_cond_init(&_cond, nullptr);for (int i = 1; i < _thread_num; i++){string name = "thread-";name += std::to_string(i);Date td(name);_threads.emplace_back(name,bind(&PthreadPool<T>::ThreadRun, this,placeholders::_1),td);cout<<name.c_str()<<" is created..."<<endl;}}PthreadPool(const PthreadPool<T> &tp) = delete;const PthreadPool<T> &operator=(const PthreadPool<T>) = delete;public:static PthreadPool<T> *GetInstance(){if (instance == nullptr){LockGuard lockguard(&sig_lock);if (instance == nullptr){cout<<"创建单例成功..."<<endl;instance = new PthreadPool<T>();}}return instance;}bool Start(){// 启动for (auto &thread : _threads){thread.Start();cout<<thread.ThreadName()<<" is running..."<<endl;}return true;}void ThreadWait(const Date &td){cout<<td.threadname.c_str()<<" no task,is sleeping..."<<endl;pthread_cond_wait(&_cond, &_mutex);}void ThreadWakeup(){pthread_cond_signal(&_cond);}void ThreadRun(Date &td){while (true){// 取任务T t;{LockGuard lockguard(&_mutex);while (_q.empty()){ThreadWait(td);cout<<(td.threadname).c_str()<<" thread is wakeup"<<endl;}t = _q.front();_q.pop();}// 处理任务t();cout<<td.threadname<<" habdler task "<<t.PrintTask().c_str()<<" done,result is "<<t.PrintResult().c_str()<<endl;}}void Push(T &in){cout<<"other thread push a task,task is :"<<in.PrintTask().c_str()<<endl;LockGuard lockguard(&_mutex);_q.push(in);ThreadWakeup();}~PthreadPool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}void Wait(){for (auto &thread : _threads){thread.Join();}}private:queue<T> _q;vector<Thread<Date>> _threads;int _thread_num;pthread_mutex_t _mutex;pthread_cond_t _cond;static PthreadPool<T> *instance;static pthread_mutex_t sig_lock;
};template <class T>
PthreadPool<T> *PthreadPool<T>::instance = nullptr;
template <class T>
pthread_mutex_t PthreadPool<T>::sig_lock = PTHREAD_MUTEX_INITIALIZER;
//thread.h
#pragma once#include <iostream>
#include <string>
#include <functional>
#include <pthread.h>
template<class T>
using func_t = std::function<void(T&)>;template<class T>
class Thread
{
public:Thread(const std::string &threadname, func_t<T> func, T &data):_tid(0), _threadname(threadname), _isrunning(false), _func(func), _data(data){}static void *ThreadRoutine(void *args) {Thread *ts = static_cast<Thread *>(args);ts->_func(ts->_data);return nullptr;}bool Start(){int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);if(n == 0) {_isrunning = true;return true;}else return false;}bool Join(){if(!_isrunning) return true;int n = pthread_join(_tid, nullptr);if(n == 0){_isrunning = false;return true;}return false;}std::string ThreadName(){return _threadname;}bool IsRunning(){return _isrunning;}~Thread(){}
private:pthread_t _tid;std::string _threadname;bool _isrunning;func_t<T> _func;T _data;
};
7.线程安全的单例模式
单例模式:某些类, 只应该具有一个对象(实例), 就称之为单例。 在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据。
饿汉方式实现单例模式:
懒汉方式最核心的思想是 "延时加载". 从而能够优化服务器的启动速度。
template <class T>
class PthreadPool
{
public:static PthreadPool<T> *GetInstance(){if (instance == nullptr){LockGuard lockguard(&sig_lock);if (instance == nullptr){cout<<"创建单例成功..."<<endl;instance = new PthreadPool<T>();}}return instance;}
private:queue<T> _q;vector<Thread<Date>> _threads;int _thread_num;pthread_mutex_t _mutex;pthread_cond_t _cond;static PthreadPool<T> *instance;static pthread_mutex_t sig_lock;
};template <class T>
PthreadPool<T> *PthreadPool<T>::instance = nullptr;
template <class T>
pthread_mutex_t PthreadPool<T>::sig_lock = PTHREAD_MUTEX_INITIALIZER;
懒汉方式实现单例模式:
template <typename T>
class Singleton
{volatile static T *inst; // 需要设置 volatile 关键字, 否则可能被编译器优化.static std::mutex lock;public:static T *GetInstance(){if (inst == NULL){lock.lock();if (inst == NULL){inst = new T();}lock.unlock();}return inst;}
};