Linux多线程

线程概念

什么是线程

  • 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”
  • 一切进程至少都有一个执行线程
  • 线程在进程内部运行,本质是在进程地址空间内运行
  • 在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
  • 透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流

在这里插入图片描述
现在我们创建“进程“,不独立创建地址空间,用户级页表,甚至不进行IO将程序的数据和代码加载到内存,我们只创建task_struct,然后让新的PCB,指向和老的PCB指向同样的mm_ struct。然后,通过合理的资面分配(当前进程的资源),让每个task_struct都能使用进程的一部分资源 ! 此时我们的每个PCB被CPU调度的时候,执行的‘粒度’比原始进程执行的‘粒度’要更小一些!

注意: Linux下并不存在真正的多线程!而是用进程模拟的!

操作系统中存在大量的进程,一个进程内又存在一个或多个线程,操作系统需要对这些线程进行管理。如果要支持真的线程一定会提高设计操作系统的复杂程度。在Linux看来,描述线程的控制块和描述进程的控制块是类似的,因此Linux并没有重新为线程设计数据结构,而是直接复用了进程控制块,所以我们说Linux中的所有执行流都叫做轻量级进程。

但也有支持真的线程的操作系统,比如Windows操作系统,因此Windows操作系统系统的实现逻辑一定比Linux操作系统的实现逻辑要复杂得多。

线程的优点

  • 创建一个新线程的代价要比创建一个新进程小得多
  • 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
  • 线程占用的资源要比进程少很多
  • 能充分利用多处理器的可并行数量
  • 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
  • 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
  • I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。

计算密集型: 执行流的任务主要以计算为主。比如加密解密、大数据查找等为主。
IO密集型: 执行流的任务主要以IO为主。比如刷磁盘、访问数据库、访问网络等为主。

线程的缺点

性能损失 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。

健壮性降低 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。

缺乏访问控制 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。

编程难度提高 编写与调试一个多线程程序比单线程程序困难得多

线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

线程用途

  • 合理的使用多线程,能提高CPU密集型程序的执行效率
  • 合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)

Linux进程VS线程

进程和线程

  • 进程是资源分配的基本单位
  • 线程是调度的基本单位
  • 线程共享进程数据,但也拥有自己的一部分数据

(1)线程ID
(2)一组寄存器。(存储每个线程的上下文信息)
(3)栈。(每个线程都有临时的数据,需要压栈出栈)
(4)errno。(C语言提供的全局变量,每个线程都有自己的)
(5)信号屏蔽字。
(6)调度优先级。

进程的多个线程共享

因为线程是在同一地址空间,所以代码段(Text Segment)、数据段(Data Segment)都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到。

除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表
  • 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
  • 当前工作目录
  • 用户id和组id

进程和线程的关系如下图:

在这里插入图片描述

Linux线程控制

POSIX线程库

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
  • 要使用这些函数库,要通过引入头文<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的“-lpthread”选项

创建线程

创建函数: 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变量的开销更小。

使用示例:

#include<iostream>
#include<pthread.h>
#include<unistd.h>

using namespace std;

void *thread_run(void* args)
{
        while(true){
                cout<<(char*)args<<endl;
                sleep(2);
        }
        return nullptr;
}

int main()
{
        pthread_t tid;
        pthread_create(&tid,nullptr,thread_run,(void*)"thread 1");

        while(true){
                cout<<"main thread is running..."<<endl;
                sleep(1);
        }

        return 0;
}

运行结果
在这里插入图片描述
当我们用ps axj命令查看当前进程的信息时,虽然此时该进程中有两个线程,但是我们看到的进程只有一个,因为这两个线程都是属于同一个进程的。
在这里插入图片描述
我们可以使用ps -aL命令,显示当前的轻量级进程。
在这里插入图片描述
注意: 在Linux中,应用层的线程与内核的LWP(Light Weight Process)是一一对应的,实际上操作系统调度的时候采用的是LWP,而并非PID,只不过我们之前接触到的都是单线程进程,其PID和LWP是相等的,所以对于单线程进程来说,调度时采用PID和LWP是一样的。

让主线程创建一批新线程

让主线程一次性创建五个新线程,每个线程都去执行

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdio>

using namespace std;

// void* 是系统设计的一个通用接口
void *ThreadRoutine(void* args)
{
        int i=*(int*)args;
        delete  (int*)args;

        int cnt=0;
        while(cnt<5){
                cout<<"thread index : "<<i<<"count : "<<cnt<<endl;
                sleep(1);
                cnt++;
        }

        return nullptr;
}

