Linux线程基础详解

x33g5p2x  于2022-04-06 转载在 Linux  
字(14.0k)|赞(0)|评价(0)|浏览(806)

Linux线程

Linux线程概念

什么是线程

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

书本上的概念:线程是进程的一个执行分支,是在进程内部运行的一个执行流!

那么线程具体化点是什么?

可执行程序在磁盘当中,磁盘中的代码和数据加载到物理内存时,是可以加载在任意位置的,进程地址空间通过页表映射到代码和数据,如果我们创建进程,不独立创建地址空间,不创建用户级页表,甚至不进行IO将程序的数据和代码加载到内存,我们只创建task_struct,然后让新的PCB,指向和老的PCB指向同样的mm_struct(进程地址空间)

然后,通过合理的资源分配(当前进程的资源)让每个task_struct都能使用进程的一部分资源,此时我们的每个PCB被CPU调度的时候,执行的粒度,是不是比原始进程执行的粒度要更小一些,这就是Linux线程的原理

地址空间:站在资源的角度,其实是进程的资源窗口

如何理解线程是在进程内部运行的一个执行流?线程本质是在进程的地址空间内运行

那么什么又是进程呢?

站在OS角度:进程承担分配系统资源的基本单位,一个进程被创建好之后,后续可能内部存在多个执行流(线程)

那么如何看待我们曾经所了解的进程呢?
本质是承担系统资源的基本实体,只不过内部只有一个执行流

举个例子:在生活当中我们想找爸爸妈妈要点钱,爸爸妈妈的钱早就有了,是在社会中赚的存在卡里面,这里的社会是OS,爸爸妈妈是进程,而我们是线程。

Linux线程和其他平台的线程

Linux当中的线程:站在CPU的角度,线程和进程有区别吗,没有任何区别,在CPU看来,它看到的都是task_struct,实际上,CPU执行的时候,可能执行的进程流已经比历史变得更加轻量化了,Linux下其实是没有真正意义上的线程概念的,而是用进程来模拟的!Linux下的线程称为"轻量级进程",Linux不可能直接在OS层面提供线程系统调用接口,最多轻量级进程的调度接口,而线程操作接口是由原生线程库提供。

其他平台不一定是Linux这样的,比如windows具有真正的线程概念!

系统内可能存在大量的进程,进程:线程=1:n,如果存在批量的进程,系统一定可能存在大量的线程,OS系统就需要管理这么多的线程:通过先描述,再组织进行管理,支持真线程的系统一定要做到描述线程:TCB(线程控制块),该操作系统既要进行进程管理,又要进行线程管理,这样的系统往往比较复杂,windows上有相关线程操作的系统调用接口

如果支持真线程,数据结构里面包含什么属性呢??必须包含和进程相似的一些属性

总结
进程:本质是承担分配系统资源的基本实体,线程是OS调度的基本单位

线程的优点

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

线程的缺点

因为所有的PCB都共享地址空间,理论上,每个线程都能看到进程的所有的资源,好处:线程间通信,成本特别低,缺点:一定存在大量的临界资源!势必可能需要使用各种互斥和同步机制!

  • 性能损失

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

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

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

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

异常

#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
//void*是系统层面设计的一个通用接口
void* thread_run(void *args)
{
    while(true)
    {
        cout<<(char*)args<<endl;
        sleep(2);
        int *p;
        *p = 100;
    }
    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;
}

我们发现新线程异常导致主线程也退出了,这是为什么呢?
线程是进程的一个执行分支,野指针,除0等异常操作导致线程退出的同时,也意味着进程触发了该错误,进而导致进程退出

进程和线程的区别

进程是资源分配的基本单位

线程是调度的基本单位

线程共享进程数据,但是也拥有自己的一部分资源:

  1. 上下文数据
    这部分数据独立说明线程是可以切换的

  2. 独立的栈结构
    独立的栈结构说明线程是独立运行的

  3. 信号屏蔽字

  4. 调度优先级

进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:

  • 文件描述符表共享
    需要注意的是多进程不共享文件描述符表,但是表里面的内容可以是一样的,而多线程共享文件描述符表

  • 每种信号的处理方式(SIG_IGN、SIG_DFL或者自定义的信号处理函数)

  • 当前工作目录

  • 用户id和组id

