Linux【多线程】之线程概念&线程特点&线程控制


在这里插入图片描述

一、线程概念

1.1 宏观上的说法

线程是进程中的一个执行流,我们来谈谈Liunx下具体的“线程”。

1.2 由进程引入线程

在这里插入图片描述

  • 之前所说的进程=内核数据结构+进程的代码和数据。每个进程都有自己独立的进程地址空间和独立的页表,也就意味着所有进程在运行时本身就具有独立性。
  • 那么当我们现在想要让不同的“进程”访问父进程中一部分资源,我们只需要创建PCB,并让其指向父进程资源。像这种只创建PCB,并从父进程中给它分配资源的执行流就叫做“线程”
  • 因为我们可以通过虚拟地址空间+页表的方式对进程进行资源划分,单个“进程”执行力度一定要比之前的进程要细
  • 线程是进程中的一个执行流—>线程在进程内部运行—>线程在地址空间内运行,并拥有一部分进程资源

⚠️1.3 Linux下进程与线程概念重构

  • 不同于Windows下线程设计方式(windows下专门为线程设计出了线程对象TCB)
  • ⚠️Linux中没有真正意义的线程; Linux设计者考虑到进程与线程调度的相似性,决定用进程PCB模拟线程,属于自己的一套方案
  • 进程用来整体申请资源,线程用来伸手向进程要资源
  • 进程模拟线程的好处:PCB模拟线程,为PCB编写的结构与算法都能进行复用,不用单独为线程创建调度算法,降低维护成本,复用进程的那一套.可靠高效

⚠️进程:内核角度:承担分配系统资源的基本实体
⚠️线程:CPU调度的基本单位

之前所说的进程内部只有一个执行流,现在有多个执行流
在Linux中,CPU看到task_struct,统一叫做轻量级进程

简单总结:进程是整个家庭,线程就是家庭中的成员

二、线程创建与分析

OS只认线程,用户(程序员)也只认线程,Linux没有真正意义上线程,所以Linux便无法直接提供创建线程的系统调用接口,而只能给我们提供创建轻量级进程的接口(clone系统调用)!
因此OS在用户和系统调用之间提供了一个用户级线程库(pthread库),用户在使用对线程的操作时,库里面会将其转换成对轻量级进程的操作
pthread库:原生线程库,任何linux系统都有

线程创建函数------pthread库提供的方法,编译时需要“-lpthread”
int pthread_create(pthread_t *tidp, const pthread_attr_t *attr,
( void *)(*start_rtn)( void *), void arg);
参数
第一个参数为指向线程 标识符的 指针。
第二个参数用来设置线程属性,设置成nullptr就可以。
第三个参数是线程运行函数的起始地址。
最后一个参数是函数指针的参数。
返回值
若线程创建成功,则返回0。若线程创建失败,则返回出错编号,并且
thread中的内容是未定义的。

//新线程
void * thread_routine(void* args)
{
    const char* name =(const char*) args;
    while (true)
    {
        cout<<"我是新进程,我正在运行 ! name: "<<name<<endl;
        sleep(1);
    }
    
}
int main()
{
    pthread_t tid;
    //创建线程
    int n=pthread_create(&tid,nullptr,thread_routine,(void*)"thread one");
    assert(0==n);
    (void)n;

    //主线程
    while (true)
    {
        char tidbuffer[64];
        snprintf(tidbuffer,sizeof tidbuffer,"0x%x",tid);
        cout << "我是主线程, 我正在运行!, 我创建出来的线程的tid: " << tidbuffer << endl;
        sleep(1);
    }
    return 0;
    
}

以上代码我们创建一个线程,让它去执行thread_routine函数,主线程执行后续代码

运行之后我们可以通过以下图片发现:只有一个进程20258,但是用发送信号的时候却把两个执行流全结束了,信号是发送给进程的,与进程直接相关,进程结束了,进程里面无论有多少线程也会随之结束(对信号有疑惑的可参考我主页关于信号的博客)
在这里插入图片描述

当线程在运行的时候我们可以通过 ps -aL 查看“线程”信息
LWP:轻量型进程ID
PID==LWP:主线程
PID!=LWP:新线程
CPU调度的时候,是以LWP表示特定的一个执行流
之前我们以PID识别独立的进程并没有问题,当只有一个执行流的时候,PID和LWP是等价的
在这里插入图片描述