int main()
{
#define NUM 5
        pthread_t tids[NUM];
        for(auto i=0;i<NUM;i++){
                int* p=new int(i);
                pthread_create(tids+i,nullptr,ThreadRoutine,p);
        }

        while(true){
                cout<<"main thread is running..."<<endl;
                sleep(1);
        }
        return 0;
}

运行结果如下
在这里插入图片描述

线程终止

终止某个线程(不是终止整个进程)有三种方法

  1. 线程函数return。
  2. 调用pthread_exit函数终止自己。
  3. 调用pthread_cancel函数终止同一进程中的另一个线程。

pthread_exit

功能: 终止线程
函数原型

void pthread_exit(void *retval);

参数: retval:线程退出时的退出码信息。

注意:

  • 该函数无返回值,类似进程,线程结束时无法返回它的调用者(自身)。
  • pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其他线程得到这个返回指针时,线程函数已经退出了。

例,下面代码中,使用pthread_exit函数终止线程,并将线程的退出码设置为10。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdio>

using namespace std;

// void* 是系统设计的一个通用接口
void *ThreadRoutine(void* args)
{
        int i=*(int*)args;
        delete  (int*)args;

        int cnt=0;
        while(cnt<5){
                cout<<"thread index : "<<i<<"count : "<<cnt<<endl;
                sleep(1);
                cnt++;
        }

        pthread_exit((void*)10);
}

int main()
{
#define NUM 5
        pthread_t tids[NUM];
        for(auto i=0;i<NUM;i++){
                int* p=new int(i);
                pthread_create(tids+i,nullptr,ThreadRoutine,p);
        }

        while(true){
                cout<<"main thread is running..."<<endl;
                sleep(1);
        }
        return 0;
}

注意: 不能使用exit函数终止线程,exit函数的作用是终止进程,线程调用exit函数也是终止整个进程。

pthread_cancel

功能: 取消某一个线程
函数原型

int pthread_cancel(pthread_t thread);

参数: thread:要取消线程的ID
返回值: 线程取消成功返回0,失败返回错误码。

pthread_cancel需要获取线程的ID,获取进程ID有两种方法

  1. 创建线程时通过输出型参数获得。
  2. 通过调用pthread_self函数获得。

pthread_self函数的函数原型如下:

pthread_t pthread_self(void);

调用pthread_self函数即可获得当前线程的ID

下面代码先让主线程休息,新线程跑,再依次回收新线程。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
#include<cstdio>

using namespace std;

// void* 是系统设计的一个通用接口
void *ThreadRoutine(void* args)
{
        int i=*(int*)args;
        delete  (int*)args;

        while(true){
                cout<<"thread index : "<<i<<"count : "<<cnt<<endl;
                sleep(1);
        }
}

int main()
{
#define NUM 2
        pthread_t tids[NUM];
        for(auto i=0;i<NUM;i++){
                int* p=new int(i);
                pthread_create(tids+i,nullptr,ThreadRoutine,p);
        }

        sleep(5);
        for(auto i=0;i<NUM;i++){
                pthread_cancel(tids[i]);
                cout<<"thread : "<< tids[i]<<" been cancel!"<<endl;
                sleep(1);
        }

        while(true){
                cout<<"main thread is running..."<<endl;
                sleep(1);
        }
        return 0;
}

运行结果如下
在这里插入图片描述

虽然线程可以自己取消自己,但一般不这样做,我们往往是用于一个线程取消另一个线程,常用的有主线程取消新线程。

注意: 新线程也是可以取消主线程的,取消后新线程继续运行,主线程不再往下运行。一般不建议这样做,新线程需要主线程来回收,如果干掉主线程,新线程无法回收会造成内存泄漏。

线程等待

为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。

pthread_join

功能: 等待线程结束
函数原型

int pthread_join(pthread_t thread, void **value_ptr);

参数:

  • thread:线程ID
  • value_ptr:它指向一个指针,后者指向线程的返回值

返回值: 成功返回0;失败返回错误码

调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:

  1. 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
  2. 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
  3. 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
  4. 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

注意: PTHREAD_CANCELED实际上就是头文件<pthread.h>里面的一个宏定义,它的值本质就是-1。

例:下面代码中等待线程退出后,打印线程的信息

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;

#define NUM 3

void* Routine(void* args)
{
        int cnt=3;
        while(cnt){
                cout<<"thread : "<<pthread_self()<<" | count : "<<cnt<<" | is running!"<<endl;
                cnt--;
                sleep(1);
        }
        return nullptr;
}

