Linux线程概念 | 线程控制

Posted 阿亮joy.

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux线程概念 | 线程控制相关的知识,希望对你有一定的参考价值。

​🌠 作者:@阿亮joy.
🎆专栏:《学会Linux》
🎇 座右铭:每个优秀的人都有一段沉默的时光,那段时光是付出了很多努力却得不到结果的日子,我们把它叫做扎根

目录

👉知识补充👈


👉Linux线程概念👈

什么是线程


  • 线程是在进程内部执行的,也就是说线程是在进程的地址空间内运行的,其是操作系统调度的基本单位。
  • 进程等于内核数据结构加上该进程对应的代码和数据,内核数据结构可能不止一个 PCB,进程是承担分配系统资源的基本实体,将资源分配给线程!
  • 那如何理解我们之前写的代码呢?其实我们之前学习的是只有一个执行流的进程,而今天学习的是具有多个执行流的进程(task_struct 是进程内部的一个执行流),所以这两者是不冲突的。
  • 在运行队列中排队的都是 task_struct,CPU 只能看到 task_struct,CPU 根本不关系当前调度的是进程还是线程,只关心 task_struct。所以,CPU 调度的基本单位是”线程”。
  • Linux 下的线程是轻量级进程,没有真正意义上的线程结构,没有为线程专门设计内核数据结构,而是通过 PCB 来模拟实现出线程的。
  • Linux 并不能直接给我们提供线程相关的接口,只能提供轻量级进程的接口!在用户层实现了一套多进程方案,以库的方式提供给用户进行使用,这个库就是 pthread 线程库(原生线程库)。

知道了什么是线程,我们来学习创建线程的接口,来验证一下上面的结论!


pthread_create 函数的功能是创建一个新的线程。thread 是输出型参数,返回进程的 ID;attr 设置线程的属性,attr 为 nullptr 表示使用默认属性;start_routine 是一个函数地址,即线程启动后要执行的函数;arg 是传给线程启动函数的参数。调用成功是返回 0,错误是返回错误码。

Makefile

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

注:使用原生线程库时,必须带上 -lpthread,告诉编译器你要链接原生线程库,否则就会产生链接错误。


// 注:一下代码是示例代码,有些许问题
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstdio>
#include <string>

using namespace std;

void* threadRun(void* args)

    string name = (char*)args;
    while(1)
    
        cout << name << " id: " << getpid() << '\\n' << endl;
        sleep(1);
    
    return nullptr;


int main()

    pthread_t tid[5];
    char name[64];
    for (int i = 0; i < 5; i++)
    
        snprintf(name, sizeof name, "%s-%d", "thread", i);
        pthread_create(tid + i, nullptr, threadRun, (void*)name);
        sleep(3); // 缓解传参的bug
    

    while (true)
    
        cout << "main thread, pid: " << getpid() << endl;
        sleep(3);
    

    return 0;

ps -aL | head -1 && ps -aL | grep mythread | grep -v grep  #查找线程


将进程 16889 杀掉时,全部执行流都会终止。因为线程用的资源都是进程给的,而杀掉进程就要回收进程的资源,那么线程终止了是理所当然的。

线程是如何看到进程内部的资源的呢?

我们知道,线程的运行依赖于进程的资源,一旦进程退出,线程也会退出。那进程的哪些资源是线程之间共享的,哪些资源又是线程独自占用的呢?

进程的大多数资源都被线程所共享:

  • 文件描述符表,如果一个线程打开了一个文件,那么其他的线程也能够看到。
  • 每种信号的处理方式(SIG_IGN、SIG_DFL 或者自定义的信号处理函数)
  • 当前工作目录
  • 用户 ID 和组 ID
  • 进程地址空间的代码区、共享区
  • 已初始化、未初始化数据区,也就是全局变量
  • 堆区一般也是被所有线程共享的,但在使用时,认为线程申请的堆空间是线程私有的,因为只有这个线程拿到这段空间的其实地址

线程独自占用的资源:

  • 线程 ID
  • 一组寄存器。线程是 CPU 调度的基本单位,一个线程被调度一定会形成自己的上下文,那么这组寄存器必须是私有的,才能保证正常的调度。
  • 栈。每个线程都是要通过函数来完成某种任务的,函数中会定义各种临时变量,那么线程就需要有自己私有的栈来保存这些局部变量。
  • 错误码 errno、信号屏蔽字、调度优先级