三、线程的特点

进程一旦被创建,几乎所有的资源都是被线程共享的
⚠️线程一定有自己的私有资源,独属于线程
1.属于自己的PCB属性
2.私有的上下文数据(线程切换动态运行的证据,下同)
3.独立的栈结构虽然它们共享一个地址空间,但是在线程PCB里面会有一个独立的栈结,主线程用地址空间的栈,其它线程用共享区内的栈,其实是库里面维护的栈结构

3.1 线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 进程间切换,需要切换页表、虚拟空间、切换PCB、切换上下文
  • 线程间切换,线程都指向同一个地址空间,页表和虚拟地址空间就不需要切换了,只需要切换PCB和上下文,成本较低
  • 少的多体现在,线程切换不需要更新太多cache(存储大量经常使用的数据),进程切换要全部更新

在等待慢速I/O操作结束的同时,程序可执行其他的计算任务

计算密集型应用(CPU,加密,解密,算法等),为了能在多处理器系统上运行,将计算分解到多个线程中实现

I/O密集型应用(外设,访问磁盘,显示器,网络),为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作

3.2 线程的缺点

  1. 性能损失:一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
  2. 健壮性降低:编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的,一个线程崩可能影响另一个线程
  3. 缺乏访问控制:进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
  4. 编程难度提高:编写与调试一个多线程程序比单线程程序困难得多

四、线程控制

4.1 多个线程之间的相互影响

一个线程如果发生异常,是会影响其他线程的:前面我们提到了信号是整体发给进程的,当遇到错误信号,OS会给每个线程的PCB写入信号(线程的PID都一样),每个信号的处理行为都会把当前执行流终止掉,所以所有的线程都会退出
换个角度理解:线程的资源是进程给的,当线程因为错误退出的时候,要收回资源,即使其他的线程没有错误,也要被收回

void* static_routine(void * args)
{
    string name=static_cast<const char*>(args);//安全转型
    while (true)
    {
        cout<<"new thread create success,name "<<name<<endl;
        sleep(1);

        //检测线程间影响
        int* p=nullptr;
        *p=10;//野指针解引用,引发11号信号
    }
    
}

int main()
{
    pthread_t id;
    pthread_create(&id,nullptr,static_routine,(void*)"new");
	//没有错误也会退出
    while (true)
    {
        cout<<"main thread create success,name main"<<endl;
        sleep(1);
    }
    
    return 0;
}

可以看出线程的鲁棒性较差,如果是进程,因为具有独立性,即使一个进程出现问题,也不会影响另一个。

int main()
{
	    //创建多个线程
    vector<pthread_t*> tids;
#define NUM 10
    for(int i=0;i<NUM;++i)
    {
        pthread_t tid;
        char namebuffer[64];
        snprintf(namebuffer,sizeof(namebuffer),"%s:%d","thread",i+1);
        pthread_create(&tid,nullptr,static_routine,namebuffer);
    }
}

当我们采用以上方法创建多个进程的时候,会有些问题,由于pthread_create()函数最后一个参数传的是缓冲区的地址,并且主线程,新线程调度是随机的,就会引发创建的线程信息都是最新被刷新到缓冲区的信息,之前的缓冲区都被新的数据覆盖了!!!

因此我们采用以下方法创建多个线程

class ThreadData//包含线程信息的结构体
{
public:
    pthread_t tid;
    char namebuffer[64];

};
void* static_routine(void * args)
{
    ThreadData* td=static_cast<ThreadData*>(args);//安全转型
    int cnt=10;
    while (cnt)
    {
    	cout<<"cnt"<<cnt<<"&cnt"<<&cnt<<endl;//仅做演示,分析可重入函数把他忽略
        cnt--;
        sleep(1);
    }
    delete td;
    return nullptr;
}
int main()
{
        //创建多个线程
    vector<ThreadData*> threads;
#define NUM 10
    for(int i=0;i<NUM;++i)
    {
        
        //创建线程信息对象
        ThreadData* td=new ThreadData();
        //格式化写入
        snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);
        //每一个线程都有一个对象,并将对象的地址“拷贝”给函数的形参,这样每个线程都有自己的namebuffer
        //并且传到函数里面的td与主线程中的td没有关系
        
        //由于td已经是一个指针,并且是对结构体内容操作,这里不需要&td,虽然接受的形参也对不上
        //小编当时在编写的时候由于指针理解问题,纠结了一会
        pthread_create(&td->tid,nullptr,static_routine,td);
        threads.push_back(td);
    }
 }