进程和线程的关系图:

pthread线程库

原生线程库是在应用层实现的线程库,不负责真正的在操作系统上帮你创建线程,而是给用户提供一些创建线程的接口,Linux是不会提供出一些创建线程的接口的,但是可以创建出一些轻量级进程接口(vfork)

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;失败返回错误码

第一个是输出型参数,第二个线程属性,第三个参数是函数指针

下面我们写一个创建线程的程序:

Makefile的编写:

mythread:mythread.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f mythread

-lpthread链接线程库,g++编译程序成为.o目标文件,然后在链接阶段找线程操作的相关函数实现

#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
//void*是系统层面设计的一个通用接口
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 ajx查到的是进程,ps -aL是查看系统轻量级进程的选项:

此时我们就看到了两个线程,需要注意的是这两个线程的PID相等,而这两个线程有个LWP。这是什么呢?PID相等,说明两个mythread本质是属于一个进程的
LWP表示执行流是一个轻量级进程,标识唯一性,CPU调度,看的就是LWP

那么这个创建线程的输出型参数是什么呢?下面我们来打印一下它:

#include<iostream>
using namespace std;
#include<pthread.h>
#include<unistd.h>
//void*是系统层面设计的一个通用接口
void* thread_run(void *args)
{
    while(true)
    {
        cout<<(char*)args<<endl;
        sleep(2);
        //int *p;
        //*p = 100;
    }
    return nullptr;
}
int main()
{
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    pthread_t tid4;
    
    pthread_create(&tid1,nullptr,thread_run,(void*)"thread 1");    									pthread_create(&tid2,nullptr,thread_run,(void*)"thread 2");
    pthread_create(&tid3,nullptr,thread_run,(void*)"thread 3");
    pthread_create(&tid4,nullptr,thread_run,(void*)"thread 4");
    
    printf("%d\n",tid1);
    printf("%d\n",tid2);
    printf("%d\n",tid3);
    printf("%d\n",tid4);
    while(true)
    {
        cout<<"main thread is running..."<<endl;
        sleep(1);
    }
    return 0;
}

我们打印一下这些tid到底是什么?

我们发现是一些很大的数字,注意:
LWP和tid是不一样的,LWP是Linux区别轻量级进程的唯一性,tid实际上是应用层管理库当中线程的相关标识符

我们以地址(%p)形式打印tid:

我们感觉tid有可能是一种地址数据,这个tid我们后面再说它是什么,下面我们先来看线程终止:

线程终止

线程终止有多种方法,线程退出的方法:

  • 线程退出,从自己的例程中return,就是线程退出

那我们可以在线程例程中调用exit进行终止线程吗?

注意
不可以调用exit终止线程,这样会将也进程(主线程)终止,下面将会验证

  • 调用pthread_exit
    终止线程

我们首先创建五个线程来验证线程终止:

return终止:

#include<iostream>
using namespace std;
#include<cstdio>
#include<pthread.h>
#include<unistd.h>
//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];
    //创建5个线程
    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;
}

我们打印结果看到五个线程已经退出了,已经不在打印了,但是我们在ps -aL可以看到还存在呢,这几个线程的资源并没有回收

所以后面还要线程等待

不能exit退出的验证:

#include<iostream>
using namespace std;
#include<cstdio>
#include<pthread.h>
#include<unistd.h>
//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++;
    }
    cout<<"thread: "<<i<<"quit"<<endl;
    exit(0);
    //return nullptr;//线程退出:从自己的例程中return就代表线程退出
}
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终止线程

pthread_self

每个线程可以获得自己的线程id:pthread_self

void* ThreadRoutine(void *args)
{
    //暂时的方案
    int i = *(int*)args;
    delete (int*)args;
    int cnt = 0;
    while(cnt<5)
    {
        cout<<"thread index : "<< i <<"count:"<<cnt<<"thread id: "<<pthread_self()<<endl;
        sleep(1);
        cnt++;
    }
    cout<<"thread: "<<i<<"quit"<<endl;
    //exit(0);
    return nullptr;//线程退出:从自己的例程中return就代表线程退出
}
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);  
        cout<< "create thread: "<<tid[s]<<"success"<<endl;
    }
    
    while(true)
    {
        cout<<"main thread is running..."<<"main thread id:"<<pthread_self()<<endl;
        sleep(1);
    }
    return 0;
}
pthread_exit线程终止
#include<iostream>
using namespace std;
#include<cstdio>
#include<pthread.h>
#include<unistd.h>
//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++;
    }
    cout<<"thread: "<<i<<"quit"<<endl;
    pthread_exit((void*)10);
    //exit(0);
    //return nullptr;//线程退出:从自己的例程中return就代表线程退出
}
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;
}

