1.为什么要有多线程?
一个进程中并不是只有一个main函数的执行流,而是有很多个执行流在同一时刻占用不同的CPU进行运算,我们称之为并行。同一时间有多个执行流在执行代码,程序的运行效率就可以大大提高,创建一个线程就相当于创建了一个执行流。
2.什么是线程?
线程就是创建出来的执行流,它在内核中拷贝当前进程的PCB并创建了一块PCB,和进程共用一块虚拟地址空间。
创建一个进程(fork,vfork)
fork:子进程拷贝父进程的PCB创建出来一块PCB,PCB在内核中用双向链表管理起来。此时,父子进程共用同一块虚拟地址空间,若父进程或者子进程的内容即将发生改变,则给子进程重新开辟一个虚拟地址空间(写时拷贝技术)
vfork:拷贝父进程的PCB并创建,两个PCB一模一样,并且指向同一虚拟地址空间,因此共用同一栈空间。vfork为了解决调用栈混乱的问题,让子进程先运行,直到退出,父进程才开始运行。
创建一个线程(pthread_create)
pthread_create:在内核中拷贝进程PCB,并创建一块PCB,和进程共用同一虚拟地址空间,但是线程在虚拟地址的共享区,会有自己独立的东西(线程ID,栈,信号屏蔽字,错误信息,寄存器,调度优先级等)
注意事项: 在Linux中其实没有线程这一概念(是C库中的概念),Linux中叫轻量级进程(LWP)。线程的一系列接口都是C库提供的,因此链接时需要加上库名称。例:gcc test.c -o test -lpthread
3.线程的优缺点:
优点:
缺点:滑稽吃鸡
总结:
1. 线程和进程拥有同一虚拟内存空间,且线程是并发式执行,即新增了一条执行流
2. 同一时刻一个资源最多只能有一个线程使用,否则可能造成二义性,因此要保证对临界资源的顺序访问,具体见 - 线程安全
4.线程的独有和共享:
线程拷贝进程的PCB并创建,共用同一块虚拟地址空间,在虚拟地址空间的共享区中有一块独有的空间。
独有:(共享区内一块独有的)
共享:(虚拟地址空间中其它的)
总结:
1.线程都拥有各自的栈因此可以并行,不用担心调用栈混乱,而堆上空间是所有线程共享的。
2.若函数内存在共享内容,一般都是不可重入的。
线程控制:创建,终止,等待,分离
前提:线程控制当中的接口都是库函数,所以线程控制的接口需要链接线程库,线程库的名称叫pthread,链接的时候增加lpthread,且大多函数都以pthread开头。
一、线程创建:
int pthread_creat(pthread_t* thread,const pthread_attr, void* (* thread_start) (void * ) , void* arg);
用例:pthread_creat(&tid,NULL,func,NULL);
thread:线程标识符pthread_t,是一个出参。
attr:线程属性,NULL是默认属性,一般都是用NULL
thread_start:线程入口函数,接收一个函数地址,这个函数的返回值是void*,参数也是void*
arg:给线程入口函数传递的参数的值,类型是void* ,一般这个参数也是传的NULL
返回值:==0创建成功,<0创建失败
下面看一段代码:arg接收临时变量i的地址
void* thread_start(void* arg) //自定义的线程入口函数
{
int* ti=(int*)arg; //创建一个int*接收强转后的arg
while(1)
{
printf("i am new thread!%d\n",*ti); //这里使用的是临时变量,在主线程中循环结束之后i被释放,访问的就是非法地址,没有崩溃是因为这段空间还没有被重新开辟。
sleep(1);
}
return NULL;
}
int main()
{
pthread_t tid; //线程标识符
for(int i=0;i<4;i++) //创建4个线程
{
int ret=pthread_create(&tid,NULL,thread_start,(void*)&i); //创建线程,传入临时变量i
if(ret<0)
{
perror("pthread_create");
return -1;
}
}
while(1)
{
printf("i am main thread\n");
sleep(1);
}
return 0;
}
运行结果:
i am new thread!2
i am new thread!3
i am new thread!4
i am new thread!3
i am main thread
i am new thread!4
i am main thread
^C
这段代码有两个问题:
总结:
二、线程终止的方式:
注意:
三、线程等待:为了释放线程退出资源,防止内存泄露
pthread_join(pthread_t,void ** ) 这是一个阻塞接口
pthread_t:线程标识符
void ** :获取线程退出的什么东西,线程不同的退出方式会收到不同的值,一般是传NULL
return:接收入口函数return返回的内容,
pthread_exit:获取pthread_exit(void*)的参数
pthread_cancel:获取到一个常数,
PTHERAD_CANCELED:#define PTHERAD_CANCELED (void*)(-1)
工作线程的资源可以由主线程来回收,主线程(进程)的资源由它的父进程bash(1号进程)来回收。
这样引起了一个问题:
1.若不等待处理线程退出,会造成内存泄露
2.若等待处理线程,因为是阻塞接口,则必须另一个线程专门等待他退出并回收资源,则线程并没有实质性提高效率
四、线程分离:改变线程的joinable属性,变成detach,从而使该线程退出时不需要其他线程来回收该线程的资源,可以被操作系统来回收
pthread_detach(pthread_t tid); detach(分离)
tid:线程标识符,设置tid为detach属性
1.分离的本质是给线程设置了一个属性
2.分离可以线程自己分离,也可以是组内其他线程通过获取到线程标识符去分离
进程ID: 即tgid(thread group ID),也可以叫线程组ID。主线程的tid==tgid,工作线程的tid都不相同,但是tgid都指的是进程ID。在工作线程和主线程中使用getpid()接口,返回的都是进程ID。
线程ID: 即tid(thread ID),可以看到每一个在进程中被创建出来的线程,其线程ID都不相同。用syscall(SYS_gettid)接口,返回线程ID,或者在bash中输入top -H -p+进程ID查看
LWP: 轻量级线程,它在内核中是一块PCB,通过内核对PCB的调度,实现对线程的切换。
总结:
下面模拟一个黄牛抢票的场景:
#define SCALS 4 //模拟四个黄牛抢票
int g_val=100;
void* thread_start(void* arg)
{
(void)arg; //不使用这个arg时把他void一下就行,防止报警告
while(g_val>0) //只要有票黄牛就一直抢票
{
printf("i am scaplers:%p,i get tickes:%d\n",pthread_self(),g_val);
g_val--; //抢到票则g_val--
}
return NULL;
}
int main()
{
pthread_t tid[SCALS];
for(int i=0;i<4;i++)
{
int ret=pthread_create(&tid[i],NULL,thread_start,NULL);
if(ret<0)
{
perror("pthread_create");
return -1;
}
}
for(int i=0;i<4;i++)
{
pthread_join(tid[i],NULL); //回收线程并清理线程退出资源
}
return 0;
}
问题:
这里出现了一张票被不同黄牛,抢到多次的情况,很显然是不能存在的。这种情况就是我们所说的线程不安全,下面介绍如何保证线程安全。
重入概念: 同一个函数被不同执行流调用(线程入口函数),当一个执行流还没执行完,另一个执行流再次进入函数,就叫重入。
函数可重入:多个执行流同时调用同一函数,不会对运行结果产生影响则为可重入
函数不可重入:运行结果产生了二义性
常见不可重入:
注意:
1.函数可重入!=线程安全
2.函数可重入则一定线程安全,而线程安全不一定可重入,即可重入函数是线程安全函数的一种
线程安全: 多个线程并发的访问临界资源,而不会导致程序的结果产生二义性,则称线程安全。
前提:
下面看一下黄牛抢票程序中g_val- -操作的流程:
完整解释为什么会出现一张票被抢多次原因:
拓展:对应汇编指令–>1.load:从内存加载到寄存器 。2.updata:更新寄存器中的值进行-- 。3.store:将寄存器中的值回写到内存中
这是三个指令操作,因此每个要执行时都可能被打断。
那如何保证线程的安全性呢?
1.互斥:互斥锁,在同一时刻只能有一个执行流访问临界资源
2.同步:条件变量,程序对临界资源的合理访问,也就是不同类型的线程顺序访问,一个类型的执行流访问完成就切换,这里切换是操作系统调度的,抢占式执行
互斥锁能保证互斥属性的原理:
互斥锁是一个阻塞接口,使用互斥锁来保证互斥属性,底层是互斥量,互斥量的本质是一个计数器,有0和1
计数器的值为:
0:无法获取互斥锁,即临界资源无法访问,并阻塞在加锁操作
1:可以获取互斥锁,互斥锁成功返回,并访问临界资源
加锁操作:对互斥锁中的互斥量计数器-1,实现本质是交换寄存器中的1和内存中的0
解锁操作:对互斥锁中的互斥量计数器+1
那么引出一个问题:互斥量计数器本身也是一个变量,那这个变量的取值进行++、–时,会是原子性的操作吗?
寄存器中的值(默认给0)和内存中互斥量计数器的值进行交换,这个操作在汇编中的指令是xchgb,把寄存器和内存中的值进行交换,由于只有一条指令,因此保证了原子性
加锁流程:
总结:
1.对临界资源的访问时非原子性,因此导致程序运行可能产生二义性
2.互斥锁在同一时刻只能有一个执行流获取,且互斥量的操作是原子性的,因此可以保证互斥属性
互斥锁使用流程:
1.定义互斥锁
pthread_mutex_t:互斥锁变量类型,mutex(互斥锁),在内核中是一个结构体
用例:pthread_mutex_t lock;
2.初始化互斥量
动态初始化:
int pthread_mutex_init(pthread_mutex_t* mutex,const pthread_mutexattr_t* attr)
用例:pthread_mutex_init(&lock,NULL);
mutex:互斥锁变量,传参的时候传入互斥锁变量的地址
attr:互斥锁的属性,一般用NULL
静态初始化:pthread_mutex_t lock=PTHREAD_MUTEX_INITIALIZER;
PTHREAD_MUTEX_INITIALIZER :宏定义了结构体pthread_mutex_t(互斥锁)的值
3.加锁
1.int pthread_mutex_lock(pthread_mutex_t* mutex); 这是一个阻塞接口
用例:pthread_mutex_lock(&lock);
mutex:传阻塞变量的地址,&lock
注意:该加锁方式是阻塞接口
若计数器为1,则上锁,计数器清0,并执行之后代码
若计数器为0,则不可上锁,阻塞等待直到解锁,不能执行下面代码
2.int pthread_mutex_trylock(pthread_mutex_t* mutex);非阻塞加锁,不会阻塞等待(直接返回EBUSY),一般要循环等待解锁,不然线程会直接往下执行会访问到临界资源,造成二义性
mutex:传阻塞变量的地址,&lock
注意:该加锁方式是非阻塞
若计数器为1,则加锁,计数器清0,并执行之后代码
若计数器为0,则不可加锁,接口返回EBUSY(拿不到互斥锁),要循环进行加锁操作,防止该执行流返回EBUSY之后,直接往下执行代码,造成二义性的结果
3.int pthread_mutex_timedlock(pthread_mutex_t* mutex,const struct timespec* abs_timeout);阻塞等待一定时间之后,若还没获取互斥锁,则报错返回ETIMEOUT
mutex:传阻塞变量的地址,&lock
abs_timeout:加锁时的最长等待时间,超过时间还未加锁报错返回,有两个变量:一个是秒,一个是纳秒
4.解锁
int pthread_mutex_unlock(pthread_mutex_t* mutex);
三种加锁方式都可以解锁(万能钥匙),
注意:要在所有可能退出获得互斥锁线程的地方都加上解锁!!!!非常重要,一旦上锁的线程没解锁就退出,其它线程会一直阻塞。
锁子只有一个!!!
5.销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t* mutex);
互斥锁销毁,若使用完成后不进行销毁,造成内存泄露
使用互斥锁需要注意:
死锁:
总结:
1.在访问临界资源之前都应该进行加锁操作,
2.在所有退出的地方都应该进行解锁操作
3.加锁操作有点像,我们去某个地方设置一个门卫,门里有人则不放行
4.用continue实现同步,疯狂判断,浪费CPU资源,入等待队列之后,挂起等待,因此不会浪费CPU资源
下面是黄牛改进代码:
#define SCALS 4 //四个黄牛
int g_val=100; //100张票
pthread_mutex_t lock; //互斥锁
void* thread_start(void* arg) //抢票程序
{
(void)arg; //不使用这个arg时把他void一下就行,避免报警告
while(1)
{
pthread_mutex_lock(&lock); //加锁
if(g_val>0)
{
printf("i am scaplers:%p,i get tickes:%d\n",pthread_self(),g_val);
g_val--; //抢到票则g_val--
}
else
{
//pthread_mutex_unlock(&lock); //这个位置和return NULL位置解锁的效果是一样的
break;
}
pthread_mutex_unlock(&lock); //这里不解锁的话,拿到第100张票之后会继续循环,自己卡住了自己,死锁!
}
pthread_mutex_unlock(&lock); //这里不解锁的话,最后一次拿到锁的线程直接退出,会造成其它3个线程一直卡在pthread_mutex_lock处,无法加锁。
return NULL;
}
int main()
{
pthread_mutex_init(&lock,NULL);
pthread_t tid[SCALS];
for(int i=0;i<4;i++)
{
int ret=pthread_create(&tid[i],NULL,thread_start,NULL);
if(ret<0)
{
perror("pthread_create");
return -1;
}
}
for(int i=0;i<4;i++)
pthread_join(tid[i],NULL); //回收线程并清理线程退出资源
pthread_mutex_destroy(&lock); //如果使用完毕不销毁锁,会造成内存泄露
return 0;
}
问题:
这里100张票是成功被抢光,且程序正常退出了,但有一个问题:没有保证同步—>每个黄牛各抢一次票然后切换,下面介绍条件变量来保证同步。
概念: 同步保证了各个执行流对临界资源访问的合理性,通俗来讲:也就是一个执行流访问一次,就切换另一个执行流访问。通常我们用条件变量来保证同步属性。
什么是条件变量?
本质:PCB等待队列+两个接口(等待接口+唤醒接口)
条件变量接口:
1.定义条件变量:
pthread_cond_t 条件变量类型
用例:pthread_cond_t cond;
cond:条件变量类型的变量
2.初始化条件变量:
动态:使用完毕需要调用pthread_cond_destroy()销毁
pthread_cond_init(pthread_cond_t* cond,pthread_condattr_t* attr);attrutibe:属性
pthread_cond_t*:条件变量类型变量的地址
pthread_condattr_t*:条件变量的属性,一般传NULL,默认属性
静态:使用完毕不用销毁
pthread_cond_t cond=PTHREAD_COND_INITIALIZER; (initialize:初始化)
3.等待接口:将调用该等待接口的执行流放到PCB等待队列当中去,进行等待
int pthread_cond_wait(pthread_cond_t* cond,pthread_mutex_t* mutex)
pthread_cond_t*:传入条件变量类型变量的地址
pthread_mutex_t*:传入互斥锁变量的地址
4.唤醒接口:唤醒PCB等待队列当中的执行流进行出队
int pthread_cond_signal(pthread_cond_t* cond)
唤醒至少一个PCB等待队列当中的线程
int pthread_cond_broadcast(pthread_cond_t* cond)
唤醒所有PCB等待队列当中的线程(抢占式执行)
5.销毁:释放动态初始化的条件变量所占用的内存
int pthread_cond_destroy(pthread_cond_t* cond);
这里需要重点介绍等待接口:pthread_cond_wait(&cond,&lock);
下面模拟一下吃面和做面:
pthread_mutex_t lock; //定义一个锁资源
pthread_cond_t cond; //定义一个条件变量
int noodles=0; //临界资源
void* thread_a(void* arg) //顾客线程
{
(void)arg;
while(1) //循环吃面
{
pthread_mutex_lock(&lock); //加锁
if(noodles==0){
pthread_cond_wait(&cond,&lock); //如果没有面,则调用wait接口阻塞并入队,等待厨子做面。注意这里传了lock的地址,接口底层封装了解锁(phtread_mutex_unlock)和被唤醒时加锁(pthread_mutex_lock)的代码。
}
noodles--;
sleep(1);
printf("i am consumer,i ate noodles:%d\n",noodles);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond); //已经吃完面了,给厨子发送一个做面信号,唤醒厨子做面
}
return NULL;
}
void* thread_b(void* arg) //厨子
{
(void)arg;
while(1)
{
pthread_mutex_lock(&lock); //加锁
if(noodles==1){
pthread_cond_wait(&cond,&lock); //如果有面,则入队等待顾客吃面。
}
noodles++;
sleep(1);
printf("i am cooker,i made noodles:%d\n",noodles);
pthread_mutex_unlock(&lock);
pthread_cond_signal(&cond); //已经做好面了,给顾客发送信号,唤醒顾客吃面。
}
return NULL;
}
int main()
{
pthread_t tid[2]; //定义两个线程标识符
pthread_mutex_init(&lock,NULL); //初始化锁资源
pthread_cond_init(&cond,NULL); //初始化条件变量
if(pthread_create(&tid[0],NULL,thread_a,NULL)<0){
perror("pthread_create");
return -1;
}
if(pthread_create(&tid[1],NULL,thread_b,NULL)<0){
perror("pthread_create");
return -1;
}
for(int i=0;i<2;i++)
pthread_join(tid[i],NULL);
pthread_mutex_destroy(&lock);
pthread_mutex_destroy(&lock);
return 0;
}
运行结果:
[test@localhost cond]$ ./noodles
i am cooker,i made noodles:1
i am consumer,i ate noodles:0
i am cooker,i made noodles:1
^C
这段代码还是有问题:
如果有多个消费者和多个生产者,那么程序会执行一段之后卡住
原因:
生产者和消费者用了同一个PCB等待队列,如果消费者A访问完临界资源使,g_val==0,并调用pthread_cond_signal接口通知PCB等待队列。唤醒的还是一个消费者B,那么消费者B会直接入队,就再也没有通知PCB等待队列的了,因此程序运行一段之后都会卡死!!
解决:
1.用pthread_cond_boardcast接口唤醒PCB等待队列的所有线程(不建议,唤醒还是抢占式执行效率低)
2.生产者和消费者分别用一个PCB队列进行管理,消费者完成消费通知生产者队列,生产者完成生产通知生产者队列。
模板:
消费者:
pthread_mutex_lock(&lock); //加锁
while(如果临界资源不可用)
{
pthread_cond_wait(&consumer_cond,&lock);//入消费者队列
}
g_val--; //操作临界资源
pthread_mutex_unlock(&lock); //解锁
pthread_cond_signal(&producter_cond); //通知生产者队列
生产者:
pthread_mutex_lock(&lock); //加锁
while(如果临界资源不可用)
{
pthread_cond_wait(&producter_cond,&lock);//入生产者队列
}
g_val--; //操作临界资源
pthread_mutex_unlock(&lock); //解锁
pthread_cond_signal(&consumer_cond); //通知生产者队列
总结:
123规则:1个场景(队列)+两种角色(消费者与生产者)+3种关系(消费者与消费者互斥+生产者与生产互斥+消费者和生产者同步加互斥)
生产者与消费者模型的优点:
可以解耦合:生产者只负责生产,消费者只负责消费,生产者和消费者都是通过队列进行交互。
支持忙闲不均:队列起到了缓冲作用,只要队列不满,生产者可以一直往队列中插入数据,消费者也是同样道理。
支持并发:消费者只关心队列中是否有数据可以进行消费,生产者只关心队列中是否有空闲的节点进行生产
如何实现生产者与消费者模型
1.队列,借助STL中的queue,队列属性:先进先出,所有满足先进先出特性的数据结构都可称为队列。
2.线程安全的队列
queue 为了高效率,STL中的queue并不是线程安全的
互斥:使用互斥锁
同步:使用条件变量
3.两种角色的线程
生产者线程:负责往队列插入数据
消费者线程:负责从队列里取数据
push:生产者 pop:消费者
队列指针要在线程退出之后,释放堆空间
现象:消费者先于生产者打印一条,原因:已经生产后,将打印时,时间片耗尽,被切换。
只保证了插入和取出的顺序,但不能保证打印顺序
下面模拟一下生产者和消费者模型:
#include
#include
#include
#include
#include
#define CAPACITY 4
#define THREADCOUNT 2
class BlockQueue
{
public:
BlockQueue(size_t Capacity = CAPACITY)
{
pthread_mutex_init(&lock_, NULL);
pthread_cond_init(&procond_, NULL);
pthread_cond_init(&comcond_, NULL);
Capacity_ = Capacity;
}
~BlockQueue()
{
pthread_mutex_destroy(&lock_);
pthread_cond_destroy(&procond_);
pthread_cond_destroy(&comcond_);
}
void Push(int& Data)
{
pthread_mutex_lock(&lock_);
while (IsFull())
{
pthread_cond_wait(&procond_, &lock_);
}
Queue_.push(Data);
pthread_mutex_unlock(&lock_);
pthread_cond_signal(&comcond_);
}
void Pop(int* Data)
{
pthread_mutex_lock(&lock_);
while (Queue_.empty())
{
pthread_cond_wait(&comcond_, &lock_);
}
*Data = Queue_.front();
Queue_.pop();
pthread_mutex_unlock(&lock_);
pthread_cond_signal(&procond_);
}
private:
bool IsFull()
{
return Queue_.size() == Capacity_;
}
private:
std::queue<int> Queue_;
size_t Capacity_;
pthread_mutex_t lock_;
pthread_cond_t procond_;
pthread_cond_t comcond_;
};
void* thread_com(void* arg)
{
BlockQueue* bq = (BlockQueue*)arg;
int data;
while (1)
{
bq->Pop(&data); //这里bq是指针,要用->
printf("Consumed:[%p] [%d]\n", pthread_self(), data);
}
return NULL;
}
void* thread_pro(void* arg)
{
BlockQueue* bq = (BlockQueue*)arg;
int i = 0;
while (1)
{
bq->Push(i); //这里bq是指针,要用->
printf("Producted:[%p] [%d]\n", pthread_self(), i);
++i;
}
return NULL;
}
int main()
{
BlockQueue* bq = new BlockQueue;
pthread_t pro_tid[THREADCOUNT];
pthread_t com_tid[THREADCOUNT];
int i = 0;
for (; i < THREADCOUNT; i++)
{
if (0 > pthread_create(&pro_tid[i], NULL, thread_pro, (void*)bq))
{
perror("pthread_create");
return -1;
}
if (0 > pthread_create(&com_tid[i], NULL, thread_com, (void*)bq))
{
perror("pthread_create");
return -1;
}
}
for (i = 0; i < THREADCOUNT; ++i)
{
pthread_join(com_tid[i], NULL);
pthread_join(pro_tid[i], NULL);
}
delete bq;
bq = NULL;
return 0;
}