验证发现:cnt地址都不一样,即每个线程都有一个独立的cnt
在这里插入图片描述

  • static_routine被多个线程同时执行且没有产生异常和二义性–>可重入函数
  • 像cnt,td这种在函数内定义的变量是局部变量,具有临时性。它们是线程独属的栈结构

4.2 线程等待–pthread_join

一个线程创建出来,那就要如同进程一样,也是需要被等待的。如果线程不等待,对应的PCB没被释放,就会造成类似僵尸进程的问题:内存泄漏。
所以线程也要被等待:
1.获取新线程的退出信息
2.回收新线程对应的PCB等内核资源,防止内存泄漏。

线程等待函数,默认成功调用,不考虑异常问题【如果有异常全都退出 】
int pthread_join(pthread_t thread, void **retval);

参数:thread:被等待线程的ID,retval:线程退出时的退出码信息

void** retval:输出型参数,主要用来获取线程函数结束时返回的退出结果。
返回值:线程等待成功返回0,失败返回错误码

class ThreadData//包含线程信息的结构体
{
public:
    pthread_t tid;
    char namebuffer[64];

};

void* static_routine(void * args)
{
    ThreadData* td=static_cast<ThreadData*>(args);//安全转型
    int cnt=10;
    while (cnt)
    {
        cout << "cnt: " << cnt << " &cnt: " << &cnt << endl;
        cnt--;
        sleep(1);
       
    }
    //delete td;//避免悬空指针问题,这里只使用,不回收
    pthread_exit(nullptr);
    //return nullptr;
}
int main()
{

    vector<ThreadData*> threads;
#define NUM 10
    for(int i=0;i<NUM;++i)
    {

        ThreadData* td=new ThreadData();
        snprintf(td->namebuffer,sizeof(td->namebuffer),"%s:%d","thread",i+1);

        pthread_create(&td->tid,nullptr,static_routine,td);
        threads.push_back(td);
    }	
	//线程等待
    for(auto& iter:threads)
    {
        int n=pthread_join(iter->tid,nullptr);
        assert(n==0);
        cout<<"join "<<iter->namebuffer<<" : "<<iter->tid<<" success"<<endl;
        //不在static_routine释放的原因:主线程与新线程是并行交叉运行的,执行到这一步有可能部分线程资源已经被释放,指针指向的地址是非法的了,访问这部分资源会引发未定义的行为
        delete iter;//外部统一申请,统一释放
    }
    cout<<"main thread quit"<<endl;
    return 0;
}

简单说明主线程创建,主线程回收由于线程之间并行交叉运行,有可能主线程先运行完,已经在线程等待了,而要等待的线程可能还没来得及释放资源那么上述线程等待代码在解引用的时候就不会报错,也有一部分线程已经释放了资源,那么解引用就会引发段错误。

4.3 线程终止(退出)–pthread_exit&return

return方式:在线程函数调用结束return的时候,线程就会终止,仅对从线程有用,对主线程return相当于调用exit(),整个进程都会退出

exit(); 任何一个执行流调用exit(),整个进程都会退出,所以不能用来终止线程

pthread_exit():

void* static_routine(void * args)
{
    ThreadData* td=static_cast<ThreadData*>(args);//安全转型
    int cnt=10;
    while (cnt)
    {
        cout << "cnt: " << cnt << " &cnt: " << &cnt << endl;
        cnt--;
        sleep(1);
        pthread_exit(nullptr);//执行一次就终止
    }
    delete td;
    //return nullptr;
}

通过监控脚本可以发现一共十一个线程,执行之后立马退出,只留下一个主线程(打印出现问题是因为线程抢占资源原因,后面会解决)
在这里插入图片描述

4.3.1 线程退出的返回值问题

线程退出时函数返回值会被保存在pthread库中,我们无法直接获取这个返回值,因此我们需要借用库函数,传递ret的地址,库函数内部会对retval解引用,再将函数返回值赋给它,相当于把返回值赋给了ret!!!
在这里插入图片描述