pthread_cancel

取消线程

void* ThreadRoutine(void *args)
{
    //暂时的方案
    int i = *(int*)args;
    delete (int*)args;
    int cnt = 0;
    while(true)//死循环执行
    {
        cout<<"thread index : "<< i <<"count:"<<cnt<<"thread id: "<<pthread_self()<<endl;
        sleep(1);
        cnt++;
    }
    cout<<"thread: "<<i<<"quit"<<endl;
    return nullptr;//线程退出:从自己的例程中return就代表线程退出
}
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);  
        cout<< "create thread: "<<tids[i]<<"success"<<endl;
    }
    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..."<<"main thread id:"<<pthread_self()<<endl;
        sleep(1);
    }
    return 0;
}

主线程可以取消创建的线程,那么创建的线程能够取消主线程吗?

pthread_t main_pthread;
void* ThreadRoutine(void *args)
{
    //暂时的方案
    int i = *(int*)args;
    delete (int*)args;
    int cnt = 0;
    while(true)
    {
        cout<<"thread index : "<< i <<"count:"<<cnt<<"thread id: "<<pthread_self()<<endl;
        sleep(1);
        cnt++;
        pthread_cancel(main_pthread);
    }
    
    cout<<"thread: "<<i<<"quit"<<endl;
    //exit(0);
    return nullptr;//线程退出:从自己的例程中return就代表线程退出
}
int main()
{
#define NUM 5
    main_pthread = pthread_self();
    pthread_t tids[NUM];
    for(auto i = 0;i<NUM;i++)
    {
        int *p = new int(i);
    	pthread_create(tids+i,nullptr,ThreadRoutine,p);  
        cout<< "create thread: "<<tid[s]<<"success"<<endl;
    }
    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..."<<"main thread id:"<<pthread_self()<<endl;
        sleep(1);
    }
    return 0;
}

我们通过ps -aL可以看到主线程变成了僵尸进程:

线程终止推荐前两种,不推荐使用最后一种

线程等待

线程一般终止以后,必须进行等待,那么由谁等呢?由main thread来等,如果不等待会造成和进程退出类似的效果(僵尸进程)

为什么需要线程等待?

  • 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
  • 创建新的线程不会复用刚才退出线程的地址空间。
int pthread_join(pthread_t thread,void **retval);

功能:等待线程结束
参数:

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

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

#include<iostream>
#include<unistd.h>
#include<stdlib.h>
#include<pthread.h>
using namespace std;
#define NUM 5
void* Routinue(void* args)
{
    int cnt = rand()%5+5;
    while(cnt)
    {
        cout<<"thread : "<<pthread_self()<<"| count: "<<cnt<<"| is running"<<endl;
        cnt--;
        sleep(1);
    }
    return nullptr;
}
int main()
{
    srand((unsigned long)time(nullptr));
    pthread_t tids[NUM];
    for(auto i = 0;i<NUM;i++)
    {
        pthread_create(&tids[i],nullptr,Routinue,nullptr);
    }
    
    for(auto i = 0;i<NUM;i++)
    {
        pthread_join(tids[i],nullptr);//线程等待
        cout<<"thread "<<tids[i]<<"| quit ... join success"<<endl;
    }
    return 0;
}

创建一个线程的目的是为了让我们知道这个线程把任务跑的怎么样?那么线程退出的状态我们怎么获知呢?

在进程当中有一个退出码,退出码为0,则success,!0则failed。

那么线程是不是这样呢?确实是类似的

其实在线程的例程函数当中return返回的就是退出码!,而我们怎么获取这个退出码呢?

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

我们线程等待的第二个参数是一个void**类型,其实它是一个输出型参数,我们定义一个void的变量,将这个变量的地址传给第二个参数,当线程的例程函数返回时,它将返回的void这个参数赋值给我们前面定义的变量。因为需要能够修改外面的值所以传的是二级指针