int main()
{
        pthread_t tids[NUM];
        for(auto i=0;i<NUM;i++){
                pthread_create(&tids[i],nullptr,Routine,nullptr);
        }

        cout<<"main thread join begin..."<<endl;
        for(auto i=0;i<NUM;i++){
                if(0==pthread_join(tids[i],nullptr)){// 不关心线程的退出信息时可将pthread_join函数的第二次参数设置为NULL
                        cout<<"thread "<<tids[i]<<" | quit...join success"<<endl;
                }
        }
        cout<<"main thread join end..."<<endl;
        return 0;
}

主线程对这五个线程进行了等待
在这里插入图片描述

下面再给线程设置退出码,为了方便查看这里设置成10,在主线程等待成功后打印线程的退出码。

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;

#define NUM 3

void* Routine(void* args)
{
        int cnt=3;
        while(cnt){
                cout<<"thread : "<<pthread_self()<<" | count : "<<cnt<<" | is running!"<<endl;
                cnt--;
                sleep(1);
        }
        return (void*)10;
}

int main()
{
        pthread_t tids[NUM];
        for(auto i=0;i<NUM;i++){
                pthread_create(&tids[i],nullptr,Routine,nullptr);
        }

        cout<<"main thread join begin..."<<endl;
        for(auto i=0;i<NUM;i++){
  						void* ret = nullptr;
  						pthread_join(tids[i],&ret)
                        cout<<"thread "<<tids[i]<<" exitcode: "<<(int)ret<<" | quit...join success"<<endl;
        }
        cout<<"main thread join end..."<<endl;
        return 0;
}

从运行结果可看出退出码改变为10
在这里插入图片描述

注意: pthread_join函数默认是以阻塞的方式进行线程等待的,而且pthread_join函数只能获取到线程正常退出时的退出码,用于判断线程的运行结果是否正确。

pthread_join函数不能获取到线程异常退出的退出码,因为线程是进程内的一个执行分支,如果进程中的某个线程崩溃了,那么整个进程也会因此而崩溃,此时我们没有机会执行pthread_join函数,因为整个进程已经退出了。

分离线程

  • 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成内存泄漏。
  • 但如果我们不关心线程的返回值,join也是一种负担,此时我们可以将该线程进行分离,后续当线程退出时就会自动释放线程资源。

pthread_detach

功能: 分离线程
函数原型:

int pthread_detach(pthread_t thread);

参数: thread:被分离线程的ID。

返回值: 分离成功返回0,失败返回错误码。

线程分离可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离

创建一个线程让其自己分离

#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

void* Routinue(void* args)
{
        pthread_detach(pthread_self());// 分离线程
        int cnt=5;
        while(cnt){
                cout<<"thread : "<<pthread_self()<<" cnt: "<<cnt--<<endl;
                sleep(1);
        }
        return (void*)10;
}

int main()
{
        pthread_t tid;
        pthread_create(&tid,nullptr,Routinue,nullptr);

        sleep(1);

        int ret=pthread_join(tid,nullptr);
        cout<<"main thread ret: "<<ret<<endl;

        return 0;
}

在这里插入图片描述
让主线程分离新线程

#include<iostream>
#include<unistd.h>
#include<pthread.h>

using namespace std;

void* Routinue(void* args)
{
        int cnt=5;
        while(cnt){
                cout<<"thread : "<<pthread_self()<<" cnt: "<<cnt--<<endl;
                sleep(1);
        }
        return (void*)10;
}

int main()
{
        pthread_t tid;
        pthread_create(&tid,nullptr,Routinue,nullptr);
        pthread_detach(tid);// 分离线程

        sleep(1);

        int ret=pthread_join(tid,nullptr);
        cout<<"main thread ret: "<<ret<<endl;

        return 0;
}

注意:

  • 如果一个线程被设置为分离状态,该线程不应该被join
  • 即使线程被设置为分离状态,线程出错崩溃仍会影响主线程和其他正常线程

线程ID及进程地址空间布局

  • pthread_create函数会产生一个线程ID,存放在第一个参数指向的地址中,该线程ID和内核中的LWP不是一回事。
  • 内核中的LWP属于进程调度的范畴,因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,这个ID属于NPTL线程库的范畴,线程库的后续操作就是根据该线程ID来操作线程的。
  • 线程库NPTL提供的pthread_self函数,获取的线程ID和pthread_create函数第一个参数获取的线程ID是一样的。

pthread_t 到底是什么类型呢?

取决于实现。对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址。

在这里插入图片描述

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值