4.3.2 线程取消–pthread_cancel

pthread_cancel
线程是可以被其他线程取消的,但是线程要被取消,前提是这个线程是已经运行起来了。pthread_create取消也是线程终止的一种

#include <pthread.h>
int pthread_cancel(pthread_t thread);

线程如果是被取消的,退出码:-1
在这里插入图片描述

4.4 线程分离–pthread_detach

一个线程默认是joinable的,如果设置了分离状态,就不能够进行等待了

#include <pthread.h>
//获取线程ID
pthread_t pthread_self(void);
返回值:此函数始终成功,返回调用线程的ID。

//分离线程
int pthread_detach(pthread_t thread);
thread:线程ID
返回值:在成功时,pthread_detach()返回0; 在错误时,它返回一个错误码。

线程分离测试

//格式化线程ID
std::string changeId(const pthread_t &thread_id)
{
    char tid[128];
    snprintf(tid, sizeof(tid), "0x%x", thread_id);
    return tid;
}

void *start_routine(void *args)
{
    std::string threadname = static_cast<const char *>(args);
    // pthread_detach(pthread_self()); //设置自己为分离状态,但是由于线程创建之后调度随机,存在主线程已经执行线程等待,而新线程还没分离,最后退出的时候主线程还是回收了

    int cnt = 5;
    while (cnt--)
    {
        std::cout << threadname << " running ... : " << changeId(pthread_self()) << std::endl;
        sleep(1);
    }

    return nullptr;
}
int  main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, (void *)"thread 1");
    //获取主线程ID
    std::string main_id = changeId(pthread_self());
    
    pthread_detach(tid);//此处让主线程分离新线程,避免上述错误,分离之后也不用考虑回收问题

    std::cout << "main thread running ... new thread id: " << changeId(tid) <<" main thread id: " << main_id << std::endl;
	
	//如果设置分离状态成功,就不能再等待了,以下代码也会输出错误码
   // int n = pthread_join(tid, nullptr);
   // std::cout << "result: " << n << " : " <<strerror(n) << std::endl;
    return 0;
}

线程分离要注意分离时刻!

五、线程概念补充

5.1 线程ID

pthread_create创建一个线程,产生一个线程ID存放在第一个参数之中,该线程ID与内核中的LWP并不是一回事。pthread_create函数第一个参数指向一块虚拟内存单元,该内存单元的地址就是新创建线程ID,这个ID(tid)是线程库的范畴,而内核中LWP是进程调度的范畴,轻量级进程是OS调度的最小单位,需要一个数值来表示该唯一线程。


当我们创建轻量级进程的时候,pthread库中也会为我们创建对应的数据结构来描述线程。linux中我们称这种线程是用户级线程,其中用户关心的线程的线程属性在库中,内核中的提供线程执行流调度,也就是说用户级线程/内核轻量级线程=1。
在这里插入图片描述
其实所谓的tid就是库中所对应的一个地址,根据地址找到线程的存储,就能找到线程的属性,当用户想要使用线程,只需要拿着线程id就可以操作了

每个线程都有独自的栈:主线程采用的栈是进程地址空间中的栈,其他线程采用的是共享区(mmap+页表映射)中的栈,其实是库里面维护的栈结构

5.2 局部存储

所有线程共享全局资源,但是如果给全局变量加上__thread,可以将一个内置类型设置为线程局部存储变量还是全局变量,但是每个线程都有一份,不会互相影响

__thread int g_val = 100;
void *start_routine(void *args)
{
    std::string threadname = static_cast<const char *>(args);
    while (true)
    {
        std::cout << threadname<<" g_val: "<< g_val << " &g_val: " << &g_val << std::endl;
        sleep(1);
        g_val++;
    }

    return nullptr;
}
int  main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, start_routine, (void *)"new  thread");
    pthread_detach(tid);
    while(true)
    {
        std::cout << "main thread"<< " g_val: "<< g_val << " &g_val: " << &g_val << std::endl;
        sleep(1);
    }
    return 0;
}

在这里插入图片描述
从上图可以看到,两个线程的的g_val值并没有同时变化且两个变量地址不一样!

  • 10
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值