我们也可以定义一个结构体来返回结构体对象来描述线程的退出状态:

#include<iostream>
#include<unistd.h>
#include<pthread.h>
using namespace std;
#define NUM 5
struct exit_status
{
    int code;
    std::string desc;
}
exit_status*
void* Routinue(void* args)
{
    int cnt = rand()%5+5;
    while(cnt)
    {
        cout<<"thread : "<<pthread_self()<<"| count: "<<cnt<<"| is running"<<endl;
        cnt--;
        sleep(1);
    }
    exit_status* p = new exit_status();
    p->code = 10;
    p->desc = "thread exit normal!";
    return p;
}
int main()
{
    srand((unsigned long)time(nullptr));
    pthread_t tids[NUM];
    for(auto i = 0;i<NUM;i++)
    {
        pthread_create(&tids[i],nullptr,Routinue,nullptr);
    }
    
    cout<<"main thread join begin ..."<<endl;
    void* st = nullptr;
    for(auto i = 0;i<NUM;i++)
    {
        if(pthread_join(tids[i],&st) == 0)
        {
        	exit_status* p = st;
        	cout<<"thread "<<tids[i]<<"| exit code:"<<p->code<<"exit string: "<<p->desc<<"| quit ... join success"<<endl;  
        }
    }
    cout<<"main thread join end ..."<<endl;
    return 0;
}

上面讨论的退出都是正常的情况,如果发生异常呢??进程的exit code中,是包括退出码+信号的

进程退出的情况:

  1. 代码跑完结果正确
  2. 代码跑完,结果不正确
  3. 代码没跑完,程序崩溃

前两种通过退出码就可以辨别,最后一种通过信号辨别

那么main主线程join需不需要考虑线程崩溃的问题呢?

答案是不需要,线程崩溃了,进程也会崩溃,主线程的join变得毫无意义了,此时任务就转交给了父进程。

注意:

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

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

一个线程被取消,退出码是-1,PTHREAD_ CANCELED实际上是一个宏(void*)-1

cancel本身具有一定的延时性,可能并不是被立即受理的,在线程执行中取消最好。

线程分离pthread_detach

默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。

分离的本质是让主线程不用再join新线程,从而可以让新线程退出的时候,自动回收资源。

那么如何分离?

pthread_detach

void* Routinue(void* args)
{
    //pthread_detach(pthread_self());
    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;//ret不是0,join失败
    return 0;
}

如果一个线程被设置为分离状态,该线程不应该被join,如果你join,结果是未定义,至少一定会join出错,可以主线程将新线程分离:

ret不是0,join失败

注意:

一个细节:即便线程被设置为分离状态,但是如果该线程依旧出错崩溃,还是会影响主线程和其他正常线程

可以自己把自己分离,也可以主线程把你分离。

我们上面所说的这些接口都是原生线程库提供,总结:

  1. Linux操作系统没有真正意义上的线程是用进程模拟的!称之为轻量级进程
  2. Linux操作系统本身并不会提供类似线程创建,终止,等待,分离相关system call接口,但是会提供创建轻量级进程的接口,vfork
  3. 但是用户需要有线程创建,终止,等待,分离相关接口,所以,为了更好的适配,系统基于轻量级进程的接口,模拟封装了一个用户层的原生线程库,pthread
  4. 但是,系统可以通过PCB来进行管理,用户层,也得至少得知道,线程id,状态,优先级,其他属性用来进行用户级线程管理,tcb,不用内核维护,而在用户空间维护

原生线程库是一个库,只要是库,运行期间这个库一定会被加载到内存当中,这些代码和数据就被映射到共享区,从而被所有得执行流看到

曾经输出的pthread_t tid,获得的线程id是一个用户层的概念。本质其实是一个地址,这里的地址就是pthread库中的某一个起始位置,是共享区中的地址,是库当中的某个地址,是对应的线程相关属性的起始地址

用户层线程ID本质其实是一个地址,主线程不使用线程库中的栈结构,直接使用地址空间中的栈,Linux当中用户级线程和轻量级进程是1:1的,就相当于警察和卧底是一对一。

相关文章