目录
- 线程的理解
- 重谈页表
- 线程切换
- 线程控制
- 线程库的理解
- 线程的互斥
- 锁的原理
- 锁的封装
- Lockguard的使用
- 关于死锁
- 线程同步
- 条件变量
- 生产者与消费者CP理论
- 基于阻塞队列的生产消费模型
- 基于环形队列的生产消费模型
- POSIX信号量
- 线程池
- 线程的封装
- STL,智能指针和线程安全
- 线程安全的单例模式
- 其它各种常见的锁
- 读者和写者问题
线程的理解
线程是什么?
线程是进程内的一个执行分支。线程的执行粒度,要比进程要细。是CPU调度的基本单位。
那如何看待地址空间和页表呢?
地址空间是进程能看到的资源窗口;页表决定进程真正拥有的资源情况。
如果我们创建一个子进程
如果我们自己创建一个进程,不创建地址空间、页表,并将代码分它一部分,页表的映射也分它一部分。即如图所示:
重新定义线程和进程
什么叫做线程?操作系统调度的基本单位
什么是进程呢?承担分配系统资源的基本实体(如task_struct、进程地址空间、页表、IO加载(物理内存的代码和数据))
他们之间的关系:操作系统以进程为单位给我们分配资源,当我们进入进程内部,去执行执行流。所以线程是进程的一个分支,是进程的一部分资源
如现实生活中,社会分配资源的基本单位是家庭(进程),我们的家庭成员(线程)做的事是不一样的但最终目的是一样的,为了幸福的明天
linux是如何实现的这线程的呢?
linux并不像windows一样专门描述了一个结构体struct tcb;来表示线程,而是复用了进程数据结构和管理算法,仍用struct task_struct来模拟线程–所以linux中的执行流被叫作轻量级进程
重谈页表
虚拟地址是如何通过页表映射到物理地址的呢?
虚拟地址空间有232个地址,假设页表是一整块,页表里不仅仅存储地址也存储对内存访问的权限,一行条目按照6字节来算,则需要232x 6byte≈24GB,所以操作系统不会这么设计
我们知道虚拟地址空间有4GB,有232个下标,每个位置占1字节。一字节有32位,把前十位,中间十位,后面十二位共分为三组。前十位表示页目录的下标,中间十位二级页表的下标,后面十二位为物理内存的页框内的偏移量
如图所示:
这个设计最多要占多大空间呢?页目录大小:210x 4byte、所有二级页表大小:210x 210x 4byte。所有相加=4MB。
但一个进程一般不会访问所有地址,所以二级页表是不全的,即使要访问全也有页表置换算法,这个页表访问完不再使用了就会被未访问的页表置换
这也说明了创建线程比创建进程成本更低,因为线程只用创建task_struct即可
进程切换vs线程切换
为什么线程比进程更轻量化?
a.创建和释放更加轻量化(生死)
b.切换更轻量化(运行)
CPU内有cache来缓冲运行数据(运行一段时间即为热数据,效率更快),如果是进程切换,则cache内的所有运行数据均会失效,必须要重新缓冲,而线程不需要重新由冷变热。
这里我们看到的是CPU内cache的大小
线程的优点:
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点
性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高:编写与调试一个多线程程序比单线程程序困难得多
线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
- 文件描述符
- 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
线程控制
前面我们说到内核中没有线程结构体的概念,只有轻量级进程的概念。这就导致了系统不会直接提供线程的系统调用,只会给我们提供轻量级进程的系统调用,这样的接口操作起来十分复杂。
但我们用户需要线程的接口,所以Linux给我们提供了一个第三方的线程库,对轻量级进程接口进行了封装。几乎所有的linux平台都是默认自带这个库。
如何使用呢?
第一个参数:为输出型参数,为线程id,有了该id旧方便操作这个线程了
第二个参数:线程属性,不需要了解,填NULL即可
第三个参数:类型为函数指针,返回值是void*,参数是void* main函数是主线程的入口函数,这是线程的入口函数
第四个参数:要传给第三个函数指针的参数值
返回值:成功返回0,非0为错误,不会设置errno,用返回值告诉我们是否创建成功
//makefile
ThreadTest:ThreadTest.cc
g++ -o $@ $^ -std=c++11 -lpthread #当我们用第三方库时要加上要连接的库名(在连接动静态库里有详细讲解)
.PHONY:clean
clean:
rm -rf ThreadTest
//ThreadTest.cc
void* start_routine(void* args)//返回类型为void* 参数为void*
{
while (1)
{
cout << "new thread, pid:" << getpid() << endl;
sleep(2);
}
}
int main()
{
pthread_t tid;//pthread_t是typedef类型为unsigned long int
pthread_create(&tid, nullptr, start_routine, nullptr);
while (1)
{
cout << "main thread, pid:" << getgid() << endl;
sleep(1);
}
return 0;
}
运行结果
我们也能看到只有一个进程内有两个线程
LWP(light weight process)为轻量级进程的id,CPU调度的id,CPU根本不看pid。如果pid等于lwp则这个线程叫做主线程,不相等则说明是被创建出来的线程
给任何一个线程发送信号,一个线程挂掉了其余的都会挂掉
void show(const string& name)
{
cout << name << "say#" << "hello thread" << endl;
}
void* start_routine(void* args)
{
while (1)
{
show("[new thread]");
sleep(1);
}
}
int main()
{
pthread_t tid;//pthread_t是typedef类型为unsigned long int
pthread_create(&tid, nullptr, start_routine, &tid);
while (1)
{
show("[main thread]");
sleep(1);
}
return 0;
}
该show函数可以被多个执行流执行,show函数被重入了
int globalnum = 0;
void* start_routine(void* args)//返回类型为void* 参数为void*
{
while (1)
{
cout << "new thread, pid:" << getpid() << "globalnum:" << globalnum << endl;
sleep(2);
}
}
int main()
{
pthread_t tid;//pthread_t是typedef类型为unsigned long int
pthread_create(&tid, nullptr, start_routine, nullptr);
while (1)
{
cout << "main thread, pid:" << getgid() << "globalnum:" << globalnum << endl;
globalnum++;
sleep(1);
}
return 0;
}
说明全局变量是共享的
int globalnum = 0;
void* start_routine(void* args)//返回类型为void* 参数为void*
{
while (1)
{
cout << "new thread, pid:" << getpid() << "globalnum:" << globalnum << endl;
sleep(5);
int a = 10;
a /= 0;
}
}
int main()
{
pthread_t tid;//pthread_t是typedef类型为unsigned long int
pthread_create(&tid, nullptr, start_routine, nullptr);
while (1)
{
cout << "main thread, pid:" << getgid() << "globalnum:" << globalnum << endl;
globalnum++;
sleep(1);
}
return 0;
}
任何一个线程出错都会影响整个进程
线程也是要等待的,如果不等待,会有内存泄漏问题
第一个参数:线程的pid
第二个参数:线程函数的返回值 的二级指针
void* start_routine(void* args)//返回类型为void* 参数为void*
{
string* str = new string("线程");
*str += to_string(*(int*)args);
*str += "退出";
return str;
}
int main()
{
pthread_t tid;//pthread_t是typedef类型为unsigned long int
pthread_create(&tid, nullptr, start_routine, &tid);
cout << "wait pthread" << endl;
void* ret;
pthread_join(tid, &ret);
cout << "pthread return:" << *(string*)ret << endl;
return 0;
}
为什么我们不在thread_join的时候检查信号异常呢?
因为做不到,信号异常影响的是整个程序
线程传参和传返回值类型的建议:常量(传递时为它本身)、全局区(传递时为指针)、堆区(传递时为指针)
pthread_cancel函数作用是取消已经运行起来的线程
参数为线程id。成功返回0,失败返回非0
void* start_routine(void* args)//返回类型为void* 参数为void*
{
int cnt = 5;
while (1)
{
printf("I am pthread pid: %d\n", getpid());
sleep(1);
cnt--;
if (cnt == 0) break;
}
}
int main()
{
pthread_t tid;//pthread_t是typedef类型为unsigned long int
pthread_create(&tid, nullptr, start_routine, nullptr);
sleep(2);
pthread_cancel(tid);
void* ret;
pthread_join(tid, &ret);
cout << "pthread return:" << (long long)ret << endl;//指针为8字节
return 0;
}
如果我们的线程想直接退出呢?—exit行不行呢?
void* start_routine(void* args)//返回类型为void* 参数为void*
{
const char* name = (const char*)args;
int cnt = 5;
while (1)
{
printf("I am pthread pid: %d\n", getpid());
sleep(1);
cnt--;
if (cnt == 0) break;
}
exit(11);
}
int main()
{
pthread_t tid;//pthread_t是typedef类型为unsigned long int
pthread_create(&tid, nullptr, start_routine, &tid);
cout << "wait pthread" << endl;
void* ret;
pthread_join(tid, &ret);
cout << "pthread return:" << endl;
return 0;
}
发现并没有打印pthread return,说明exit是直接退出了进程,并不是只退出了该线程,那我们应该用什么退出呢?
参数为你想设置线程的返回值
void* start_routine(void* args)//返回类型为void* 参数为void*
{
const char* name = (const char*)args;
int cnt = 5;
while (1)
{
printf("I am pthread pid: %d\n", getpid());
sleep(1);
cnt--;
if (cnt == 0) break;
}
pthread_exit((void*)11);
}
int main()
{
pthread_t tid;//pthread_t是typedef类型为unsigned long int
pthread_create(&tid, nullptr, start_routine, &tid);
cout << "wait pthread" << endl;
void* ret;
pthread_join(tid, &ret);
cout << "pthread return:" << (long long int)ret << endl;
return 0;
}
为什么线程的返回值类型是void*,而不是其它的类型呢?
因为void*可以接收任何类型,包括对象的指针
class Request
{
public:
Request(int start, int end, const string& threadname)
:_start(start)
,_end(end)
,_threadname(threadname)
{}
public:
int _start;
int _end;
string _threadname;
};
class Response
{
public:
Response(int exitcode, int result)
:_exitcode(exitcode)
,_result(result)
{}
public:
int _exitcode; //计算结果是否可靠
int _result; //计算结果
};
void* sumCount(void* args)//返回类型为void* 参数为void*
{
Request* rq = static_cast<Request*>(args);//这种写法等价于强转(Request*)args, 只不过这种转换更安全
Response* rp = new Response(0, 0);
for (int i = rq->_start; i <= rq->_end; ++i)
{
rp->_result += i;
cout << rp->_result << " ";
}
cout << endl;
return rp;
}
int main()
{
Request* rq = new Request(1, 100, "thread 1");
pthread_t tid;//pthread_t是typedef类型为unsigned long int
pthread_create(&tid, nullptr, sumCount, rq);//将结构体作为参数传递给了线程
void* ret;
pthread_join(tid, &ret);
Response* rsp = static_cast<Response*>(ret);
printf("exitcode: %d, result: %d\n", rsp->_exitcode, rsp->_result);
delete rsp;//都是用指针来传递。提高了效率又不会轻易发生内存泄漏
return 0;
}
线程库的理解
pthread库为原生库,什么是原生库?只要linux存在则该库就存在,那为什么又会有原生线程库的概念呢?
前面我们提到了linux中没有真正意义上的线程,只有轻量级进程的概念,所以最开始的linux系统只提供了对轻量级进程操作的系统接口,如clone函数等,那为什么我们不使用他们呢?因为这些接近底层的系统接口使用起来太复杂,在用户的强烈要求下,linux社区将这些底层的接口封装并将线程库合并进去了
在以后任何的语言中如C++11,都是对该原生线程库的封装而来的
这也就是为什么我们在g++时不加上-lpthread库会编译报错,因为我们使用了第三方库,我们不也使用了C/C++的第三方库吗?为什么没有引用他们的名字呢?因为g++/gcc是针对于C++和C语言的编译器,他们内置了这些语法,所以他们当然能找到。C/C++语言库和线程库本身是没有任何关系的,所以我们使用除了C/C++以外的库的时候都需要引用他们的名字
线程库和内存地址空间的关系如图所示
所有线程都执行一个函数会发生什么呢?
void* start_routine(void* args)//返回类型为void* 参数为void*
{
int cnt = 0;
while (1)
{
sleep(1);
cnt++;
printf("threadname:%s, cnt:%d, &cnt:%p\n", args, cnt, &cnt);
}
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, nullptr, start_routine, (void*)"threadname1");
pthread_create(&tid2, nullptr, start_routine, (void*)"threadname2");
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}
可以看到线程是具有独立的栈结构的
我们能否拿到线程内栈上的地址呢?
int* p = NULL;
void* start_routine(void* args)//返回类型为void* 参数为void*
{
int cnt = 0;
if (strcmp((const char*)args, "threadname1") == 0)
p = &cnt;
while (1)
{
sleep(1);
cnt++;
printf("threadname:%s, cnt:%d, &cnt:%p\n", args, cnt, &cnt);
}
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, nullptr, start_routine, (void*)"threadname1");
pthread_create(&tid2, nullptr, start_routine, (void*)"threadname2");
while (1)
{
cout << "main threadname cnt" << p << endl;
sleep(1);
}
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}
不仅是主线程可以拿到栈上的地址,其它线程也可以拿到,全局变量是被所有线程都可以看到的,所以线程和线程之间几乎没有秘密
在全局变量前面添加__thread,这是个编译选项,作用是使每个线程都有一份独立的全局变量,线程的局部存储。
只能定义内置类型,不能用来修饰自定义类型
__thread int global = 100;
void* start_routine(void* args)//返回类型为void* 参数为void*
{
while (1)
{
sleep(1);
cout << (const char*)args << global << "&global"<<&global<< endl;
}
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, nullptr, start_routine, (void*)"threadname1");
pthread_create(&tid2, nullptr, start_routine, (void*)"threadname2");
pthread_join(tid1, nullptr);
pthread_join(tid2, nullptr);
return 0;
}
如果主线程不想阻塞等待子线程呢?如果不关心线程的返回值,join是一种负担
线程分离函数
线程分离了就不能在join了
void* start_routine(void* args)//返回类型为void* 参数为void*
{
int cnt = 5;
while (1)
{
cnt--;
sleep(1);
cout << (const char*)args << global << "&global"<<&global<< endl;
}
}
int main()
{
pthread_t tid1, tid2;
pthread_create(&tid1, nullptr, start_routine, (void*)"threadname1");
pthread_create(&tid2, nullptr, start_routine, (void*)"threadname2");
pthread_detach(tid1);
pthread_detach(tid2);
int n = pthread_join(tid1, nullptr);
cout << "threadname1:" << n << " strerror:" << strerror(n) << endl;
n = pthread_join(tid2, nullptr);
cout << "threadname2:" << n << " strerror:" << strerror(n) << endl;
return 0;
}
线程的互斥
要让线程出问题,就要尽可能让多个线程交叉执行
多个线程交叉执行的本质:就是让调度器尽可能的频繁发生线程调度与切换
线程一般在什么时候发生切换呢?1.时间片到了2.来了个更高优先级的线程 3.线程等待的时候
线程是在什么时候检测上面的问题呢?从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以就直接发生线程切换
多线程模拟抢票逻辑
#define NUM 10
int tickets = 1000;
void* getticket(void* args)
{
while (true)
{
if (tickets > 0)
{
usleep(1000);
tickets--;
cout << "thread" << (long long)args << " get tickets:" << tickets << endl;
}
else
{
break;
}
}
}
int main()
{
vector<pthread_t> tids(NUM);
for (int i = 0; i < NUM; ++i)
{
pthread_create(&tids[i], nullptr, getticket, (void*)i);
}
for (int i = 0; i < NUM; ++i)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
锁的原理
票数大于0才能抢票,为什么等于0小于0都能抢到票?
代码tickets–在汇编层面会被翻译成三条语句
1.CPU从物理内存中读取ticket
2.在CPU中对ticket进行计算(减减操作)
3.再将ticket计算后的结果放回到物理内存
gif建议截图暂停观看
上面明明有两个线程,按理来说应该票数减2,最周却只减了1
不仅仅是减减操作,还有判断也是如此,CPU分为数值运算和逻辑运算,因为你在判断的时候又多个线程进来了,所以会出现负数的情况
那怎么解决这种情况呢?
就需要用到锁了,锁的初始化和销毁函数:
第一个函数:为锁的销毁函数
第二个函数:第一个参数为输出型参数,初始化mutex。第二个参数为mutex的属性(不用管也不需要管,设为NULL)
第三个:锁定义成全局的,不需要初始化和销毁
第一个加锁函数。第二个尝试加锁,如果锁是可用的返回非0,线程将成功获取锁,不可用则返回0。第三个解锁函数
锁是什么,我要怎么去理解它呢?
下面我给大家举个例子:
有一个超级vip自习室,这个自习室只能有一个人,当你进去的时候需要把门锁上,别人进来的时候就会被锁在外面,其它人想进来就必须有(申请)到这个锁的钥匙;当你不用了你就把这个锁的钥匙挂在墙上,这样别人就可以进去了。
这样就保证了这个自习室内始终只有一个人访问,其它人要访问就排队在外面等这个锁的钥匙
#define NUM 10
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int tickets = 1000;
void* getticket(void* args)
{
while (true)
{
pthread_mutex_lock(&mutex);
if (tickets > 0)
{
usleep(1000);
//ticktets为共享数据,造成了数据不一致问题,肯定和多线程并发访问是有关系的
cout << "thread" << (long long)args << " get tickets:" << tickets << endl;
tickets--;//多线程对一个全局变量进行++/--操作是否是安全的?
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
pthread_mutex_unlock(&mutex);
}
}
int main()
{
vector<pthread_t> tids(NUM);
for (int i = 0; i < NUM; ++i)
{
pthread_create(&tids[i], nullptr, getticket, (void*)i);
}
for (int i = 0; i < NUM; ++i)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
可以发现确实没有减到0和负数的情况了,也发现运行时间变慢了,因为这是串行执行的。又发现一个问题,为什么只有一个线程在抢票?因为刚抢完票的线程离锁的位置是最近的,导致了它对锁的竞争能力更强。我们增加一条代码,模拟生成一个抢票的订单,在解锁完后休眠一会儿,休眠的时候OS会将线程上下文切换,使所有休眠完后的线程的对锁的竞争能力相同
#define NUM 10
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int tickets = 1000;
void* getticket(void* args)
{
while (true)
{
pthread_mutex_lock(&mutex);
if (tickets > 0)
{
usleep(1000);
//ticktets为共享数据,造成了数据不一致问题,肯定和多线程并发访问是有关系的
cout << "thread" << (long long)args << " get tickets:" << tickets << endl;
tickets--;//多线程对一个全局变量进行++/--操作是否是安全的?
}
else
{
pthread_mutex_unlock(&mutex);
break;
}
pthread_mutex_unlock(&mutex);
usleep(10);//模拟生成一个抢票后的订单
}
}
int main()
{
vector<pthread_t> tids(NUM);
for (int i = 0; i < NUM; ++i)
{
pthread_create(&tids[i], nullptr, getticket, (void*)i);
}
for (int i = 0; i < NUM; ++i)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
继续上面一个例子,你在vip自习室内,想上厕所,打开门出去的时候,发现队伍排了很长,你又后悔了,又拿钥匙进去学习,过了一会儿你又出来了,发现外面还是有很多人,你又回去了,反复这个动作,这就导致了一个问题,导致了外面的人的饥饿问题
在纯互斥环境,如果锁的分配不够合理,容易导致其它线程饥饿问题
观察员发现了这个漏洞,所以就设计了几个规则:1.想要进自习室,必须排队 2.出来的人,不能立马重新申请锁,必须排到队列的尾部
让所有的线程(人)获取锁(钥匙)按照一定的顺序。按照一定的顺序性获取资源:同步
所有线程去访问的资源我们叫做共享资源,我们要保护共享资源的访问安全。锁也是所有线程去访问的资源,那是谁来保护锁的安全呢?申请锁和释放锁本身就是原子性的
原子性必然要解决的一个问题:线程1申请锁成功,进入临界资源,正在访问临界区资源期间,可不可以被线程切换走呢?绝对是可以的。当持有锁的线程被切换走的时候是抱着锁被切换走的,其它线程依旧无法申请这个锁,也便无法向后执行
这是如何做到的呢?锁的原理:
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据交换,由于只有一条指令,保证了原子性
以下gif配图可截图暂停观看
锁的封装
#include <pthread.h>
class Mutex
{
public:
Mutex(pthread_mutex_t* mutex)
:_mutex(mutex)
{}
void Lock()
{
pthread_mutex_lock(_mutex);
}
void UnLock()
{
pthread_mutex_unlock(_mutex);
}
~Mutex()
{}
private:
pthread_mutex_t* _mutex;
};
//RAII风格的锁
class LockGuard
{
public:
LockGuard(pthread_mutex_t* mutex)
:_mutex(mutex)
{
_mutex.Lock();
}
~LockGuard()
{
_mutex.UnLock();
}
private:
Mutex _mutex;
};
RAIILockGuard的应用
#define NUM 10
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int tickets = 1000;
void* getticket(void* args)
{
while (true)
{
{
LockGuard lockguard(&mutex);//临时变量生命周期为一个代码块,生命周期结束后会利用自己的析构函数自动解锁
if (tickets > 0)
{
usleep(100);
//ticktets为共享数据,造成了数据不一致问题,肯定和多线程并发访问是有关系的
cout << "thread" << (long long)args << " get tickets:" << tickets << endl;
tickets--;//多线程对一个全局变量进行++/--操作是否是安全的?
}
else
{
break;
}
}
usleep(10);
}
}
int main()
{
vector<pthread_t> tids(NUM);
for (int i = 0; i < NUM; ++i)
{
pthread_create(&tids[i], nullptr, getticket, (void*)i);
}
for (int i = 0; i < NUM; ++i)
{
pthread_join(tids[i], nullptr);
}
return 0;
}
线程安全:多个线程并发访问同一段代码时,不会出现不同的结果。
重入:同一个函数被不同的执行流调用,当一个流程还没有执行完,就有其它的执行流在此进入,我们称之为重入。
函数不可重入,多线程就可能会出问题;可重入,多线程不会出问题
关于死锁
死锁:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源儿处于一种永久等待状态 (好比两个女生打架,我扯着你的头发,你扯着我的头发,都必须让对方先松手我再松手)
问题:一把锁能否产生死锁?能产生死锁(好比你自己也会被自己绊倒)pthread_mutex_lock(&lock);pthread_mutex_lock(&lock);这两条代码写在一起就会死锁,当然这种弱智的代码你不可能故意这样写,但你不能保证你无意的会这样写
死锁四个必要条件:
互斥条件:一个资源每次只能被一个执行流使用
请求与保持条件:一个执行流因请求资源而阻塞时,对自己获得的资源保持不放
不剥夺条件:一个执行流已获得资源,在未使用完之前,不能强行剥夺其它线程的(双方只有都是僵持状态才行,能剥夺就不会僵持了)
循环条件等待:若干执行流之间形成一种头尾相接的循环等待资源的关系
如果避免死锁问题?
1.破坏4个必要条件,只要有一个不满足就可以了(如:1.trylock申请不到,把自己的锁释放了。2.强行剥夺–释放对方的锁—锁的原理可知:解锁的时候不是自己还回去,而是直接把锁置为1)
2.加锁顺序一致
3.避免未释放的场景
4.资源一次性分配(不要多次的分配)
线程同步
同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题
问题:既然线程排队了,已经有序了,为什么还要锁呢?
为什么会排队,前提是要试着访问资源,如果访问不到才会去排队,换种说法,防的是突然来的人,而防不是排队的人
条件变量
如何实现线程同步呢?——条件变量
给大家举个例子来讲解条件变量:面试的时候,你进面试间进不去,有个指示牌发现要去隔壁房间排队,然后你去排队了,前面的人面试完了走了,然后在房间里的人听到了铃声,表示下一个人去面试。
就如同线程一样:来了一个线程没申请到锁就会去排队,前面的线程访问完资源后,铃铛就会响(条件变量就会唤醒下一个线程),下一个线程就回去申请锁。
在这个过程中,条件变量一定依赖锁的使用
这些函数的参数的理解几乎和线程的一样
第一个参数为初始化后的cond。第二个参数:为锁的指针
第一个函数:唤醒所有在条件变量下等待的线程
第二个函数:唤醒一个在条件变量下等待的线程
#define NUM 5
int cnt = 0;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* Count(void* args)
{
pthread_detach(pthread_self());
int num = (long long)args;
cout << "pthread:" << num << " create success" << endl;
while (1)
{
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex);//申请了锁就去等待吗?这样不是很容易就死锁了?这条代码写到这里是否有问题?是否应该写到和上面代码互换位置?
//这条代码当然是没问题的,pthread_cond_wait在线程等待的时候会自动释放自己原有的锁 在被唤醒时,会重新获得原来的锁
cout << "pthread:" << num <<" ,cnt:" << cnt++ << endl;
pthread_mutex_unlock(&mutex);
}
}
int main()
{
pthread_t tid[NUM];
for (int i = 0; i < NUM; ++i)
{
pthread_create(&tid[i], nullptr, Count, (void*)i);
}
while (1)
{
sleep(1);
pthread_cond_signal(&cond);
cout << "signal one thread..." << endl;
}
return 0;
}
将pthread_cond_signal函数换为pthread_cond_broadcast
我们怎么知道要让一个线程去休眠呢?当临界资源不就绪的时候我们就需要让一个线程去休眠,所以临界资源也是有状态的
那你怎么知道临界资源是否就绪,你用代码判断出来的,判断是访问临界资源吗?是,也就加锁代码必须在判断代码的前面。如先加锁判断票数,如果票数大于0,则去抢票,否则休眠等待wait
生产者与消费者CP理论(consumer producer)
如果中间没有超市,消费者要商品的时候找供货商,供货商一生产就会生产出一堆商品,如果有多个消费者还好,但如果只有一个消费者必然会浪费掉许多商品,如果有非常多消费者呢,那就会供不应求了。在这里超市的作用就体现了出来,超市有很大的空间,尽管消费者不多时,供货商仍可把生产出来的商品交给超市,超市暂存起来,不管有一个消费者,和多个消费者都能供应的上来。这样就能解决供货商和消费者的忙闲不均问题,消费者和生产者进行解耦
商品是数据,供货商是提供数据的线程,消费者是消费数据的线程,超市是一块特定空间的内存空间
超市由生产者和消费者都会访问,所以它是共享资源,它也会有并发问题,所以无论生产者之间还是消费者之间还是生产者和消费者之间都有互斥关系,生产者生产了数据有了数据你消费者才能拿,所以生产者和消费者还有同步关系
给大家一个记忆方法:
“321”原则
3种关系,2种角色(生产和消费),1个交易场所(特定结构的空间)
优点:1.支持忙闲不均 2.生产和消费进行解耦
基于阻塞队列的生产消费模型
模拟
Makefile文件
main:main.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf main
Blockqueue.hpp文件
#include <iostream>
#include <queue>
#include <pthread.h>
#include <unistd.h>
using namespace std;
template<class T>
class Blockqueue
{
public:
Blockqueue(int max)
:_max(max)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cCond, nullptr);
pthread_cond_init(&_pCond, nullptr);
}
void Push(const T& in)
{
pthread_mutex_lock(&_mutex);
//if (_q.size() > _max)//这里用if是否合理?
while (_q.size() > _max)
{
pthread_cond_wait(&_pCond, &_mutex);//如果被唤醒的线程不止一个,这里就很有可能引发线程安全,所以应该用while判断
}
_q.push(in);
pthread_cond_signal(&_cCond);
pthread_mutex_unlock(&_mutex);
}
T pop()
{
pthread_mutex_lock(&_mutex);
while (_q.size() <= 0)//同样和上面一样的问题
{
pthread_cond_wait(&_cCond, &_mutex);
}
T front = _q.front();
_q.pop();
pthread_cond_signal(&_pCond);
pthread_mutex_unlock(&_mutex);
return front;
}
~Blockqueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cCond);
pthread_cond_destroy(&_pCond);
}
private:
queue<T> _q;
int _max; //阻塞队列的最大容量
pthread_mutex_t _mutex;
pthread_cond_t _cCond;//消费的条件变量
pthread_cond_t _pCond;//生产的条件变量
};
Task.hpp文件
#include <iostream>
#include <string>
using namespace std;
// 给我两个数和操作符,我给你返回一个结果
class Task
{
public:
Task(int x, int y, int op) : _x(x), _y(y), _op(op)
{
}
void run()
{
switch (_op)
{
case '+':
_result = _x + _y;
break;
case '-':
_result = _x - _y;
break;
case '*':
_result = _x * _y;
break;
case '/':
if (_y == 0)
{
_result = _exitcode = -1;
break;
}
_result = _x / _y;
break;
case '%':
if (_y == 0)
{
_result = _exitcode = -1;
break;
}
_result = _x % _y;
break;
}
}
void operator()()
{
run();
}
string GetTask()//打印任务信息
{
string r = to_string(_x);
r += _op;
r += to_string(_y);
r += "=?";
return r;
}
string GetResult()//打印处理任务后的结果
{
string r = to_string(_x);
r += _op;
r += to_string(_y);
r += "=" + to_string(_result);
r += " exitcode:" + to_string(_exitcode);
return r;
}
private:
int _x;
int _y;
int _op;
int _result = 0;//处理任务的结果
int _exitcode = 0;//该结果是否可信
};
main.cc文件
#include "Blockqueue.hpp"
#include "Task.hpp"
#define CNUM 5 //生产者线程的数量
#define PNUM 3 //消费者线程的数量
#define BLOCKMAX 8 //阻塞队列的最大容量
void* Consumer(void* args)
{
pthread_detach(pthread_self());
Blockqueue<Task>* blockqueue = static_cast<Blockqueue<Task>*>(args);//等价于Blockqueue<Task>* blockqueue = (Blockqueue<Task>*)args
while (1)
{
int x = rand() % 50 + 1;
int y = rand() % 50;
const char* oper = "+-*/%";
Task t(x, y, oper[rand() % 5]);
blockqueue->Push(t);
cout << t.GetTask() << endl;
}
}
void* Producer(void* args)
{
pthread_detach(pthread_self());
Blockqueue<Task>* blockqueue = static_cast<Blockqueue<Task>*>(args);
while (1)
{
sleep(1);
Task t = blockqueue->pop();
t();
cout << t.GetResult() << endl;
}
}
int main()
{
Blockqueue<Task>* bq = new Blockqueue<Task>(BLOCKMAX);
srand(time(NULL));
pthread_t ctid[CNUM], ptid[PNUM];
for (int i = 0; i < CNUM; ++i)
{
pthread_create(&ctid[i], nullptr, Consumer, (void*)bq);
}
for (int i = 0; i < PNUM; ++i)
{
pthread_create(&ctid[i], nullptr, Producer, (void*)bq);
}
while (1) sleep(1);
return 0;
}
运行结果
生产者收到的任务数据从哪里来?用户或网络等给你的,所以生产者生产(收到)的数据也是要花时间获取的。同样的,消费者从阻塞队列拿到数据后,再处理数据也需要花时间
所以生产消费模型高效在哪里呢?生产者是一个一个将数据给阻塞队列,和消费者也是一个一个从阻塞队列拿数据,这里是体现不了高效。上面说到了还有两个过程,所以真正高效的是:生产者生产(收到)数据和消费者从阻塞队列拿数据可并发执行;生产者将数据投递给阻塞队列和消费者加工处理数据可并发执行。
基于环形队列的生产消费模型
POSIX信号量
先发现我们之前写的代码的不足的地方
void Push(const T& in)
{
pthread_mutex_lock(&_mutex);//先加锁
while (_q.size() > _max)//再检测临界资源是否就绪
{
pthread_cond_wait(&_pCond, &_mutex);
}
_q.push(in);
pthread_cond_signal(&_cCond);
pthread_mutex_unlock(&_mutex);
}
一个线程,在操作临界资源的时候必须临界资源是满足条件的,可是,在没访问之前,公共资源是否满足生产或消费条件我们无法直接得知,只能先加锁,再检测是否满足条件,再根据情况操作
这种情况总结为:在将访问之前不知道资源情况就去抢锁。
还有一种对标的情况:在访问之前就知道资源情况,再去抢锁。
这两种情况明显第二种情况的效率更高,也更符合实际情况。
这种情况就可由信号量来实现。SystemV信号量在进程间通信讲到过
POSIX信号量和SystemV信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但POSIX可以用于线程间同步。
什么是信号量?如同电影院的票,尽管你还没看这个电影(访问这个资源)但你已经预定了这份资源,这份资源只会被你访问。本质:对临界资源中特定小块资源的预定机制
a.信号量本质是一把计数器–衡量临界资源中资源还有数量多少的计数器
b.只要拥有信号量,就在未来一定能够拥有临界资源的一部分
有了信号量,我们在访问真正的临界资源之前,我们就可以提前知道临界资源的使用情况了。只要申请成功,就说明一定有你的资源。只要申请失败,就说明条件不就绪。这样就不需要先加锁再判断临界资源是否就绪了
第一个参数:输出型参数,初始化sem。第二个参数:0表示线程共享,非0表示进程共享。第三个参数:信号量的初始值
销毁信号量
等待信号量,会将信号量的值减1-----申请资源也被称为P操作
发布信号量,表示资源使用完毕,可以归还资源了,将信号量加1-----归还资源也被称为V操作信号量核心操作:PV原语
具体如何使用信号量,实现基于环形队列的生产消费模型
该生产消费模型生产者生产数据,和消费者消费数据是可以并发执行的
如何理解呢?现在我以单生产和单消费,给大家举一个例子
现在我和你在玩一个游戏,我在盘子里一个一个放苹果,你跟着我后面一个一个拿苹果。
有三个规则:1.你不能超过我。2.我不能把你套一个圈(否则盘子里之前的数据会被覆盖)
3.问题:我们两个什么时候会站在一起?
a.盘子全部为空-----我们两个站在一起,谁先运行呢?我(生产者)
b.盘子上全部都是苹果-----我们两个站在一起,谁先运行呢?你(消费者)
c.其它情况,我们两个指向的是不同的位置
在环形队列中,大部分情况,单生产和单消费是可以并发执行的。只有在满或者空的时候,才有互斥与同步问题
为了完成环形队列CP(生产消费)问题,我们要做的核心工作是什么?维护上面三个规则。如何维护呢?–》信号量是用来衡量临界资源中资源数量的。
1.对于生产者而言,只关心什么?队列中的剩余空间----空间资源定义一个信号量
2.对于消费者而言,只关心什么?放入队列中的数据数量----数据资源数量定义一个信号量
makefile文件
main:main.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf main
RIngQueue.hpp文件
#include <iostream>
#include <vector>
#include <semaphore.h>
#include <pthread.h>
using namespace std;
template<class T>
class RingQueue
{
public:
RingQueue(int cap = 5)
:_cap(cap), _cPos(0), _pPos(0)
{
_ringqueue.resize(_cap);
sem_init(&_producerSem, 0, _cap);
sem_init(&_consumerSem, 0, 0);
pthread_mutex_init(&_pMutex, nullptr);
pthread_mutex_init(&_cMutex, nullptr);
}
void P(sem_t* sem)
{
sem_wait(sem);
}
void V(sem_t* sem)
{
sem_post(sem);
}
void Push(const T& in)
{
P(&_producerSem);
pthread_mutex_lock(&_pMutex);
_pPos++;
_pPos %= _cap;
_ringqueue[_pPos] = in;
pthread_mutex_unlock(&_pMutex);
V(&_consumerSem);
}
T Pop()
{
P(&_consumerSem);
pthread_mutex_lock(&_cMutex);
T tmp = _ringqueue[_cPos];
_cPos++;
_cPos %= _cap;
pthread_mutex_unlock(&_cMutex);
V(&_producerSem);
return tmp;
}
~RingQueue()
{
sem_destroy(&_producerSem);
sem_destroy(&_consumerSem);
pthread_mutex_destroy(&_pMutex);
pthread_mutex_destroy(&_cMutex);
}
private:
vector<T> _ringqueue;
int _cap;
int _cPos;
int _pPos;
sem_t _producerSem;//关心剩余空间
sem_t _consumerSem;//关心里面的数据
pthread_mutex_t _pMutex;//生产者的锁
pthread_mutex_t _cMutex;//消费者的锁
};
Task.hpp文件
#include <iostream>
#include <string>
using namespace std;
// 给我两个数和操作符,我给你返回一个结果
class Task
{
public:
Task(int x = 1, int y = 1, char op = '+') : _x(x), _y(y), _op(op)
{
}
void run()
{
switch (_op)
{
case '+':
_result = _x + _y;
break;
case '-':
_result = _x - _y;
break;
case '*':
_result = _x * _y;
break;
case '/':
if (_y == 0)
{
_result = _exitcode = -1;
break;
}
_result = _x / _y;
break;
case '%':
if (_y == 0)
{
_result = _exitcode = -1;
break;
}
_result = _x % _y;
break;
}
}
void operator()()
{
run();
}
string GetTask()//打印任务信息
{
string r = to_string(_x);
r += _op;
r += to_string(_y);
r += "=?";
return r;
}
string GetResult()//打印处理任务后的结果
{
string r = to_string(_x);
r += _op;
r += to_string(_y);
r += "=" + to_string(_result);
r += " exitcode:" + to_string(_exitcode);
return r;
}
private:
int _x;
int _y;
int _op;
int _result = 0;//处理任务的结果
int _exitcode = 0;//该结果是否可信
};
main.cc文件
#include <unistd.h>
#include "Task.hpp"
#include "RingQueue.hpp"
using namespace std;
#define PNUM 5 //生产者个数
#define CNUM 3 //消费者个数
#define RINGQUEUECAP 10 //环形队列的容量
struct ThreadData
{
ThreadData(RingQueue<Task>* ringqueue, string threadname):_ringqueue(ringqueue), _threadname(threadname)
{}
RingQueue<Task>* _ringqueue;
string _threadname;
};
void* Producer(void* args)
{
ThreadData* td = (ThreadData*)args;
while (1)
{
sleep(1);
int x = rand() % 50 + 1;
int y = rand() % 50;
string oper = "+-*/%";
char op = oper[rand() % 5];
Task t(x, y, op);
td->_ringqueue->Push(t);
cout <<"threadname:" << td->_threadname << " " << t.GetTask() << endl;
}
}
void* Consumer(void* args)
{
ThreadData* td = (ThreadData*)args;
while (1)
{
sleep(1);
Task t = td->_ringqueue->Pop();
t();
cout <<"threadname:" << td->_threadname << " " << t.GetResult() << endl;
}
}
int main()
{
srand(time(NULL));
pthread_t ptid[PNUM];
pthread_t ctid[CNUM];
RingQueue<Task>* ringqueue = new RingQueue<Task>(RINGQUEUECAP);
for (int i = 0; i < PNUM; ++i)
{
ThreadData* td = new ThreadData(ringqueue, to_string(i));
pthread_create(&ptid[i], nullptr, Producer, (void*)td);
}
for (int i = 0; i < CNUM; ++i)
{
ThreadData* td = new ThreadData(ringqueue, to_string(i));
pthread_create(&ctid[i], nullptr, Consumer, (void*)td);
}
for (int i = 0; i < PNUM; ++i)
{
pthread_join(ptid[i], nullptr);
}
for (int i = 0; i < CNUM; ++i)
{
pthread_join(ctid[i], nullptr);
}
return 0;
}
运行结果
生产者和生产者关系:互斥(同一时刻只能有一个放数据)
消费者和消费者关系:互斥(同一时刻只能有一个拿数据)
生产者和消费者关系:大部分时间是并发访问的,少部分时间是互斥同步的
线程池
什么是池化技术?在进程间通信中也讲过进程池
池化技术:提前开好空间,以空间换时间,而减少系统调用的次数,而提高效率
Makefile文件
main:main.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -rf main
Task.hpp文件
#include <iostream>
#include <string>
using namespace std;
// 给我两个数和操作符,我给你返回一个结果
class Task
{
public:
Task(int x = 1, int y = 1, char op = '+') : _x(x), _y(y), _op(op)
{
}
void run()
{
switch (_op)
{
case '+':
_result = _x + _y;
break;
case '-':
_result = _x - _y;
break;
case '*':
_result = _x * _y;
break;
case '/':
if (_y == 0)
{
_result = _exitcode = -1;
break;
}
_result = _x / _y;
break;
case '%':
if (_y == 0)
{
_result = _exitcode = -1;
break;
}
_result = _x % _y;
break;
}
}
void operator()()
{
run();
}
string GetTask()//打印任务信息
{
string r = to_string(_x);
r += _op;
r += to_string(_y);
r += "=?";
return r;
}
string GetResult()//打印处理任务后的结果
{
string r = to_string(_x);
r += _op;
r += to_string(_y);
r += "=" + to_string(_result);
r += " exitcode:" + to_string(_exitcode);
return r;
}
private:
int _x;
int _y;
int _op;
int _result = 0;//处理任务的结果
int _exitcode = 0;//该结果是否可信
};
ThreadPool.hpp文件
#include <vector>
#include <string>
#include <pthread.h>
#include <queue>
#include "Task.hpp"
using namespace std;
template<class T>
class ThreadPool;
template<class T>
struct pthreadInfo
{
pthreadInfo(pthread_t tid = 0, string threadname = "")
:_tid(tid), _threadname(threadname)
{}
pthread_t _tid;
string _threadname;
};
static const int defaultNum = 5;//线程池默认的个数
template<class T>
class ThreadPool
{
void Lock()
{
pthread_mutex_lock(&_mutex);
}
void Unlock()
{
pthread_mutex_unlock(&_mutex);
}
void ThreadSleep()
{
pthread_cond_wait(&_cond, &_mutex);
}
void ThreadWakeUp()
{
pthread_cond_signal(&_cond);
}
string GetThreadName(pthread_t tid)
{
for (auto& thread : _threads)
{
if (thread._tid == tid)
{
return thread._threadname;
}
}
return nullptr;
}
public:
ThreadPool(int num = defaultNum)
:_threads(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
// 如果不加static该函数的参数列表中就会有隐藏的this指针,如果加了static不能访问类内成员了(因为静态成员不能访问非静态成员)
static void* HandlerTask(void* args)
{
ThreadPool<T>* tp = (ThreadPool<T>*)args;
while (1)
{
tp->Lock();
while (tp->_tasks.empty())
{
tp->ThreadSleep();
}
T task = tp->Pop();
tp->Unlock();
task();
cout <<"[" << tp->GetThreadName(pthread_self()) << "]" << task.GetResult() << endl;
}
}
void start()
{
for (int i = 0; i < _threads.size(); ++i)
{
_threads[i]._threadname = "threadname-";
_threads[i]._threadname += to_string(i + 1);
pthread_create(&_threads[i]._tid, nullptr, HandlerTask, this);
}
}
void Push(const T& in)
{
Lock();
_tasks.push(in);
ThreadWakeUp();
Unlock();
}
T Pop()
{
T task = _tasks.front();
_tasks.pop();
return task;
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
private:
vector<pthreadInfo<T>> _threads; //所有的线程存储在vector里面
queue<T> _tasks;//线程要竞争的任务队列
pthread_mutex_t _mutex;//保证竞争的互斥性
pthread_cond_t _cond;//条件变量:有任务才会竞争
};
main.cc文件
#include "ThreadPool.hpp"
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
ThreadPool<Task>* tp = new ThreadPool<Task>(10);
tp->start();
while (1)
{
sleep(1);
int x = rand() % 50 + 1;
int y = rand() % 50;
string oper = "+-*/%";
char op = oper[rand() % 5];
Task t(x, y, op);
tp->Push(t);
cout << "派发任务:" << t.GetTask() << endl;
}
return 0;
}
运行结果
线程的封装
Thread.hpp文件
#include <pthread.h>
#include <functional>
using namespace std;
class Thread
{
using func_t = function<void*(void*)>;
static void* start_routine(void* args)
{
Thread* _this = (Thread*)args;
return _this->_func(args);
}
public:
Thread(func_t func, void* args = nullptr)
:_func(func), _args(args)
{
pthread_create(&_tid, nullptr, start_routine, this);
}
void join()
{
pthread_join(_tid, nullptr);
}
private:
pthread_t _tid;
func_t _func;
void* _args;
};
Thread.cc文件
#include <iostream>
#include <unistd.h>
#include "Thread.hpp"
using namespace std;
void* run(void* args)
{
while (1)
{
printf("hello world\n");
sleep(1);
}
}
int main()
{
Thread t(run);
t.join();
return 0;
}
运行结果
这里是简洁版线程的封装。更具体C++线程库在C++线程库详解
STL,智能指针和线程安全
STL中的容器(如vector、list、map、set、unordered_map、unordered_set等等)是否是线程安全的?
不是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响。而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全
智能指针是否是安全的?
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
线程安全的单例模式
什么是设计模式呢?
计算机多年的发展以来,发现很多大神写出来的代码发现都很相似,所以就提取出来了一个模板—其中有一个就叫做单例模式。
在现实编写代码的时候,某些类只需要有一个对象即可。如服务器开发的时候,读取配置文件(如线程池组件,线程池,进程池等)只需要加载一次就可以了
单例模式的实现方法有两种方式:1.懒汉模式 2.饿汉模式
他们的具体思想是什么呢?给大家举个例子:
吃完饭,立刻洗碗,这种就是饿汉模式 。因为下一顿就立马能拿碗吃饭
吃完饭,不洗碗,下一顿吃饭的时候再去洗碗,这就是懒汉模式
懒汉模式最核心的思想就是“延时加载”,从而能够优化服务器启动速度—只是局部变快了,整体是没变的
饿汉实现方式
template <typename T>
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
懒汉实现方式
template <typename T>
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL) {
inst = new T();
}
return inst;
}
};
以懒汉模式为例,把线程池改成懒汉懒汉模式
ThreadPool.hpp文件
#include <vector>
#include <string>
#include <pthread.h>
#include <queue>
#include "Task.hpp"
using namespace std;
template<class T>
class ThreadPool;
template<class T>
struct pthreadInfo
{
pthreadInfo(pthread_t tid = 0, string threadname = "")
:_tid(tid), _threadname(threadname)
{}
pthread_t _tid;
string _threadname;
};
static const int defaultNum = 5;//线程池默认的个数
template<class T>
class ThreadPool
{
void Lock()
{
pthread_mutex_lock(&_mutex);
}
void Unlock()
{
pthread_mutex_unlock(&_mutex);
}
void ThreadSleep()
{
pthread_cond_wait(&_cond, &_mutex);
}
void ThreadWakeUp()
{
pthread_cond_signal(&_cond);
}
string GetThreadName(pthread_t tid)
{
for (auto& thread : _threads)
{
if (thread._tid == tid)
{
return thread._threadname;
}
}
return nullptr;
}
public:
// 如果不加static该函数的参数列表中就会有隐藏的this指针,如果加了static不能访问类内成员了(因为静态成员不能访问非静态成员)
static void* HandlerTask(void* args)
{
ThreadPool<T>* tp = (ThreadPool<T>*)args;
while (1)
{
tp->Lock();
T task = tp->Pop();
tp->Unlock();
task();
cout <<"[" << tp->GetThreadName(pthread_self()) << "]" << task.GetResult() << endl;
}
}
void start()
{
for (int i = 0; i < _threads.size(); ++i)
{
_threads[i]._threadname = "threadname-";
_threads[i]._threadname += to_string(i + 1);
pthread_create(&_threads[i]._tid, nullptr, HandlerTask, this);
}
}
void Push(const T& in)
{
Lock();
_tasks.push(in);
ThreadWakeUp();
Unlock();
}
T Pop()
{
while (_tasks.empty())
{
ThreadSleep();
}
T task = _tasks.front();
_tasks.pop();
return task;
}
static ThreadPool<T> *GetInstance(int num = defaultNum)
{
//if (nullptr == tp_)
//{
//pthread_mutex_lock(&lock_);
if (nullptr == tp_)
{
tp_ = new ThreadPool<T>(num);
}
//pthread_mutex_unlock(&lock_);
//}
return tp_;
}
private:
//因为为单例模式:所以在类外不能使用构造和析构函数
//也不能使用拷贝构造和赋值重载函数
ThreadPool(int num = defaultNum)
:_threads(num)
{
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_cond, nullptr);
}
ThreadPool(const ThreadPool&) = delete;
ThreadPool operator=(const ThreadPool<T>& ) = delete;
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);
}
vector<pthreadInfo<T>> _threads; //所有的线程存储在vector里面
queue<T> _tasks;//线程要竞争的任务队列
pthread_mutex_t _mutex;//保证竞争的互斥性
pthread_cond_t _cond;//条件变量:有任务才会竞争
static ThreadPool<T>* _tp;
//static pthread_mutex_t lock_;
};
template<class T>
ThreadPool<T>* ThreadPool<T>::_tp = nullptr;
// template <class T>
// pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
main.cc文件
#include "ThreadPool.hpp"
#include <iostream>
#include <unistd.h>
using namespace std;
int main()
{
ThreadPool<Task>::GetInstance()->start();
while (1)
{
sleep(1);
int x = rand() % 50 + 1;
int y = rand() % 50;
string oper = "+-*/%";
char op = oper[rand() % 5];
Task t(x, y, op);
ThreadPool<Task>::GetInstance()->Push(t);
cout << "派发任务:" << t.GetTask() << endl;
}
return 0;
}
这样写会产生一个问题:如果获取单例对象的时候,也是多线程获取的呢?那必然可能会产出多个对象,那怎么解决这个问题呢?获取单例的时候加锁。那又会产生一个问题:如果加锁了,那每次获取单例对象的时候都需要加锁判断,为串行访问了,会很影响效率,如何解决呢?再在外面套一层判断即可,具体实现代码为将以上所有代码的注释给去掉。
其它各种常见的锁
悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
自旋锁:
我们经常使用锁的场景就是挂起等待;自旋锁不会挂起等待,而是不断的trylock该锁,看是否能够获取。对于这两种情况,是什么决定了最终的等待方式?我们要等的时长—具体场景具体分析
接口的使用几乎和普通的锁一模一样
读者写者问题
什么是读者写者问题?在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率
也符合“321”原则:三种关系,两个角色,一个交易场所。
给大家举个例子,在学校的时候,要写黑板报,写黑板报的同学不能不顾及其它写黑板报的同学,我想在哪里写就在哪里写,这样会覆盖别人的数据。所以写者和写者是互斥关系。写黑板报正在写的时候你就读,你很可能会获得到错误的信息,必须我写完你再读,有一定的顺序,所以写者和读者是互斥同步关系,读者和读者之前可以我看我的你看你的,互不影响,所以读者和读者之间没有关系
有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
读锁和写锁都用这个接口来解锁
读写锁理解