线程 VS 进程

为什么线程的调度切换的成本更低呢?

线程进行切换时,进程地址空间和页表是不用换的。而进程进行切换时,需要将进程的上下文,进程地址空间、页表、PCB 等都要切换。CPU 内部是有 L1 ~ L3 的 Cache,CPU 执行指令时,会更具局部性原理将内存中的代码和数据预读到 CPU 的缓存中。如果是多线程,CPU 预读的代码和数据很大可能就会被所有的线程共享,那么进行线程切换时,下一个线程所需要的代码和数据很有可能已经被预读了,这样线程切换的成本就会更低!而进程具有独立性,进行进程切换时,CPU 的 Cache 缓存的代码和数据就会立即失效,需要将新进程的代码和数据重新加载到 Cache 中,所以进程切换的成本是更高的。

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

线程的优点

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

注:线程不是创建越多越好,因为线程切换也是有成本的,并不是不需要成本。创建线程太多了,线程切换的成本有可能就是最大的成本了。

线程的缺点

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

线程异常

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

线程用途

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

👉线程控制👈


clone 函数可以创建线程或者子进程,可以设置回调函数,子进程的栈区,还有各种属性等等。除了 clone 函数,还有一个 vfork 函数。vfork 函数创建出来的子进程是和父进程共享进程地址空间的。

#include <iostream>
#include <string>
#include <unistd.h>
#include <cassert>
#include <sys/types.h>
#include <sys/wait.h>

using namespace std;

int globalVal = 100;

int main()

    int id = vfork();
    // int id = fork();
    assert(id != -1);
    
    if(id == 0)
    
        // child process
        int count = 0;
        while(1)
        
            cout << "child process -> globalVal: " << globalVal << endl;
            sleep(1);
            ++count;
            if(count == 5)
            
                globalVal = 200;
                cout << "child process change globalVal!" << endl;
                exit(1);
            
        
    

    //waitpid(id, nullptr, 0); // 为了演示现象就不等待子进程了
    // parent process
    while(1)
    
        cout << "parent process -> globalVal: " << globalVal << endl;
        sleep(1);
    

    return 0;


线程创建

线程创建的函数在上面已经提过了,就不在赘述了。我们也已经知道通过 kill 命令来杀掉进程,其余线程也会跟着终止。那么现在,我们就来验证一下线程出现异常导致进程终止。

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

using namespace std;

void* threadRoutine(void* args)

    while(1)
    
        cout << "新线程: " << (char*)args << " running ..." << endl;
        sleep(1);
        int a = 100;
        a /= 0;
    


int main()

    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");

    while(1)
    
        cout << "主线程: running ..." << endl;
        sleep(1); 
    

    return 0;


结论:线程谁先运行与调度器相关。线程一旦异常都有可能导致整个进程整体退出!


线程终止

如果需要只终止某个线程而不终止整个进程,可以有三种方法:

  • 从线程函数 return。这种方法对主线程不适用,从main 函数 return 相当于调用 exit。
  • 线程可以调用 pthread_ exit 终止自己。
  • 一个线程可以调用 pthread_ cancel 终止同一进程中的另一个线程。

注:在多线程场景下,不要使用 exit 函数,exit 函数是终止整个进程的!

pthread_exit 函数

  • pthread_exit 函数的功能是终止线程。
  • retval:retval 不要指向一个局部变量。
  • 无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)。
#include <iostream>
#include <pthread.h>
#include <unistd.h>

using namespace std;

void* threadRoutine(void* args)

    int i = 0;
    while(1)
    
        cout << "新线程: " << (char*)args << " running ..." << endl;
        sleep(1);
        if(i++ == 3) break;
    
    cout << (char*)args << " quit" << endl;
    pthread_exit((void*)10);


int main()

    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");

    void* ret = nullptr;
    pthread_join(tid, &ret);
    cout << "ret: "<< (long long)ret << " main thread wait done... main quit too" << endl;

    return 0;


pthread_cancel 函数


pthread_cancel 函数的功能是取消一个执行中的线程。thread 是线程的 ID,调用成功是返回 0,失败是返回错误码。

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

using namespace std;

void* threadRoutine(void* args)

    int i = 0;
    while(1)
    
        cout << "新线程: " << (char*)args << " running ..." << endl;
        sleep(1);
    
    cout << (char*)args << " quit" << endl;
    pthread_exit((void*)13);


