第八章 多线程
线程概念
多进程任务处理是同时通过多个进程进行任务,多个pcb拥有多个虚拟地址空间,分别执行不同的代码,之间互不关联。而多线程是通过多个pcb共用一个虚拟地址空间,分别执行虚拟地址空间上所对应的多个不同的物理内存中的代码。即一个虚拟地址空间对应多个物理内存。
之前我们说linux下pcb是一个进程,但其实linux下线程以进程pcb模拟实现线程,因此linux下pcb是线程;因此linux线程也叫轻量级进程。一个进程可能拥有多个线程,而每个进程势必有一个主线程,我们在主线程中创建其他线程。那么一个进程可以理解为一堆线程的集合,我们称其为线程组,而进程的pid为了不冲突则规定是主线程的pid。
因为linux线程是pcb——因此线程是cpu的基本单位。因为进程是线程组,程序运行起来,资源是分配给整个线程组的,因此进程是资源分配的基本单位。
进程与线程的对比
一个进程中的线程共用同一个虚拟地址空间,因此线程间通信更加方便;线程的创建/销毁成本更低;线程间切换调度成本更低;线程的执行粒度更细。
线程之间缺乏访问控制——系统调用,异常针对的是整个进程,健壮性低。
vfork
创建一个子进程共用同一个虚拟地址空间,怕出现调用栈混乱,因此子进程运行完毕或程序替换后父进程才开始运行。而线程也共用同一个虚拟地址空间却不会发生调用栈混乱的情况,因为每个线程都会有一些独立的信息,会为每个线程在虚拟地址空间中单独分配一块内存用来存储这些独立的信息:栈,寄存器,errno,信号屏蔽字,调度优先级。同时线程间也有共享的数据:代码段,数据段,文件描述符表,信号处理方式,用户和组,当前工作目录。
多线程相比多进程的优点:
1、通信更加方便,灵活。
2、创建/销毁成本更低。
3、切换调度成本更低。
多线程相比多进程的缺点:
1、缺乏访问控制并且一些系统调用以及错误针对整个进程,健壮性/稳定性更低。
多进程/多线程进行多任务处理的优势
cpu密集型程序
对于读写操作比较少,更多的则是计算方面的操作,这类程序尽量少用多线程/进程,因为cpu调度线程/进程会浪费cpu资源。
io密集型程序
对于读写操作较多,cpu计算操作较少的程序则应该多使用多进程/线程进行io操作,由此来并行执行程序,减少执行时间。
线程控制
线程创建
操作系统并没有为用户提供直接创建线程的系统调用接口,但是有人自己封装了一套线程库实现线程控制。
pthread_create
由于pthread_create
所在的库pthread
并不在gcc默认的链接库中,因此我们在编译时要加参数-pthread
或者-lpthread
让其连接到这个库中。
/**
* 线程创建
**/
/**
* int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
* void *(*start_routine) (void *), void *arg);
* thread:输出型参数,获取新创建的线程id
* attr: 设置线程属性,通常置空
* start_routine: 线程入口函数
* arg:通过线程入口函数传递给线程的参数
**/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void* thr_start(void* arg)
{
while(1)
{
//pthread_self查看此线程的tid
printf("i am child---%d\n",pthread_self());
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
printf("%d\n",tid);
if(ret != 0)//0为成功
{
printf("thread vreate errno!\n");
return -1;
}
while(1)
{
//thread_self查看自己的线程id
printf("Misaki!%d\n",getpid());
sleep(1);
}
}
[misaki@localhost 第八章-多线程]$ ./create
-1544186112
Misaki!5429
i am child----1544186112
i am child----1544186112
Misaki!5429
i am child----1544186112
Misaki!5429
i am child----1544186112
Misaki!5429
i am child----1544186112
Misaki!5429
i am child----1544186112
这个创建线程的函数中的返回值tid
为线程在虚拟地址空间上所分配的属于自己的独立空间的首地址,我们以后要靠这个参数来控制线程。一个tid
唯一的表示一个线程。
线程终止
在线程入口函数中return
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
void* thr_start(void* arg)
{
while(1)
{
printf("i am child\n");
reutrn NULL;
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
printf("%d\n",tid);
if(ret != 0)
{
printf("thread vreate errno!\n");
return -1;
}
while(1)
{
//thread_self查看自己的线程id
printf("Misaki!%d\n",getpid());
sleep(1);
return 0;
}
}
[misaki@localhost 第八章-多线程]$ ./exit
2052687616
Misaki!5710
在线程入口函数中return
会让线程退出。当在主函数中使用return
退出主函数的时候这时会导致进程终止,由此进程中的所有线程都会终止。
pthread_exit()
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void* thr_start(void* arg)
{
while(1)
{
printf("i am child---%s\n", arg);
sleep(1);
//退出调用这个函数的线程
pthread_exit(0);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
while(1)
{
printf("i am main!\n");
sleep(1);
}
}
[misaki@localhost 第八章-多线程]$ ./exit
i am main!
i am child---Misaki
i am main!
i am main!
i am main!
可以看出我们自己创建的线程在执行pthread_exit()
后退出了。如果我们的主线程调用这个函数会怎样呢?
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void* thr_start(void* arg)
{
while(1)
{
printf("i am child---%s\n", arg);
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
if(ret != 0)
{
printf("thread create error\n");
return -1;
}
while(1)
{
printf("i am main!\n");
sleep(1);
//退出调用这个函数的线程
pthread_exit(0);
}
}
[misaki@localhost 第八章-多线程]$ ./exit
i am main!
i am child---Misaki
i am child---Misaki
i am child---Misaki
i am child---Misaki
i am child---Misaki
可以看出我们虽然在主线程中调用了退出函数,主线程也确实退出了,但是进程却并没有退出,这说明,主线程终止并不会让进程终止。但是我们要注意线程退出也会成为僵尸线程,但是普通线程退出并不会有过于明显大的影响。
pthread_cancel
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void* thr_start(void* arg)
{
while(1)
{
printf("i am child---%s\n", arg);
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, (void*)"Misaki");
if(ret != 0)
{
printf("thread create error\n");
return -1;
}
while(1)
{
printf("i am main!\n");
sleep(1);
//退出id = tid的线程
pthread_cancel(tid);
}
}
[misaki@localhost 第八章-多线程]$ ./exit
i am main!
i am child---Misaki
i am child---Misaki
i am main!
i am main!
i am main!
线程等待
线程等待是为了获取指定线程的返回值,和进程等待一样为了让系统可以释放资源,因为一个线程运行起来,默认有一个属性:joinable
。这个属性决定了线程退出后,必须被等待,否则线程资源无法完全释放,成为僵尸线程,因此我们必须进行线程等待,获取线程返回值,允许系统释放资源。当然线程等待也有一个前提,线程能够被等待,即joinable
属性。
pthread_join()
/**
* int pthread_join(pthread_t thread, void **retval);
* 线程等待,获取线程退出返回值。
* thread:要等待的线程id
* retval:输出型参数,用于获取退出线程的返回值
**/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
void* thr_start(void* arg)
{
sleep(3);
return (void*)"Misaki";
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, NULL);
if(ret != 0)
{
printf("thread create error\n");
return -1;
}
char* ptr;
pthread_join(tid, (void**)&ptr);
printf("%s\n", ptr);
}
[misaki@localhost 第八章-多线程]$ ./join
Misaki
如果一个线程是被取消,则返回值是一个宏:PTHREAD_CANCELED
,它的值是-1。线程等待pthread_join
是阻塞函数,一个一个线程没有推出则会一直等待。
线程分离
将线程的一个属性从joinable
设置为detach
属性。属于detach
属性的线程,退出后资源直接自动被回收,这类线程不能被等待。
pthread_detach()
如果用户对一个线程的返回值不关心,则可以在线程入口函数对线程进行分离。
/**
* int pthread_detach(pthread_t thread);
* 线程分离。
* thread:要分离的线程id
**/
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <errno.h>
void* thr_start(void* arg)
{
//分离自己这个线程
//线程的分离对于一个线程来说,任意线程在任意位置调用都可以
// pthread_detach(pthread_self());
return (void*)"Misaki";
}
int main()
{
pthread_t tid;
int ret = pthread_create(&tid, NULL, thr_start, NULL);
if(ret != 0)
{
printf("thread create error\n");
return -1;
}
//分离这个线程
pthread_detach(tid);
char* ptr;
ret = pthread_join(tid, (void**)&ptr);
//如果一个线程无法被等待则返回值为一个宏EINVAL
if(ret == EINVAL)
{
printf("this thread can not be wait!!\n");
return -1;
}
printf("%s\t%d\n", ptr, ret);
}
[misaki@localhost 第八章-多线程]$ ./join
this thread can not be wait!!
会发现我们已经分离了我们自己创建的线程,这个线程已经无法被等待了,并且我们无法接收到线程的返回值。
线程安全
多个线程同时操作临界资源而不会出现数据二义性就说这个线程是安全的。如果在线程中进行了非原子性操作就可能会导致线程不安全,这些非原子性操作也叫做不可重入函数,即多个执行流中同时进入函数运行会出现问题的函数。
如何实现线程安全?这就要靠同步与互斥。同步指临界资源的合理访问,互斥指临界资源同一时间唯一访问。
互斥
同步和互斥要如何实现呢?我们先从互斥开始讨论。为了保证操作的原子性,在C语言中互斥锁可以帮助我们保证互斥,使我们的函数变为可重入函数。
互斥锁
互斥锁的值只能为0或1。1表示可以加锁,加锁后值-1,操作结束后就会解锁,解锁就会将值+1。如果一个操作已经加锁则值为0,因此当锁值为0时其他线程则不能加锁,不能加锁线程就会陷入等待。
互斥锁操作步骤:
1、定义互斥锁变量:pthread_mutex_t
。
2、初始化互斥锁变量:pthread_mutex_init
。
3、加锁:pthread_mutex_lock
。
4、解锁:pthread_mutex_unlock
。
5、删除锁:pthread_mutex_destroy
。
接下来我用互斥锁将一个不可重入的函数使它可重入从而使多个线程同时运行函数时变得安全。
/*实现互斥锁的基本使用以及线程安全的基本认识*/
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int ticket = 100;
//互斥锁变量不一定非要全局变量,使用的线程都能访问到就行
//互斥锁变量
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int ticket = 100;
pthread_mutex_t mutex;
void* ticket_scalper(void* arg)
{
int id = (int)arg;
while(1)
{
//加锁要在临界资源访问之前
//int pthread_mutex_lock(pthread_mutex_t* mutex);阻塞加锁
//int pthread_mutex_trylock(pthread_mutex_t* mutex);非阻塞加锁,加不上锁就返回
pthread_mutex_lock(&mutex);
if(ticket > 0)
{
printf("scalper:%d--get a ticket:%d\n", id, ticket);
ticket--;
usleep(1000);
}
else
{
//解锁
pthread_mutex_unlock(&mutex);
pthread_exit(0);
}
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
int i = 0;
int ret;
pthread_t tid[4];
//初始化互斥锁
//int pthread_mutex_init(pthread_mutex_t *restrict mutex,
// const pthread_mutexattr_t *restrict attr);
//
pthread_mutex_init(&mutex, NULL);
//创建线程
for(i = 0; i < 4; i++)
{
ret = pthread_create(&tid[i], NULL, ticket_scalper, (void*)i);
if(ret != 0)
{
perror("thread creat error:");
return -1;
}
}
for(i = 0; i < 4; i++)
{
pthread_join(tid[i], NULL);
}
//销毁互斥锁
//int pthread_mutex_destroy(pthread_mutex_t *mutex);
pthread_mutex_destroy(&mutex);
}
[misaki@localhost thread_2019_9_2_class45]$ ./main
scalper:2--get a ticket:100
scalper:2--get a ticket:99
scalper:2--get a ticket:98
scalper:2--get a ticket:97
scalper:2--get a ticket:96
scalper:2--get a ticket:95
scalper:3--get a ticket:94
scalper:3--get a ticket:93
scalper:3--get a ticket:92
scalper:3--get a ticket:91
scalper:3--get a ticket:90
scalper:3--get a ticket:89
scalper:3--get a ticket:88
scalper:3--get a ticket:87
scalper:3--get a ticket:86
scalper:3--get a ticket:85
scalper:3--get a ticket:84
scalper:3--get a ticket:83
scalper:3--get a ticket:82
scalper:3--get a ticket:81
scalper:3--get a ticket:80
scalper:3--get a ticket:79
scalper:3--get a ticket:78
scalper:3--get a ticket:77
scalper:3--get a ticket:76
scalper:3--get a ticket:75
scalper:3--get a ticket:74
scalper:3--get a ticket:73
scalper:3--get a ticket:72
scalper:3--get a ticket:71
scalper:3--get a ticket:70
scalper:3--get a ticket:69
scalper:3--get a ticket:68
scalper:3--get a ticket:67
scalper:3--get a ticket:66
scalper:3--get a ticket:65
scalper:3--get a ticket:64
scalper:3--get a ticket:63
scalper:3--get a ticket:62
scalper:3--get a ticket:61
scalper:3--get a ticket:60
scalper:3--get a ticket:59
scalper:3--get a ticket:58
scalper:3--get a ticket:57
scalper:3--get a ticket:56
scalper:3--get a ticket:55
scalper:3--get a ticket:54
scalper:3--get a ticket:53
scalper:3--get a ticket:52
scalper:3--get a ticket:51
scalper:3--get a ticket:50
scalper:3--get a ticket:49
scalper:3--get a ticket:48
scalper:3--get a ticket:47
scalper:3--get a ticket:46
scalper:3--get a ticket:45
scalper:3--get a ticket:44
scalper:3--get a ticket:43
scalper:3--get a ticket:42
scalper:3--get a ticket:41
scalper:3--get a ticket:40
scalper:3--get a ticket:39
scalper:3--get a ticket:38
scalper:3--get a ticket:37
scalper:3--get a ticket:36
scalper:3--get a ticket:35
scalper:3--get a ticket:34
scalper:3--get a ticket:33
scalper:3--get a ticket:32
scalper:3--get a ticket:31
scalper:3--get a ticket:30
scalper:3--get a ticket:29
scalper:3--get a ticket:28
scalper:3--get a ticket:27
scalper:3--get a ticket:26
scalper:3--get a ticket:25
scalper:3--get a ticket:24
scalper:3--get a ticket:23
scalper:3--get a ticket:22
scalper:3--get a ticket:21
scalper:3--get a ticket:20
scalper:3--get a ticket:19
scalper:3--get a ticket:18
scalper:3--get a ticket:17
scalper:3--get a ticket:16
scalper:3--get a ticket:15
scalper:3--get a ticket:14
scalper:3--get a ticket:13
scalper:3--get a ticket:12
scalper:3--get a ticket:11
scalper:3--get a ticket:10
scalper:3--get a ticket:9
scalper:3--get a ticket:8
scalper:3--get a ticket:7
scalper:3--get a ticket:6
scalper:3--get a ticket:5
scalper:3--get a ticket:4
scalper:3--get a ticket:3
scalper:3--get a ticket:2
scalper:3--get a ticket:1
这样就达成了互斥,在一个线程操作临界资源时,其他线程不会同时干涉。
死锁
死锁是指因为对一些无法加锁的锁进行加锁操作而导致程序卡死。死锁是我们一定要在使用锁时要注意和避免的
死锁产生的四个必要条件:
1、互斥条件。一个线程操作时其他线程不能操作。
2、不可剥夺条件。一个线程加的锁别的线程不能释放。
3、请求与保持条件。一个线程已经有了锁却还在请求其他的锁,但是其他的锁请求不到第一个锁也不释放。
4、环路等待条件。
死锁产生往往是因为加锁解锁的顺序不同。要想避免死锁就要避免死锁产生的四个必要条件——死锁检测算法,银行家算法。
同步
通过对当前是否满足对临界资源的操作条件来判断线程是否该等待或唤醒这种方式实现对临界资源访问的合理性。资源产生后才能进行使用,没有资源则等待资源产生,生产资源后则唤醒等待,这样则达成同步。然而互斥锁虽然可以帮助我们完成等待但是无法判断何时将我们唤醒,不能在合适的事件唤醒,因此便要借助新的东西——条件变量。
条件变量
条件变量的使用流程:
1、定义条件变量:pthread_cond_t
。
2、初始化条件变量:pthread_cond_init
。
3、等待或者唤醒:pthread_cond_wait/pthread_cond_signal
。
4、销毁条件变量:pthread_cond_destroy
。
pthread_cond_wait
中一共有三个操作,首先它要让让当前线程等待,但是此时有一点,此时的互斥量还处于加锁状态其他线程无法操作临界资源,呢又怎么做到让临界资源达到要求呢?因此他在让线程等待前要先解除了互斥量的加锁状态,并且这两部操作为原子操作。为什么要是原子操作?因为如果不是原子操作有可能在解锁后已经条件满足而此时线程还未进行等待可能会忽略唤醒。之后在线程被唤醒后pthread_cond_wait
还会再加锁保证互斥。这就是三部操作:解锁->等待->唤醒后加锁
。
在每一个条件变量内部都有一个等待队列,将所有等待的线程排列在上面,如果有其他线程唤醒则逐一唤醒。
接下来我们用互斥锁加条件变量模拟实现一个顾客去餐馆吃饭的情景,但是在这个情境中为了符合设计要注意两个顾客不能同时吃一碗饭,并且只有一个锅因此两个厨师不能同时做饭。如果没饭了2个厨师中其中一个做饭,又犯了2个顾客其中一个吃饭。
/*实现条件变量的基本使用*/
/*吃面前提有人吃面,如果没有线程的面,等待老板做出来
* 老板做出来面就要唤醒顾客
* 老板不会做太多的面,老板只会提前做一碗面
* 如果已经有面做出来,但是没人吃,不会再做(等待)
* 顾客吃完面后,老板再来一碗(唤醒老板的等待)*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <pthread.h>
//是否右面
int have_noodle = 1;
//为了让客人与厨师间的不同的同步性,需要定义多个条件变量
pthread_cond_t customer;
pthread_cond_t boss;
pthread_mutex_t mutex;
//老板做面
void* thr_boss(void* arg)
{
while(1)
{
pthread_mutex_lock(&mutex);
//由于多个顾客,为了避免两个顾客吃一碗面的情况这里要循环判断
while(have_noodle == 1)//有面
{
//等待
//int pthread_cond_timedwait(pthread_cond_t *restrict cond,
// pthread_mutex_t *restrict mutex,
// const struct timespec *restrict abstime);
//限时等待
//cond:条件变量
//mutex:互斥锁
//abstime:限时等待时长
//时间到后返回时间超市,停止阻塞
//int pthread_cond_wait(pthread_cond_t *restrict cond,
// pthread_mutex_t *restrict mutex);
//cond:条件变量
//mutex:互斥锁
//pthread_cond_wait 集合了解锁后挂起的操作(原子操作,不可被打断)
//有可能还没来得及挂起就已经有人唤醒,白唤醒,导致死等
//因此这里的wait将三个操作进行了原子性封装不让其中断
//解锁 -》 等待 -》 被唤醒后加锁
pthread_cond_wait(&boss, &mutex);
}
//面没了,要再做
printf("拉面 + 1\n");
have_noodle = 1;
//面好了,唤醒顾客
pthread_cond_signal(&customer);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
//顾客吃面
void* thr_customer(void* arg)
{
while(1)
{
while(have_noodle == 0)
{
//若没有现成的面等老板做好
//等待
pthread_cond_wait(&customer, &mutex);
}
//有面了
printf("真好吃!\n");
have_noodle -= 1;
//唤醒厨师再做一碗
pthread_cond_signal(&boss);
//解锁
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_t tid1, tid2;
int ret;
//条件变量初始化
//int pthread_cond_init(pthread_cond_t *restrict cond,
// const pthread_condattr_t *restrict attr);
pthread_cond_init(&boss, NULL);
pthread_cond_init(&customer, NULL);
pthread_mutex_init(&mutex, NULL);
//各建立两个线程同时工作,相当于两个厨师两个客人
//客人间具有互斥性,厨师间也有互斥性,客人与厨师间有同步与互斥性
for(int i = 0; i < 2; i++)
{
ret = pthread_create(&tid1, NULL, thr_boss, NULL);
if(ret != 0)
{
printf("boss error");
return -1;
}
}
for(int i = 0; i < 2; i++)
{
ret = pthread_create(&tid2, NULL, thr_customer, NULL);
if(ret != 0)
{
printf("customer error");
return -1;
}
}
pthread_join(tid1, NULL);
pthread_join(tid2, NULL);
//销毁条件变量
//int pthread_cond_destroy(pthread_cond_t *cond);
pthread_cond_destroy(&customer);
pthread_cond_destroy(&boss);
//销毁锁
pthread_mutex_destroy(&mutex);
}
真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
真好吃!
拉面 + 1
^C真好吃!
在以上这个例子中要注意几个点:
1、用户对条件判断需要使用循环进行判断(防止角色不符合条件被唤醒之后因为不循环判断直接操作临界资源)。这个问题也被称为虚假唤醒问题。在多核处理器下,pthread_cond_signal
可能会激活多于一个线程(阻塞在条件变量上的线程)。结果就是,当一个线程调用pthread_cond_signal()
后,多个调用pthread_cond_wait()
或其他等待在队列上的线程返回。这种效应就会造成虚假唤醒。
2、不同角色的线程因该等待在不同的条件变量上。(防止角色的误唤醒,导致程序阻塞)
但是要注意条件变量并不保证安全,因此往往使用条件变量的时候会与互斥锁共同使用。面生产一碗顾客吃一碗没有出现异常,因此实现是成功的。这种在多线程情况下有人生产数据有人消费数据利用同步与互斥达到合理与安全的模式十分经典,因此产生了一种固定的设计模型,这就是生产者消费者模型。
生产者与消费者模型
生产者与消费者模型中有两种角色:生产者与消费者,同时包含三种关系:生产者与生产者之间互斥,消费者与消费者之间互斥,生产者与消费者之间同步与互斥。他们工作在一个场景中,这个场景通常是一个队列,用来保存数据。
实现
/**
* 基于互斥锁与条件变量实现一个线程安全的队列
* 实现生产者与消费者模型
**/
#include <iostream>
#include <queue>
#include <pthread.h>
#define MAXQ 10
class BlockQueue
{
public:
BlockQueue(int maxq = MAXQ)
:_capacity(maxq)
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_cond_consumer, NULL);
pthread_cond_init(&_cond_productor, NULL);
}
~BlockQueue()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond_consumer);
pthread_cond_destroy(&_cond_productor);
}
bool QueuePush(int data)
{
pthread_mutex_lock(&_mutex);
while(_queue.size() == _capacity)
{
pthread_cond_wait(&_cond_productor, &_mutex);
}
_queue.push(data);
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond_consumer);
return true;
}
bool QueuePop(int &data)
{
pthread_mutex_lock(&_mutex);
while(_queue.empty())
{
pthread_cond_wait(&_cond_consumer, &_mutex);
}
data = _queue.front();
_queue.pop();
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond_productor);
return true;
}
private:
std::queue<int> _queue;
int _capacity;
pthread_mutex_t _mutex;
pthread_cond_t _cond_productor;
pthread_cond_t _cond_consumer;
};
void* thr_consumer(void* arg)
{
BlockQueue* q = (BlockQueue*)arg;
int data;
while(1)
{
//消费者一直获取数据进行打印
q->QueuePop(data);
std::cout << "consumer gets a piece of data--" << data << std::endl;
}
}
void* thr_productor(void* arg)
{
BlockQueue* q = (BlockQueue*)arg;
int data = 0;
while(1)
{
//生产者一直添加数据
q->QueuePush(data);
std::cout << "producer produces a data--" << (data++) << std::endl;
}
return NULL;
}
int main()
{
pthread_t ctid[4], ptid[4];
int ret, i;
BlockQueue q;
for(i = 0; i < 4; i++)
{
ret = pthread_create(&ctid[i], NULL, thr_consumer, (void*)&q);
if(ret != 0)
{
std::cerr << "create thread error" << std::endl;
}
}
for(i = 0; i < 4; i++)
{
ret = pthread_create(&ptid[i], NULL, thr_productor, (void*)&q);
if(ret != 0)
{
std::cerr << "create thread error" << std::endl;
}
}
for(i = 0; i < 4; i++)
{
pthread_join(ctid[i], NULL);
pthread_join(ptid[i], NULL);
}
}
consumer gets a piece of data--2111
consumer gets a piece of data--2112
consumer gets a piece of data--2113
consumer gets a piece of data--2114
consumer gets a piece of data--2115
consumer gets a piece of data--2116
consumer gets a piece of data--2117
consumer gets a piece of data--2118
producer produces a data--2119
producer produces a data--2120
producer produces a data--2121
producer produces a data--2122
producer produces a data--2123
producer produces a data--2124
这里打印之所以看上去乱是因为xshell的显示跟不上虚拟机计算的速度。
优点
生产者与消费者模型有三个优点:
1、解耦合
2、支持忙闲不均
3、支持并发
一个场所,两种角色,三种关系。
posix标准信号量
system V
是内核中的计数器,posix
是线程间的全局计数器。它也有着实现进程/进程间同步与互斥的贡藕功能。
与条件变量的区别
条件变量是通过等待、唤醒操作来让线程等待在等待队列上来完成同步,这需要用户自己进行外部条件判断并且要搭配互斥锁一起使用。
信号量是通过自身内部技术实现条件的判断,不需要搭配互斥锁,自身已经保证了原子操作。
信号量的工作原理
信号量通过一个计数器实现对资源的计数,并且通过这个计数来判断当前线程/进程能否对临界资源进行访问,对临界资源进行访问之前先发起调用访问信号量进行判断是否能够访问。
信号量实现同步:首先资源计数-1,若此时资源计数>=0
,则可以直接进行访问,调用直接返回,若信号量内部计数器<0
表示没有资源无法访问,调用阻塞(挂起线程);若其他线程生产了一个资源则发起调用,首先资源计数+1,如果此时计数器<=0
则唤醒等待队列上的线程,若此时计数器>0
则什么都不做。
信号量实现互斥:计数只有0/1,资源只有一个,同一时间只有 一个线程可以访问。首先信号量-1,若此时信号量<0
则调用阻塞,若>0
,则调用返回,对临界资源进行访问,访问完毕,进行计数+1,唤醒所有线程,所有线程继续进行抢夺。
同时如果信号量小于0则表示当前阻塞在等待队列上的线程/进程数,等于0表示资源刚好完全分配,大于0则表示多余资源数。
接口
sem_t//定义信号量。
sem_init(sem_t* sem, int flag, int initval);//初始化,
//flag:0-线程间,!0-进程间
//initval:用于设置初值
sem_wait(sem_t* sem);//,进行判断是否有资源,<=0则阻塞,>0则-1并调用返回并。
sem_trywait(sem_t* sem);//非阻塞,没有资源直接报错返回。
sem_timedwait(sem_t* sem);//限时阻塞,等待一段时间,若一直没有资源则超时报错返回
sem_post(sem_t* sem);//计数+1,并且唤醒等待的线程
sem_destroy(sem_t* sem);//销毁信号量
应用
用信号量实现线程安全的环形队列。
/**
* 利用信号量完成线程安全的环形队列
**/
#include <iostream>
#include <vector>
#include <semaphore.h>
#include <thread>
#define MAXQ 10
class RingQueue
{
public:
RingQueue(int maxq = MAXQ)
:_capacity(maxq)
,_queue(maxq)
,_step_read(0)
,_step_write(0)
{
sem_init(&_lock, 0, 1);
sem_init(&_idle_space, 0, maxq);
sem_init(&_data_space, 0, 0);
}
~RingQueue()
{
sem_destroy(&_lock);
sem_destroy(&_idle_space);
sem_destroy(&_data_space);
}
bool QueuePush(int data)
{
//没有空闲空间则阻塞
sem_wait(&_idle_space);
//加锁
sem_wait(&_lock);
_queue[_step_write] = data;
_step_write = (_step_write + 1) % _capacity;
//解锁
sem_post(&_lock);
//唤醒消费者
sem_post(&_data_space);
return true;
}
bool QueuePop(int& data)
{
sem_wait(&_data_space);
sem_wait(&_lock);
data = _queue[_step_read];
_step_read = (_step_read + 1) % _capacity;
sem_post(&_lock);
sem_post(&_idle_space);
return true;
}
private:
std::vector<int> _queue;//用vector实现环形队列
int _capacity;//容量
int _step_read;//读指针
int _step_write;//写指针
sem_t _lock;//初始计数=1,负责完成互斥
//也需要有两个等待队列,分别完成两个角色间的同步
sem_t _idle_space;//空闲空间节点个数,生产者等待在这里,完成同步
sem_t _data_space;//数据节点个数,初始=0,消费者等待在这里,完成同步
};
void thr_producer(RingQueue* q)
{
int data = 0;
while(1)
{
q->QueuePush(data);
std::cout << "push data ----" << data++ << std::endl;
}
}
void thr_consumer(RingQueue* q)
{
int data = 0;
while(1)
{
q->QueuePop(data);
std::cout << "get data ----" << data << std::endl;
}
}
int main()
{
RingQueue q;
std::vector<std::thread> list_con(4);
std::vector<std::thread> list_pro(4);
for(int i = 0; i < 4; i++)
{
list_pro[i] = (std::thread(thr_producer, &q));
}
for(int i = 0; i < 4; i++)
{
list_con[i] = (std::thread(thr_consumer, &q));
}
for(int i = 0; i < 4; i++)
{
list_con[i].join();
list_pro[i].join();
}
}
push data ----4028
get data ----push data ----4102
41004029
3996push data ----4030
push data ----4031
push data ----4032
push data ----push data ----4033
push data ----4034
push data ----4101
push data ----4102
get data ----
push data ----4103
push data ----4104
40974102
get data ----4101
push data ----4103
push data ----get data ----4029
4095
get data ----4031
get data ----4032
get data ----4033
get data ----4034
get data ----4102
get data ----get data ----40304103
push data ----get data ----4104
get data ----4103
get data ----4105
get data ----4104
push data ----3997
push data ----3998
push data ----3999
push data ----4000
push data ----4001
4104get data ----
push data ----4105
push data ----4106
push data ----4107
push data ----4108
push data ----push data ----4035
3997
get data ----3999
get data ----4000
读写锁
读写锁在数据库中就有着极为重要的应用,这样才得以让数据得到共享修改得到合理的保存而不出现数据的二义性。
特点,原理及应用
读写锁有着自己的特点:写互斥,读共享,一个用户写时所有其他所有用户都不能读和写;一个用户读时其他所有用户都可以读但不能写,因此适用于多读少写的应用场景,保证数据不会出现二义性并且保证读取和写入的效率。
读写锁内部有两个计数器,读者计数与写者计数。加写锁时对两个技术进行判断如果任意一个计数器>0,都无法加写锁需要等待。加读锁时对写者计数进行判断,若大于0,则无法加读锁需要进行等待。
读写锁通过自旋锁实现,不满足条件时自旋等待。自旋锁的特点是:等待中不停循环对条件进行判断,因此可以及时响应,但是cpu消耗较高。自旋锁一般应用在对于挂起等待被唤醒时间相较于数据处理时间可以忽略不记的情况下,这样更倾向于挂起。
线程池
为什么要有线程池
假如在一个任务的处理时间中,若线程创建及销毁的时间占用任务处理的大量比例,则意味着大量任务处理中,资源被浪费在线程的创建与销毁中。因此产生线程池,创建大量线程,但并不推出线程并不断把任务交给这些线程处理,避免了大量创建线程/销毁带来的时间成本。
线程池中线程数量是有上限的,为了防止出现峰值压力导致资源瞬间耗尽程序崩溃。
线程池的实现
/**
* 线程池由两个部分构成一个是一个任务类
* 另一个部分是一个线程安全的队列,由此构成任务队列,
* 再用一组线程从任务队列中获取任务来执行
* 由此构成线程池
**/
#include <iostream>
#include <unistd.h>
#include <pthread.h>
#include <queue>
#include <thread>
#include <string>
#include <time.h>
#include <stdlib.h>
#include <sstream>
#define MAX_THREAD 5
#define MAX_QUEUE 10
typedef void(*handler_t)(int val);
//任务类
//1、决定线程处理的任务,处理什么数据,怎么处理都由用户传入
class Task
{
private:
int _data;//数据
handler_t _handler;//处理数据的方法,函数指针,用于传入线程中给线程下达命令
public:
Task(int data, handler_t handler)
:_data(data)
,_handler(handler)
{
}
void SetTask(int data, handler_t handler)
{
_data = data;
_handler = handler;
}
void Run()
{
return _handler(_data);
}
};
//线程池类
class ThreadPool
{
private:
std::queue<Task> _queue;//任务队列
int _capacity;//线程池最大任务数量
pthread_mutex_t _mutex;//锁,完成互斥,类似于生产者消费者模型
pthread_cond_t _cond_pro;//条件变量,完成
pthread_cond_t _cond_con;
int _thr_max;//线程池拥有的总线程数
std::vector<std::thread> _thr_list;//线程组,存储线程操作句柄
bool _quit_flag;//用于控制线程是否退出
int _thr_cur;//线程的数量,线程退出时,判断当前线程数量
void thr_start()
{
while(1)
{
pthread_mutex_lock(&_mutex);
while(_queue.empty())
{
if(_quit_flag == true)
{
std::cout << "thread exit " << pthread_self() << std::endl;
pthread_mutex_unlock(&_mutex);
_thr_cur--;
return;
}
pthread_cond_wait(&_cond_con, &_mutex);
}
Task tt = _queue.front();
_queue.pop();
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond_pro);
//任务处理放到锁外,防止线程处理任务时间过长,一直加锁导致其他线程无法处理其他任务
tt.Run();
}
}
public:
//初始化线程池
ThreadPool(int maxq = MAX_QUEUE, int maxt = MAX_THREAD)
:_capacity(maxq)
,_thr_max(maxt)
,_thr_list(maxt)
,_thr_cur(0)
{
pthread_mutex_init(&_mutex, NULL);
pthread_cond_init(&_cond_pro, NULL);
pthread_cond_init(&_cond_con, NULL);
}
~ThreadPool()
{
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond_pro);
pthread_cond_destroy(&_cond_con);
}
//初始化线程组
bool PoolInit()
{
for(int i = 0; i < _thr_max; i++)
{
_thr_list[i] = std::thread(&ThreadPool::thr_start, this);
_thr_list[i].detach();
_thr_cur++;
}
return true;
}
//添加任务
bool AddTask(Task& tt)
{
pthread_mutex_lock(&_mutex);
while(_queue.size() == _capacity)
{
pthread_cond_wait(&_cond_pro, &_mutex);
}
_queue.push(tt);
pthread_mutex_unlock(&_mutex);
pthread_cond_signal(&_cond_con);
return true;
}
//销毁线程池,停止工作
bool PoolStop()
{
pthread_mutex_lock(&_mutex);
_quit_flag = true;
pthread_mutex_unlock(&_mutex);
while(_thr_cur > 0)
{
//std::cout << "cont:" << _thr_cur << std::endl;
pthread_cond_broadcast(&_cond_con);
usleep(1000);
}
//for(int i = 0; i < _thr_max; i++)
//{
// _thr_list[i].join();
//}
return true;
}
};
void test(int data)
{
srand(time(NULL));
int nsec = rand() % 5;
std::stringstream ss;
ss << "thread:" << pthread_self() << " processint data ";
ss << data << " and sleep " << nsec << " sec" << std::endl;
std:: cout << ss.str();
sleep(nsec);
return;
}
int main()
{
ThreadPool pool;
pool.PoolInit();
for(int i = 0; i < 10; i++)
{
Task tt(i, test);
pool.AddTask(tt);
}
pool.PoolStop();
}
[misaki@localhost thread]$ ./threadpool
thread:139941165012736 processint data 0 and sleep 4 sec
thread:139941156620032 processint data 1 and sleep 4 sec
thread:139941190190848 processint data 2 and sleep 4 sec
thread:139941181798144 processint data 3 and sleep 4 sec
thread:139941173405440 processint data 4 and sleep 4 sec
thread:139941190190848 processint data 6 and sleep 4 sec
thread:139941156620032 processint data 7 and sleep 4 sec
thread:139941181798144 processint data 8 and sleep 4 sec
thread:139941165012736 processint data 5 and sleep 4 sec
thread:139941173405440 processint data 9 and sleep 4 sec
thread exit 139941156620032
thread exit 139941190190848
thread exit 139941181798144
thread exit 139941165012736
thread exit 139941173405440
设计模式
单例模式
单例模式是一种常见设计模式,之前已经多次介绍,Cpp章节中也有实现。单例模式使用场景是在一个资源只能被加载分配一次,一个类只能实例化一个对象的情况。
饿汉模式
资源一次性加载分配完,对象在程序初始化阶段实例化完毕,这种实现是线程安全的,程序运行起来比较流畅,但是启动加载时间可能过长。
懒汉模式
资源使用时再加载分配,对象再使用的时候再去实例化,这种实现加载快,同一时间消耗资源少,但是运行中可能卡顿。这种实现是线程不安全的,因此我们要加锁判断类是否实例化过,如果没有则实例化。