前置知识
在谈多进程之前,我们在谈一谈页表,在语言中:char* str=”hello world”; *str=”H”;运行时会报错,原因在于:字符串在已初始化数据区和代码区之间的,需要写的时候,我们需要对str进行虚拟地址和物理地址的转换,查到了物理地址,继续查页表,然后查到了只有R权限,然后你的操作是写操作,MMU会将当前行为终止,向硬件报错,OS识别到硬件报错,把硬件操作转换为信号(段错误,11号信号),给进程发送11号信号,进程就要处理信号,信号的默认动作是终止,所以就终止了
如何看待地址空间和页表:
- 地址空间是进程能看到的资源窗口
- 页表决定进程真正拥有资源的情况
- 合理的对地址空间+页表进行资源划分,我们就可以对一个进程的所有资源进行分类
页表是如何从虚拟地址到物理地址的?
先查找虚拟地址的前10个比特位,索引页目录找到指定的页表项,再从虚拟地址的后10个比特,索引页表项找到物理内存的启始位置,到页框以后再从虚拟地址的后12位找到页内起始地址,剩下的根据类型,从起始地址处再继续向后找
何为线程?
**线程:**线程是OS能够进行运算调度的基本单位。线程是一个进程中的一个单一执行流,通俗地说,一个程序里的一个执行路线就叫做线程。
可以知道的是,一个进程至少有一个执行线程,这个线程就是主执行流。一个进程的多个执行流是共享进程地址空间内的资源,也就是说进程的资源被合理分配给了每一个执行流,这些样就形成了线程执行流。所以说线程在进程内部运行,本质是在进程地址空间内运行。
因为一个进程有多个线程,所以要对线程描述再组织,tcb,例如windows,单纯从线程角度,线程和进程有很多地方是重叠的,linux没有给线程设计专门的数据结构,而是直接服用pcb
linux下的线程
Linux下没有真正意义上的线程,线程是通过进程来模拟实现的
进程是承担分配系统资源的基本实体,即一堆的task_struct,地址空间,页表,代码和数据,而线程就是创建多个task_struct,指向父进程的地址空间
- 站在CPU视角,每一个PCB,都可以称之为轻量级进程(因为CPU内部看到PCB可能就是一个进程内部的执行流,并不能表示完整独立的一个进程)
- 线程是CPU调度的基本单位,而进程是承担分配系统资源的基本单位
- 进程用来整体申请资源 ,线程向进程要资源
没有真正的线程的好处是什么?
简单,维护成本降低
补充知识:虚拟内存决定了进程能够看到的“资源”
关系图:
线程创建
1.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:传给启动函数的参数(**这个是void *(start_routine) (void ),对应的参数)
返回值:
成功返回0,失败返回错误码
错误检查:
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
按照以前的编译是过不去的!因为linux没有真正意义上的线程,linux也就无法直接提供创建线程的系统调用接口!而只能给我们提供轻量级的进程接口!
如果我们使用的库不是语言上的接口,也不是操作系统的接口,是用户及线程库提供的,这是动静态链接所涉及的问题,-L找到动静态库在哪里, -I(大写i)找到头文件在哪里,-l(小写l)找到库名,这里的库名是phread,所以写成gcc -o #@ #^ -lpthread
实验:创建一个线程
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
using namespace std;
int cnt=0;
//新线程
void* thread_routine(void* args)
{
char* name = (char*)args;
while(1)
{
// cout << "我是新线程,我正在运行! " << name << "cnt " << cnt << endl;
printf("我是新线程,我正在运行...,name=%s,cnt=%d\n", name,cnt);
sleep(1);
}
}
int main()
{
//创建线程tid
pthread_t tid;
int n=pthread_create(&tid,nullptr,thread_routine,(void*)"thread one ");
assert(n==0);
//主线程
while(1)
{
cnt ++ ;
// cout << "我是主线程,我正在运行!" << "cnt " << cnt << endl;
printf("我是主线程,我正在运行...,cnt=%d\n",cnt);
sleep(1);
}
return 0;
}
运行结果如下:
结论:每一个轻量级进程都有一个id,每一个轻量级进程id不同,但是pid是一样的!CPU调度的时候,以LWP为标识符标定一个特定的执行流,线程一旦被创建,几乎所有资源都是被所有线程共享的!
但同时线程也一定要有自己的私有资源,什么资源应该是私有的呢?
- pcb属性私有(独立的线程id,独立的优先级,独立的状态……)
- 要有一定私有的上下文结构(一个线程在进行切换的时候代码可能没跑完,要切换就要上下文保存)
- 每一个线程都要有独立的栈结构
一个线程如果出现了异常,会影响其他线程,健壮性比较差,因为信号叫做进程信号,信号是整体发送给进程的
实验:创建多个线程
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <vector>
using namespace std;
//新线程
void* thread_routine(void* args)
{
char* name = (char*)args;
while(1)
{
printf("我是新线程,我正在运行...,name=%s\n", name);
sleep(1);
}
}
int main()
{
//1.创建一批线程
vector<pthread_t> tids;
#define NUM 10
for(int i=0;i<NUM;++i)
{
char namebuffer[64];
snprintf(namebuffer,sizeof(namebuffer),"%s:%d","thread",i);
pthread_t tid;
pthread_create(&tid,nullptr,thread_routine,namebuffer);
}
//主线程
while(1)
{
printf("我是主线程,我正在运行...\n");
sleep(1);
}
return 0;
}
运行结果如下:
如果我们在pthread_create后面加上sleep(1);又会正常显示线程的编号,这是为什么呢?
当我们创建新线程谁先运行不确定,可能线程被创建出来了,还没来得及进行后续代码,继续跑主线程代码,主线程对缓冲区重新写入 ,你只是把缓冲区的地址传过去,但是缓冲区被下一次重新格式化显示覆盖了,所以是最新的编号
解决方法如下:
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <vector>
using namespace std;
class ThreadData
{
public:
pthread_t tid;
char namebuffer[64];
};
//新线程
//这个函数被多个执行流访问,就是可重入函数!因为一个线程运行也没有影响另一个线程
//如果在函数里定义了变量,是局部变量,具有临时性,在多线程里也一样,这也证明了每一个线程都有独立的栈结构
void* thread_routine(void* args)
{
ThreadData* td = (ThreadData*)args;
while(1)
{
//不过这里的打印有问题,这里无法解释,后面解释
printf("我是新线程,我正在运行...,name=%s\n", td->namebuffer);
sleep(1);
}
delete td;
return nullptr;
}
int main()
{
//1.创建一批线程
vector<pthread_t> tids;
#define NUM 10
for(int i=0;i<NUM;++i)
{
//每一个线程创建都会new一个对象,把对象的地址传过去,不会共享buffer!
ThreadData* td=new ThreadData();
char namebuffer[64];
snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i);
pthread_create(&td->tid,nullptr,thread_routine,td);
}
//主线程
while(1)
{
printf("我是主线程,我正在运行...\n");
sleep(1);
}
return 0;
}
运行结果如下:
线程终止
如果在线程中使用exit,会导致整个进程结束,因为exit是用来结束进程的而不是线程!
如果值想终止某个线程而不是整个进程,有三种方式:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。(如果主线程在没有循环执行的情况下,走到return相当于终止整个进程!)
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程
1.pthread_exit函数———线程终止
void pthread_exit(void *retval);
参数:
retval:不能指向局部变量
实验:退出线程
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <vector>
using namespace std;
class ThreadData
{
public:
pthread_t tid;
char namebuffer[64];
};
//新线程
void* thread_routine(void* args)
{
sleep(1);
ThreadData* td = (ThreadData*)args;
while(1)
{
printf("我是新线程,我正在运行...,name=%s\n", td->namebuffer);
sleep(1);
// return nullptr;
pthread_exit(nullptr);
}
}
int main()
{
//1.创建一批线程
vector<ThreadData*> threads;
#define NUM 10
for(int i=0;i<NUM;++i)
{
//每一个线程创建都会new一个对象,把对象的地址传过去,不会共享buffer!
ThreadData* td=new ThreadData();
char namebuffer[64];
snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i);
pthread_create(&td->tid,nullptr,thread_routine,td);
//方便后续处理
threads.push_back(td);
}
//主线程查看其他线程的创建情况
for(auto& it : threads)
{
cout << "create thread :" <<it->namebuffer << ": " << it->tid << "success" << endl;
}
//主线程
while(1)
{
printf("我是主线程,我正在运行...\n");
sleep(1);
}
return 0;
}
运行结果如下:
2.pthread_cancel函数———取消一个线程
前提是这个线程已经运行起来了!
int pthread_cancel(pthread_t thread);
参数:
thread:线程ID
返回值:
成功返回0,失败返回错误码
实验:主线程取消新线程
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <vector>
using namespace std;
class ThreadData
{
public:
pthread_t tid;
char namebuffer[64];
};
class ThreadReturn
{
public:
int exit_code;
int exit_result;
};
void* thread_routine(void* args)
{
int cnt=3;
while(cnt--)
{
cout << "新线程" << endl;
sleep(1);
}
return (void*)16;
}
int main()
{
//1.创建一批线程
vector<ThreadData*> threads;
#define NUM 10
for(int i=0;i<NUM;++i)
{
//每一个线程创建都会new一个对象,把对象的地址传过去,不会共享buffer!
ThreadData* td=new ThreadData();
char namebuffer[64];
snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i);
pthread_create(&td->tid,nullptr,thread_routine,td);
//方便后续处理
threads.push_back(td);
}
for(int i = 0; i < threads.size()/2; i++)
{
pthread_cancel(threads[i]->tid);
cout << "pthread_cancel : " << threads[i]->namebuffer << " success" << endl;
}
//主线程
for(auto& it : threads)
{
int* ret=nullptr;
pthread_join(it->tid,(void**)&ret);
cout << "join :" <<it->namebuffer << "success,number" << (long long)ret <<endl;
delete it;
}
return 0;
}
运行结果如下:
线程的等待
线程不等待也会造成类似僵尸进程的问题———内存泄露
- 获取线程的退出信息(可以不关心)
- 回收新线程对应的PCB等内核资源,防止内存泄露(这个暂时无法查看)
1.pthread_join函数———等待一个线程结束(阻塞式等待)
int pthread_join(pthread_t thread, void **retval);
参数:
thread:线程ID
retval:输出型参数,指向线程退出的返回值
返回值:
成功返回0,失败返回错误码
为什么没有所谓的退出信号的获取?因为线程出异常会全部退掉,只需要获取退出码
实验:回收线程
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <vector>
using namespace std;
class ThreadData
{
public:
pthread_t tid;
char namebuffer[64];
};
//新线程
//这个函数被多个执行流访问,就是可重入函数!因为一个线程运行也没有影响另一个线程
//如果在函数里定义了变量,是局部变量,具有临时性,在多线程里也一样,这也证明了每一个线程都有独立的栈结构
void* thread_routine(void* args)
{
sleep(1);
ThreadData* td = (ThreadData*)args;
while(1)
{
printf("我是新线程,我正在运行...,name=%s\n", td->namebuffer);
sleep(1);
return nullptr;
}
}
int main()
{
//1.创建一批线程
vector<ThreadData*> threads;
#define NUM 10
for(int i=0;i<NUM;++i)
{
//每一个线程创建都会new一个对象,把对象的地址传过去,不会共享buffer!
ThreadData* td=new ThreadData();
char namebuffer[64];
snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i);
pthread_create(&td->tid,nullptr,thread_routine,td);
//方便后续处理
threads.push_back(td);
}
//主线程查看其他线程的创建情况
for(auto& it : threads)
{
cout << "create thread :" <<it->namebuffer << ": " << it->tid << "success" << endl;
}
//主线程
for(auto& it : threads)
{
pthread_join(it->tid,nullptr);
cout << "join :" << it->namebuffer << "success" <<endl;
//不能在新线程里delete,因为主线程和新线程的执行是随机的,可能已经被删除了,但是主线程才访问,会造成段错误
delete it;
}
return 0;
}
运行结果如下:
实验:线程等待获得退出结果
#include <stdio.h>
#include <iostream>
#include <unistd.h>
#include <assert.h>
#include <pthread.h>
#include <vector>
using namespace std;
class ThreadData
{
public:
pthread_t tid;
char namebuffer[64];
};
void* thread_routine(void* args)
{
//一样的效果
// return (void*)16;
pthread_exit((void*)16);
}
int main()
{
//1.创建一批线程
vector<ThreadData*> threads;
#define NUM 10
for(int i=0;i<NUM;++i)
{
//每一个线程创建都会new一个对象,把对象的地址传过去,不会共享buffer!
ThreadData* td=new ThreadData();
char namebuffer[64];
snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i);
pthread_create(&td->tid,nullptr,thread_routine,td);
//方便后续处理
threads.push_back(td);
}
//主线程
for(auto& it : threads)
{
void* ret=nullptr;
pthread_join(it->tid,&ret);
cout << "join :" << it->namebuffer << "success,number" << (long long)ret <<endl;
}
return 0;
}
运行结果如下:
为什么二级指针可以这样使用?如图所示
线程分离
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
如果设置了默认分离状态,不能够进行等待了!
1.pthread_detach函数———对一个线程进行分离
int pthread_detach(pthread_t thread);
参数:
thread:线程ID
返回值:
成功返回0,失败返回错误码
为了获得线程id我们还需要学一个函数
2.pthread_self()———获得线程id
pthread_t pthread_self(void);
实验:分离线程
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void* pthreadrun(void* arg)
{
int count = 0;
pthread_detach(pthread_self());
while (1){
printf("new threaad is running, pid is %d, thread id is %p\n", getpid(), pthread_self());
sleep(1);
if (++count == 3){
pthread_exit(NULL);
}
}
}
int main()
{
//pthread_t pthread[5];
pthread_t thread;
pthread_create(&thread, NULL, pthreadrun, NULL);
sleep(1);// 让线程先分离
if (pthread_join(thread, NULL) == 0){
printf("wait success\n");
}else{
printf("wait failed\n");
}
return 0;
}
运行结果如下:
线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
1.进程切换:页表&&虚拟地址空间&&切换PCB&&上下文切换
2.线程(轻量级进程)切换:切换PCB&&上下文切换
3.CPU除了寄存器,还存在cache(CPU内的硬件级缓存),软件存在一个属性:局部性原理——当前正在访问的代码和数据附近的代码有较大的概率被访问到,如果当前进程内部在进行处理的数据的时候,访问到的代码和数据被预先访问到cache中,CPU在读取时先读取cache,如果cache没有命中,再去内存中读取,读取先缓冲到cache再读取,一个运行很久的进程,cache缓存了很多热点数据,线程在切换时,cache不用被切换,但进程切换,需要重新缓存,线程切换cache不用太更新,但是进程切换重新更新
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用(使用CPU资源,例如加密,解密),为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用(使用外设资源,例如磁盘,网络),为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作
线程的缺点
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型
线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的
同步和调度开销,而可用的资源不变
- 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了
不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的
- 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
- 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
线程的共享与独立
共享:
进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
独立:
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
线程的深层理解
原生线程库中可能要存在多个线程,我们需要对线程进行管理,先描述(线程的属性比较少),再组织,每一次创建线程都要在pthread库中创建结构体,每一个结构体都会对应到一个轻量级进程的执行流
至于线程有独立的栈结构,具体如何实现的?如下图:
主线程用主线程的栈,其他线程用的栈在共享区,更准确点是在线程库中
如果我们就想再全局区创建变量,但是要求在线程独立的栈中我们可以在类型前加_thread,例如_thread int g_val=100;
封装用户级线程库
虽然这个是用户级线程库,但是还是不够简单,所以我们可以封装起来
.hpp开源代码,类的生命和实现放在一起不用像之前.c .h分开写
Thread.hpp
#include <iostream>
#include <vector>
#include <cstdio>
#include <cstring>
#include <string>
#include <unistd.h>
#include <functional>
#include <cassert>
#include <pthread.h>
using namespace std;
class Thread;
class context
{
public:
Thread* this_;
void *args_;
context()
:this_(nullptr)
,args_(nullptr)
{}
};
class Thread
{
public:
typedef function<void*(void*)> func_t;
const int num=1024;
Thread(func_t func,void* args,int number)
:func_(func)
,args_(args)
{
char buffer[num];
snprintf(buffer,sizeof(buffer),"thread-%d",number);
name_=buffer;
context *ctx=new context();
ctx->this_= this;
ctx->args_=args_;
int n=pthread_create(&tid_,nullptr,start_routine,ctx);
assert(n==0);
(void)n;
}
static void *start_routine(void* args) //类内成员,使用static没有this指针了
{
//把对应的方法再做一下包 装
context* ctx=(context*)args;
void* ret=ctx->this_->run(ctx->args_);
delete ctx;
return ret;
// 静态方法不能调用成员方法,把成员变量变量变为static可以,但不是好方法!
// return func_(args);
}
void* run(void* args)
{
return func_(args);
}
void join()
{
int n=pthread_join(tid_,nullptr);
assert(n==0);
(void)n;
}
~Thread()
{
//do nothing
}
private:
string name_;
pthread_t tid_;
func_t func_;
void* args_;
};
void *getTicket(void* args)
{
string work_type=(char*) args;
while(true)
{
cout << "我是一个新线程,我正在做:" << work_type << endl;
sleep(1);
}
}
test.cpp
#include"Thread.hpp"
int main()
{
Thread* thread1=new Thread(getTicket,(void*)"user1",1);
Thread* thread2=new Thread(getTicket,(void*)"user2",2);
Thread* thread3=new Thread(getTicket,(void*)"user3",3);
Thread* thread4=new Thread(getTicket,(void*)"user4",4);
// thread1->join();
// thread2->join();
// thread3->join();
// thread4->join();
while(1)
{
;
}
}