int main()

    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");
    // pthread_cancel(tid); // 不要一创建线程就取消它

    int count = 0;
    while(1)
    
        cout << "main线程 running ..." << endl;
        sleep(2);
        count++;
        if(count == 5) break;
    

    pthread_cancel(tid);
    cout << "pthread cancel tid: " << tid << endl;

    void* ret = nullptr;
    pthread_join(tid, &ret);
    cout << "ret: "<< (long long)ret << " main thread wait done... main quit too" << endl;

    return 0;


当一个线程被取消时,线程的退出结果是 -1(PTHREAD_CANCELED)。使用 pthread_cancel 函数的前提是线程已经跑起来了才能够取消,所以不要穿甲一个线程后就立马取消(可能刚创建的线程还没有跑起来)。一般情况下,都是用主线程来取消新线程的。如果使用新线程来取消主线程的话,这样会影响整个进程。

线程 ID 的深入理解

线程 ID 本质是一个地址!!!因为我们目前用的不是 Linux 自带的创建线程的接口,用的是 pthread 库中的接口!用户需要的是线程,而 Linux 系统只提供轻量级进程,无法完全表示线程,所以在用户和操作系统之间加了个软件层 pthread 库。操作系统承担轻量级进程的调度和内核数据结构的管理,而线程库要给用户提供线程相关的属性字段,包括线程 ID、栈的大小等等。

pthread_self 函数可以获取当前线程的 ID,既然能获得当前线程的 ID,那么线程就可以自己取消自己,但是这种方式不推荐!

线程局部存储:用 __thread 修饰全局变量带来的结果就是让每一个线程各自拥有一个全局变量,这就是线程的局部存储。

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

using namespace std;

__thread int g_val = 0;

void* threadRoutine(void* args)

    while(1)
    
        cout << (char*)args << "  g_val:" << g_val << "  &g_val:" << &g_val << endl;
        g_val++;
        sleep(1);
    
    return nullptr;


int main()

    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    while(1)
    
        cout << "main thread" << "  g_val:" << g_val << "  &g_val:" << &g_val << endl;
        sleep(2);
    

    pthread_join(tid, nullptr);

    return 0;
   


去掉 __thread 修饰后,所有线程看到的全局变量都是同一个!__thread 所有 pthread 库给 g++ 编译器的一个编译选项!

在多线程的场景下进行进程替换

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

using namespace std;

__thread int g_val = 0;

void* threadRoutine(void* args)

    sleep(5);
    execl("/bin/ls", "ls", "-l", nullptr);
    while(1)
    
        cout << (char*)args << "  g_val:" << g_val << "  &g_val:" << &g_val << endl;
        g_val++;
        sleep(1);
    
    return nullptr;


int main()

    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread 1");

    while(1)
    
        cout << "main thread" << "  g_val:" << g_val << "  &g_val:" << &g_val << endl;
        sleep(1);
    

    pthread_join(tid, nullptr);

    return 0;
   

在多线程的场景下执行进程替换,那么先会将除主线程外的其它线程都终止掉,然后再进行进程替换。


线程等待

线程在创建并执行的时候,线程也是需要被等待的。如果不等待线程的话,会引起类似于进程的僵尸问题,进而导致内存泄漏。已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。创建新的线程不会复用刚才退出线程的地址空间。


pthread_join 函数的功能是等待线程结束。thread
是要线程的 ID,retval 指向线程所执行的函数的返回值。调用该函数的线程将阻塞等待,直到 ID为 thread 的线程终止。thread 线程以不同的方法终止,通过 pthread_join 得到的终止状态是不同的,总结如下:

  • 如果 thread 线程通过 return 返回,retval 所指向的单元里存放的是 thread 线程函数的返回值。
  • 如果 thread 线程被别的线程调用 pthread_ cancel 异常终掉,retval 所指向的单元里存放的是常数
    PTHREAD_ CANCELED。
  • 如果 thread 线程是自己调用 pthread_exit 终止的,retval 所指向的单元存放的是传给 pthread_exit 的参数。
  • 如果对 thread 线程的终止状态不感兴趣,可以传 nullptr 给 retval 参数。
  • thread 线程函数的返回值不会考虑异常的情况,如果线程出现了异常,那么整个进程都会崩掉。注:状态寄存器是所有线程共享的。

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

