线程的概念,线程的控制,线程的同步和互斥,队列结构,线程池,锁
1.预备知识
1.1可重入函数
1.1.1链表的头插
- main函数调用insert函数向一个链表head中插入节点node1,插入操作分为两步,刚做完第一步的时候,因为硬件中断使进程切换到内核,再次回用户态之前检查到有信号待处理,于是切换到sighandler函数,sighandler也调用insert函数向同一个链表head中插入节点node2,插入操作的两步都做完之后从sighandler返回内核态,再次回到用户态就从main函数调用的insert函数中继续 往下执行,先前做第一步之后被打断,现在继续做完第二步。结果是,main函数和sighandler先后向链表中插入两个节点,而最后只有一个节点真正插入链表中了。
- 像上例这样,insert函数被不同的控制流程调用,有可能在第一次调用还没返回时就再次进入该函数,这称为重入,insert函数访问一个全局链表,有可能因为重入而造成错乱,像这样的函数称为不可重入函数,反之,如果一个函数只访问自己的局部变量或参数,则称为可重入(Reentrant) 函数。
main执行流和信号捕捉方法执行流同时发生,就会出现两个执行流,
结论:1.一般而言,我们认为:main执行流和信号捕捉执行流是两个执行流
2.如果在main中,和handler中,该函数被重复进入,出问题,该函数被称为-----不可重入函数
3.如果在main中,和handler中,该函数被重复进入,没有出问题,该函数被称为-----可重入函数
***************我们目前用到的接口都是不可重入的。
函数可不可以重入是特性,是一个中性词!
如果一个函数符合以下条件之一则是不可重入的:
- 调用了malloc或free,因为malloc也是用全局链表来管理堆的。
- 调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。
1.2 volatile(保持内存可见性)
#include <stdio.h>
#include <signal.h>
int quit = 0;
void handler(int signo)
{
printf("%d号信号正在被捕捉\n",signo);
printf("quit: %d",quit);
quit = 1;
printf(" -> %d\n",quit);
}
int main(int argc, char const *argv[])
{
signal(2,handler);
while(!quit);
printf("我是正常退出的\n");
return 0;
}
cpu主要做三件事情,取指令,分析指令,执行指令
正常情况下在main执行流中,更改quit的值是在内存中改的,但是进行优化后,quit被放到寄存器中,这时如果执行信号捕捉动作流的时候,改的还是内存的数据,但寄存器中的值还是0,没有被改,所以main执行流还是使用寄存器中的quit.
所以为了解决这类问题,我们要使用关键字volatile
让每一次读取quit的值,不从寄存器中读取,而是从内存中读取。
2.什么是线程
在创建进程的时候会有虚拟内存,虚拟内存里面决定了进程能够看到的资源(代码区,堆区,栈区。。。),我们通过地址空间和页表就能访问代码所需要的资源,
一个进程是可以把自己的划分出一部分,让另一样执行流去执行,如fork(),让父子进程分别执行不同的代码块。也可以发生写实拷贝。
先在我们让再次创建的进程不再有独立的地址空间,而是共享父进程的地址空间,就相当于一个房间,有5,6个人一起通过窗户看,每个人都可以通过同一个窗户看,让每一个进程访问我代码中的一部分,访问一部分资源
类似这种,只创建pcb(struct_task)不分配地址空间的,从父进程中分配资源的方法叫做线程。
因为我们可以通过虚拟地址空间+页表方式对进程进行资源划分,单个“进程”执行力度,一定要比之前的进程要细
- 站在CPU的角度,会如何看待一个一个的PCB(task_struct)呢?
CPU不会管你有没有虚拟地址空间。它只认识task_struct,只会对每一个PCB进行计算
- 如果OS要专门创建设计“线程”概念,OS要不要进程管理?
可能需要,如果有,如何管理呢?
先描述在组织
一定要对线程设计专门的数据结构对象(TCB),常见的Windows系统就是专门这样做的
线程创建的本质就是为了被执行。被调度(id,状态,优先级,上下文,栈。。。。);
这时会发现,线程和进程有很多的地方是重叠的,
所以我们的Linux工程师,我们不想给Linux系统专门设置线程的数据结构,我们直接用PCB来表示Linux下的“线程”。
Linux下的线程,就是在OS内创建pcb,然后指向父线程的地址空间,通过页表给线程分配一些资源
结论:线程在进程内运行,线程在进程地址空间内运行!拥有进程的一部分资源。
进程的概念:内核视角下,承担分配系统资源的基本实体(创建进程时候,申请一个地址空间,创建一个pcb和一堆的页表和加载到物理内存的代码和数据,所有这些消耗的资源,我们叫做进程,以前讲的进程内部只有一个执行流)
线程的概念:CPU调度的基本单位!
以前是一个pcb和地址空间页表内存中的代码和数据,叫做进程
现在是一堆的PCB和地址空间页表内存中的代码和数据,叫做进程
以前讲的进程内部只有一个执行流,今天讲的是一个进程内部可以有多个执行流,
站在CPU角度,以前我们讲的是CPU调度的是一个进程,今天,CPU调度的是进程中的一个分支(执行流)
今天我们喂给CPU的task_struct
- Linux内核中有没有真正意义的线程呢?
1.严格意义上是没有的,Linux利用进程PCB模拟线程的,是一种完全属于自己的一套方案。
2.站在CPU视角,每一个PCB,都可以称为轻量级进程
3.Linux线程是CPU调度的基本单位。而进程是承担分配资源的基本单位
4.进程是用来整体申请资源,线程是伸手向进程要资源。
5.Linux中没有真正意义上的线程。没有线程之名但有线程之实
6.好处是:可以服用PCB的调度算法,并不用维护线程与进程的关系,不用做数据结构之间的耦合,让编码上更简单,维护成本低------可靠高效
7.缺点是:OS、程序员只认线程,Linux无法直接提供线程的系统调用接口,而只能给我们提供轻量级进程的接口!(去银行打不了饭)
后续如果资源不足,线程需要资源,OS还是会给的,只是本质上是进程在要。
2.1pthread_create(线程创建库函数)
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
参数:
pthread_t *thread无符号整数的线程id
const pthread_attr_t *attr ,不要管,全设为NULL
void *(*start_routine) (void *) 函数指针,让创建的线程执行这个函数(回调函数)
void *arg 回调函数,在执行时使用的参数
返回值:
成功返回0,失败返回错误码
#include <iostream> #include <pthread.h> #include <cassert> #include <unistd.h> #include <cstdio> using namespace std; //新线程 void* thread_routine(void* args) { const char* name = (const char*)args; while(true) { cout<<"我是新线程,我正在运行,name:" << name << std::endl; sleep(1); } } int main(int argc, char const *argv[]) { pthread_t tid; //tid地址 int n = pthread_create(&tid,nullptr,thread_routine,(void*)"thread_on"); assert(0 == n); (void)n; //主线程 while (true) { char tidbuffer[64]; snprintf(tidbuffer,sizeof(tidbuffer),"0x%x",(unsigned int)tid); cout<<"我是主线程,我正在运行,我创建出来的线程tid:"<< tidbuffer << endl; sleep(1); } return 0; }
需要使用命令ps -aL查看轻量级进程
有两个执行流,且pid是一样的。说明这两个是属于同一个进程的,但是LWP是不一样的,LWP(light weight process)轻量级进程ID
但PID和LWP是一样的轻量级线程为主线程。
CPU调度是以LWP为标识符表示特定的一个执行流的。
以前我们理解的是PID,是因为用的都是单线程,PID==LWP
pthread_create函数得到的tid是一个地址,与系统看到的LWP是并不一样的。
2.1线程一旦被创建,几乎所有资源都是被所有线程共享的
所有的线程都能调用定义的函数
线程之间的数据的很方便共享
但是不是所有的数据都共享,只是大部分的数据是共享的。
线程也一定有自己的私有内部属性
什么资源是线程私有的?
1.PCB属性私有,
2.要有一定的私有上下文结构
3.每一个线程都要有自己的独立的栈结构
3.错误号
5.优先级
与进程之间的切换相比,进程之间的切换需要操作系统需要OS做的工作少很多。
1. 进程:切换页表 && PCB &&上下文 && 虚拟地址空间
2. 线程:切换PCB && 上下文数据
3.线程切换cache不用太更新,但是进程切换,就要全部更新
CPU中除了有寄存器还有cache(硬件级缓存),对数据的保存的功能,CPU在访问数据的时候,可以不访问内存,直接访问cache.如果cache没有命中,cache从内存中去读取,再让CPU来读取。
一个进程它的内部已经缓存了许多热点数据!
如果是进程,这里面的数据都要进行切换,而如果是线程的话,这里面的数据就不用被切换。
线程的缺点:
性能的损失:线程数和核数最好是一样的,比如CPU是单核的,现在是3个线程,线程也要进行切换。
CPU的多核:CPU中有运算器和控制器,多核可以理解为CPU中存在多个运算器
多CPU就是多个独立的CPU
健壮性降低:一个线程出问题,可能会影响多个线程
缺乏访问控制:全局变量的访问控制
编程调试困难
线程的优点:
创建新线程的代价要比创建有一个新进程的代价小的多
线程之间的切换不进程之间的切换,系统做的工作要少很多
线程占 的资源要少的多
能够充分使用处理器完成并发的工作
在等待慢速IO过程中,程序可执行其他操作
计算密集型应用能够在多处理器上运行
io密集型应用,为了提高性能,将io操作重叠
线程共享的资源
除了pcb,内存地址空间,页表,
还有文件描述符表,每种信号的处理方式是共享的,当前工作目录,用户id和组id
线程私有资源
线程ID
寄存器(上下文)
栈
ernno
信号屏蔽字
调度优先级
3.线程的健壮性问题
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
using namespace std;
void* start_routine(void* argv);
int main(int argc, char const *argv[])
{
pthread_t id;
pthread_create(&id,nullptr,start_routine,(void*)"start_routine");
while (true)
{
cout << "main thread running" <<endl;
sleep(1);
}
return 0;
}
void* start_routine(void* argv)
{
string name = static_cast<const char*>(argv); //安全的强制类型转换并检查
while (true)
{
cout << "new thread:" << name <<endl;
sleep(1);
}
int *p =nullptr;
// p = nullptr; //p本来就是空指针
*p = 0; //这个会报错,直接对nullptr解引用是不行的,本质是向0号地址内写0
}
这里 *p = 0,野指针错误。会影响整个进程
一个线程如果出现问题,会影响其他线程,这可以说是健壮性或鲁棒性差
为什么呢?
之前讲的信号是进程信号,发送给整个进程的,所有线程的pid都是想等的,所以OS向所有的同一个pid号进程发送信号会导致整个所有线程全部结束
一个进程包括地址空间,页表和内存中对应的代码和结构,多个或一个执行流
一个线程是进程的一部分,如果线程出了问题,就相当于进程出现了问题。 进程要被释放,所有依附这个进程的线程都会被释放
4.线程和进程的关系
5.clone线程创建调用库,但依旧是要系统提供接口
fork底层也是调用这个接口,
clone,生成进程或者轻量级进程
int clone(int (*fn)(void *), void *child_stack,int flags, void arg, .../ pid_t *ptid, void *newtls, pid_t *ctid */ );
参数
int (*fn)(void *) //新执行流要执行的代码
void *child_stack //子栈
vfork也可以创建子线程,不过与fork不同的是,创建出来的线程与父线程,共享地址空间,也就是轻量级进程
6.线程控制
6.1线程库
#POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
- 要使用这些函数库,要通过引入头文
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项
6.2 pthread_create创建线程
功能:创建一个新的线程
原型
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;失败返回错误码
错误检查:
- 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
- pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
- pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
6.3线程的终止
6.3.1线程函数结束,return的时候,线程就终止了
6.3.2pthread_exit(void *retval);线程终止函数
以前我们学到exit(0);函数不能用于终止线程,会让整个进程直接终止。
---------》任何一个执行流调用exit都会让整个进程结束任务
void pthread_exit(void *retval);
参数:void *retval
这个参数和如何获得这个参数,通过线程等待之后解决
6.3.3pthread_cancel线程取消
线程是可以被取消的,线程被取消,前提是线程已经跑起来了。
int pthread_cancel(pthread_t thread);
参数:线程号;
线程如果是被取消的,它的退出码就是-1,本质是一个宏PTHREAD_CANCELED;
线程必须有阻塞点(sleep)才能被取消。
如果没有取消点,可以手动添加
void pthread_testcancel(void);
有的线程可以取消,有的线程不可以取消,可以使用,但是如果不让取消,一定要保证状态修改这句代码已经执行,否者会直接取消。
int pthread_setcancelstate(int state, int *oldstate);
state:
PTHREAD_CANCEL_ENABLE //可以被取消
PTHREAD_CANCEL_DISABLE //不可以被取消
例如前5秒不能被取消,后面可以被取消
还可以设置取消类型
int pthread_setcanceltype(int type, int *oldtype);
PTHREAD_CANCEL_DEFERRED 等到取消点才取消(默认)
PTHREAD_CANCEL_ASYNCHRONOUS 目标线程会立即取消
6.2.4pthread_clean_push线程的清理
如果 线程在取消前申请了内存没有释放,就会浪费资源,这时候,就需要线程清理
void pthread_cleanup_push(void (*routine) (void *), void *arg)
void pthread_cleanup_pop(int execute)
这两个函数必须成对使用
没有成对使用报的错
正确使用
pthread_cleanup_pop的参数如果是0,就不会在执行pthread_cleanup_push里面的回调函数了
如果pthread_cleanup_pop的参数如果是非0,就会直接去执行pthread_cleanup_push里面的回调函数
这个pthread_cleanup_push里面的回调函数被执行的条件
1. 被pthraead_cancel取消掉
2.执行pthread_exit
3.非0参数执行pthread_cleanup_pop
线程中的return可以直接结束线程,但是不能触发pthread_cleanup_push里面的回调函数
6.3.5pthread_self(),获取子线程tid
7.线程的等待
线程也是需要等待的,如果不等待会发生什么问题呢?
如果不等待,也会照成类似僵尸进程的问题-----内存泄露。
等待的目的:
- 获取回收新线程的退出信息------》可以不关心,但是不能没有
- 回收新线程对应的PCB等内核资源,防止内存泄露-------暂时无法查看!
等待的方法:int pthread_join(pthread_t thread, void **retval);
7.1pthread_join()线程等待函数
int pthread_join(pthread_t thread, void **retval);
参数:
pthread_t thread //线程id
void **retval //void pthread_exit(void *retval);
返回值:
成功返回0,失败返回错误码
//多线程的等待 for(auto &iter : threads) { pthread_join(iter->tid,nullptr); }
7.2线程的返回值问题
无论是pthread_exit还是pthread_join函数,都让我们传入参数返回值
针对int pthread_join(pthread_t thread, void **retval);中的void ** retval是输出型参数,用来获取线程函数结束时,返回的退出结果!!!
线程返回值结果是void*,要想将其输出就要使用void**
类型是什么:
我身上有100,我身上有多少钱?100元,100美元,100分、、、、、
你并不知道
现在返回值如果是return (void*)106;
代表的就是返回的是地址,只是这个地址里面写的是106
在我们自己的代码空间中,定义了一个变量,void*ret;而(void*)106是指针。
我们的线程退出的时候,会将退出结果保存到pthread库中。
pthread_joint的本质是从库中调取指定线程的退出信息
现在如何获取这个退出信息到我们定义的变量中呢?
在你的空间中定义一个指针变量。由于是个变量,可以将指针变量的地址传进去,
&ret
*(&ret) 就等于库中的这个变量,
把库中的变量(void*)ret拷贝到&ret中,再解引用,就是这个数本身,就相当于直接将这个数拷贝到ret中
7.3线程退出的信号
线程出异常,整个进程都会退出。
//ptread_join默认就会调用成功,不考虑异常问题,异常问题是你进程考虑的问题。
7.4分离线程。如果线程不进行等待
线程不存在非阻塞等待,要么等,要么不等。
分离线程
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
线程可以自己分离自己,也可以由父进程进行分离
7.4.1pthread_detach线程分离函数
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstdio>
std::string changeId(const pthread_t &threadid)
{
char tid[128];
snprintf(tid,sizeof(tid),"ox%x",(unsigned int)threadid); //自己获取自己的线程id
return tid;
}
void* start_routine(void* args)
{
std::string thread_name = static_cast<const char*>(args);
pthread_detach(pthread_self()); //自己把自己设置为分离状态
while(true)
{
std::cout << "name:" << thread_name << "running...new thread id:"<< changeId(pthread_self()) << std::endl;
sleep(1);
}
}
int main(int argc, char const *argv[])
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,(void*)"thread_1");
std::string main_id = changeId(pthread_self());
std::cout << "main running...main thread id:"<< main_id << " new thread id:"<< changeId(tid) << std::endl;
pthread_join(tid,nullptr);
return 0;
}
一个线程默认是jointable的,如果设置了分离状态就不能再分离了。
7.4.2新线程自己分离自己
pthread_self首先要获得自己的线程id
谁调用这个函数,就返回这个线程的id
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
std::string changeId(const pthread_t &threadid)
{
char tid[128];
snprintf(tid,sizeof(tid),"ox%x",(unsigned int)threadid); //自己获取自己的线程id
return tid;
}
void* start_routine(void* args)
{
std::string thread_name = static_cast<const char*>(args);
pthread_detach(pthread_self()); //自己把自己设置为分离状态
int cnt = 5;
while(cnt--)
{
std::cout << "name:" << thread_name << "running...new thread id:"<< changeId(pthread_self()) << std::endl;
sleep(1);
}
}
int main(int argc, char const *argv[])
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,(void*)"thread_1");
std::string main_id = changeId(pthread_self());
std::cout << "main running...main thread id:"<< main_id << " new thread id:"<< changeId(tid) << std::endl;
int n = pthread_join(tid,nullptr);
std::cout<<"result " << n << ":" << strerror(n) << std::endl;
return 0;
}
std::cout
无论线程是否被分离,int n = pthread_join(tid,nullptr);中的n都是0,success,为什么呢?
新线程和主线程,创建号后谁先运行?
不确定,如果新线程还没执行pthread_detach,主线程直接join,就直接进入阻塞等待了。不管你后面有没有分离
这是有问题的。
一个线程被join的时候,一定要保证已经被分离了。可以在之前先sleep一下。
这种做法,不太合理,还是推荐由主线程直接将新线程分离。
7.4.3主线程分离新线程
#include <iostream>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <cstring>
std::string changeId(const pthread_t &threadid)
{
char tid[128];
snprintf(tid,sizeof(tid),"ox%x",(unsigned int)threadid); //自己获取自己的线程id
return tid;
}
void* start_routine(void* args)
{
std::string thread_name = static_cast<const char*>(args);
//pthread_detach(pthread_self()); //自己把自己设置为分离状态
int cnt = 5;
while(cnt--)
{
std::cout << "name:" << thread_name << "running...new thread id:"<< changeId(pthread_self()) << std::endl;
sleep(1);
}
}
int main(int argc, char const *argv[])
{
pthread_t tid;
pthread_create(&tid,nullptr,start_routine,(void*)"thread_1");
pthread_detach(pthread_self()); //主线程在创建好新线程的时候,直接将其设置为分离状态
std::string main_id = changeId(pthread_self());
std::cout << "main running...main thread id:"<< main_id << " new thread id:"<< changeId(tid) << std::endl;
sleep(2);
int n = pthread_join(tid,nullptr);
std::cout<<"result " << n << ":" << strerror(n) << std::endl;
return 0;
}
7.4.4pthread_attr_t attr; /*通过线程属性来设置游离态(分离态)*/
设置线程属性为分离
pthread_attr_t attr;
pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
pthread_create(&tid,&attr,func,NULL);