using namespace std;

void* threadRoutine(void* args)

    int i = 0;
    while(1)
    
        cout << "新线程: " << (char*)args << " running ..." << endl;
        sleep(1);
        if(i++ == 6) break;
    
    cout << (char*)args << " quit" << endl;
    return nullptr;


int main()

    pthread_t tid;
    pthread_create(&tid, nullptr, threadRoutine, (void*)"thread one");

    pthread_join(tid, nullptr);	// 默认会阻塞等待
    cout << "main thread wait done... main quit too" << endl;

    return 0;

线程执行的函数的返回值是返回给主线程的,主线程通过该返回值来获取线程退出的状态。

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

using namespace std;

void* threadRoutine(void* args)

    int i = 0;
    while(1)
    
        cout << "新线程: " << (char*)args << " running ..." << endl;
        sleep(1);
        if(i++ == 6) break;
    
    cout << (char*)args 🌏Linux下的线程
  • 🌏Linux下的进程和线程
  • 🌏Linux线程控制
  • 🌐总结

  • 🌏Linux下的线程

    🌲线程的概念

    线程: 线程是OS能够进行运算调度的基本单位。线程是一个进程中的一个单一执行流,通俗地说,一个程序里的一个执行路线就叫做线程。

    可以知道的是,一个进程至少有一个执行线程,这个线程就是主执行流。一个进程的多个执行流是共享进程地址空间内的资源,也就是说进程的资源被合理分配给了每一个执行流,这些样就形成了线程执行流。所以说线程在进程内部运行,本质是在进程地址空间内运行。
    需要注意的是,Linux下没有真正意义上的线程,线程是通过进程来模拟实现的。这句话如何理解?

    Linux系统下,没有专门为线程设计相关的数据结构。那线程又是如何被创建的呢?我们知道,创建一个进程,我们需要为它创建相关的数据结构,如:PCB(task_struct)、mm_sturct、页表和file_struct等。线程的创建和进程的创建是一样的,线程也是创建一个一个的PCB,因为线程是共享进程地址空间的,所以这些线程都维护同一个进程地址空间。

    这样可以看出一个线程就是一个执行流,每一个线程有一个task_struct的结构体,和进程一样,这些task_struct都是由OS进行调度。可以看出在CPU看来,进程和线程是没有区别的,所以说Linux下的线程是通过进程模拟实现的。

    继续思考,CPU如何区分Linux下的线程和进程?

    其实CPU不需要考虑这个问题,在它眼中,进程和线程是没有区别的,都是一个一个的task_struct,CPU只管负责调度即可。

    那如何理解我们之前所学的进程?

    我们都知道,进程是承担分配系统资源的基本实体,曾经CPU看到的PCB是一个完整的进程,也就是只有一个执行流的进程。现在看到的PCB不一定是完整的进程,可能是一个进程的执行流总的一个分支,也就是多执行流进程。所以说,现在CPU眼中,看到的PCB比传统的进程更加轻量化了。这种有多执行流的进程中的每一个执行流都可以看作是一个轻量级进程。总结地说,线程是轻量级进程

    🌲线程的优点和缺点

    优点:

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

    缺点:

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

    🌲线程异常

    • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
      进程之间是具有很强的独立性的,但是线程之前并不具有很强的独立性。一个线程发生异常(进程发生局部异常)时,OS会将异常解释为信号,以进程为单位发生信号终止进程,这样其他线程也随之受到影响,所以一个线程异常会影响整个进程
    • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出

    🌲线程用途

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

    🌏Linux下的进程和线程

    进程: 承担分配系统资源的实体
    线程: CPU调度的基本单位
    注意: 进程之间具有很强的独立性,但是线程之间是会互相影响的
    线程共享一部分进程数据,也有自己独有的一部分数据:

    • 线程ID
    • 一组寄存器(记录上下文信息,任务状态段)
    • 栈(线程私有栈)
    • errno(错误码)
    • 信号屏蔽字
    • 调度优先级

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

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

    关系图:

    🌏Linux线程控制

    🌲POSIX线程库

    • POSIX线程(英语:POSIX Threads,常被缩写为Pthreads)是POSIX的线程标准,定义了创建和操纵线程的一套API。
    • 与线程有关的函数构成了一个完整的系列,绝大多数的名字都是以“pthread_”打头的
    • 使用线程库需要映入头文件pthread.h,链接这些线程函数是,需要指明线程库名,所以编译时要加上选项-lpthread

    注意: Linux内核没有提供线程管理的库函数,这里的线程库是用户提供的线程管理功能

    错误检查:

    • 传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
    • pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做,不然这个全局变量就成为临界资源了)。而是将错误代码通过返回值返回
    • pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值判定,因为读取返回值要比读取线程内的errno变量的开销更小

    🌲线程创建

    函数名称: 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;失败返回错误码

    再介绍一个函数:

    实例演示:
    实例1: 创建一个线程,观察代码运行效果和函数用法

    函数名称: pthread_self
    功能: 获取线程自身ID
    函数原型:

     pthread_t pthread_self(void);
    

    返回值: 线程自身ID

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    
    void* pthreadrun(void* arg)
    
    	char* name = (char*)arg;
    	while (1)
    		printf("%s is running...\\n", name);
    		sleep(1);
    	
    
    
    int main()
    
    	pthread_t pthread;
    	// 创建新线程
    	pthread_create(&pthread, NULL, pthreadrun, (void*)"new thread");
    	
    	while (1)
    		printf("main thread is running...\\n");
    		sleep(1);
    	
    	return 0;
    
    

    代码运行结果如下:

    实例2: 创建4个线程,然后打印出各自的pid和线程id

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    
    void* pthreadrun(void* arg)
    
      long id = (long)arg;
      while (1)
        printf("threaad %ld is running, pid is %d, thread id is %p\\n", id, getpid(), pthread_self());
        sleep(1);
      
    
    
    int main()
    
      pthread_t pthread[5];
      int i = 0;
      for (; i < 5; ++i)
      
        // 创建新线程
        pthread_create(pthread+i, NULL, pthreadrun, (void*)i);
      
    
      while (1)
        printf("main thread is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
        sleep(1);
      
      return 0;
    
    

    代码运行结果如下: 可以看到的是,线程的pid是一样的,但是线程id却是不一样的

    代码运行时,我们使用命令ps -aL 查看轻量级进程:

    可以看到,这六个线程的PID是一样的,同属一个进程,但是它们还有一个表示,LWP(light wighted process),轻量级进程的ID。下面详细介绍

    🌲进程ID和线程ID

    • 在Linux下,线程是由Native POSIX Thread Library 实现的,在这种实现下,线程又被称为轻量级进程(LWP)。在用户态的每个进程,内核中都有一个与之对应的调度实体(拥有自己的task_struct结构体)。
    • 在没有线程之前,一个进程对应内核里的一个进程描述符,对应一个进程ID。引入线程概念之后,一个用户进程下管理多个用户态线程,每个线程作为一个独立的调度实体,在内核中都有自己的进程描述符。进程和内核的描述符变成了1:N的关系。
    • 多线程的进程,又被称为线程组。线程组内的每一个线程在内核中都有一个进程描述符与之对应。进程描述符结构体表面上看是进程的pid,其实它对应的是线程ID;进程描述符中的tpid,含义是线程组ID,该值对应的是用户层面的进程ID。
    struct task_struct 
    	...
    	pid_t pid;// 对应的是线程ID,就是我们看到的lwp
    	pid_t tgid;// 线程组ID,该值对应的是用户层面的进程ID
    	...
    	struct task_struct *group_leader;
    	...
    	struct list_head thread_group;
    	...
    ;
    
    • 具体关系如下:
    用户态系统调用内核进程描述符中对应的结构
    线程IDpid_t gettid(void)pid_t pid
    进程IDpid_d getpid(void)pid_t tgid

    注意: 这里的线程ID和创建线程得到的ID不是一回事,这里的线程ID是用来唯一标识线程的一个整形变量。

    如何查看线程ID?
    上面介绍过了,使用ps命令,带-L选项,可以查看到lwp,

    Linux提供了gettid系统调用来返回其线程ID,可是glibc并没有将该系统调用封装起来,在开放接口来供程序员使用。如果确实需要获得线程ID,可以采用如下方法:

    #include <sys/syscall.h> pid_t tid; tid = syscall(SYS_gettid);
    

    在前面的一张图片中(如下),我们可以发现的是,有一个线程的ID和进程ID是一样的,这个线程就是主线程。在内核中被称为group leader,内核在创建第一个线程时,会将线程组的ID的值设置成第一个线程的线程ID,group_leader指针则指向自身,既主线程的进程描述符。所以线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程。

    注意: 线程和进程不一样,进程有父进程的概念,但是在线程组中,所有的线程都是对等关系。

    🌲线程ID和进程地址空间布局

    上面说过了,pthread_create产生的线程ID和gettid获得的id不是一回事。后缀属于进程调度范畴,用来标识轻量级进程。前者的线程id是一个地址,指向的是一个虚拟内存单元,这个地址就是线程的ID。属于线程库的范畴,线程库后序对线程操作使用的就是这个ID。
    对于目前实现的NPTL而言,pthread_t的类型是线程ID,本质是进程地址空间的一个地址:

    这里的每一个线程ID都代表的是每一个线程控制块的起始地址。这些线程控制块都是struct pthread类型的,所以所有的线程可以看成是一个大的数组,被描述组织起来。

    🌲线程终止

    如果值想终止某个线程而不是整个进程,有三种方式:

    1. 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
    2. 线程可以调用pthread_ exit终止自己。
    3. 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。

    return返回退出某个线程:

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    
    void* pthreadrun(void* arg)
    
      int count = 0;
      while (1)
        printf(" new threaad is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
        sleep(1);
        if (count++ == 5)
          return (void*)10;
        
      
    
    
    int main()
    
      pthread_t thread;
      pthread_create(&thread, NULL, pthreadrun, NULL);
    
      while (1)
        printf("main thread is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
        sleep(1);
      
      return 0;
    
    

    代码运行结果如下: 代码运行6s后,新线程退出了

    pthread_exit函数:

    功能: 线程终止
    函数原型:

     void pthread_exit(void *retval); 
    

    参数:

    • retval:不能指向局部变量

    实例演示:

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    
    void* pthreadrun(void* arg)
    
      int count = 0;
      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 thread;
      pthread_create(&thread, NULL, pthreadrun, NULL);
    
      while (1)
        printf("main thread is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
        sleep(1);
      
      return 0;
    
    

    代码运行结果如下:

    注意: 需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配

    pthread_cancel函数:

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

     int pthread_cancel(pthread_t thread);
    

    参数:

    • thread:线程ID

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

    实例演示:

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    
    void* pthreadrun(void* arg)
    
      int count = 0;
      while (1)
        printf(" new threaad is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
        sleep(1);
      
    
    
    int main()
    
      pthread_t thread;
      pthread_create(&thread, NULL, pthreadrun, NULL);
      int count = 0;
      while (1)
        printf("main thread is running, pid is %d, thread id is %p\\n", getpid(), pthread_self());
        sleep(1);
        if (++count == 3)
          pthread_cancel(thread);
          printf("new thread is canceled...\\n");
        
      
      return 0;
    
    

    代码运行结果如下:

    🌲线程等待

    线程等待的原因:

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

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

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

    参数:

    • thread:线程ID
    • retval:输出型参数,指向线程退出的返回值

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

    实例演示:

    #include <stdio.h>
    #include <pthread.h>
    #include <unistd.h>
    
    long retval = 10;
    
    void* pthreadrun(void* arg)
    
      int count = 0;
      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((void*)retval);
        
      
    
    
    int main()
    
      pthread_t thread;
      pthread_create(&thread, NULL, pthreadrun, NULL);
      
      printf("main thread is waiting new thread\\n");
      void* ret = NULL;
      pthread_join(thread, &ret);
      printf("new thread has exited, exit code is %ld\\n", (long)ret);
      return 0;
    
    

    代码运行结果如下:

    总结:

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

    🌲线程分离

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

    pthread_detach函数:

    功能: 对一个线程进行分离
    函数原型:

     int pthread_detach(pthread_t thread);
    

    参数:

    • thread:线程ID

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

    实例演示:

    #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;
    
    

    代码运行结果如下:

    🌐总结

    以上就是线程控制的全部内容。喜欢的话,欢迎点赞、收藏和关注支持~

    以上是关于Linux线程概念 | 线程控制的主要内容,如果未能解决你的问题,请参考以下文章

    Linux线程概念 | 线程控制

    Linux-线程概念-线程控制-进程和线程对比-线程创建

    Linux:详解多线程(线程概念线程控制—线程创建线程终止线程等待)

    Linux___线程概念及线程控制

    Linux多线程

    Linux 多线程