【Linux】进程概念与控制(2)_进程控制

hello~ 很高兴见到大家! 这次带来的是Linux系统中关于线程这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页 : 默|笙


文章目录


一、进程控制

1.1 POSIX 线程库

  1. 与POSIX 线程有关的所有函数构成了一个完整的系列,它们绝大部分都以 pthread_ 开头。

POSIX 是一套操作系统接口标准,让不同类 UNIX 系统(Linux、macOS、BSD 等)都能用同一套函数、同一套 API (应用程序编程接口)写代码。

  1. 要使用这些函数,需要包含头文件 <pthread.h>
  2. 编译链接这些线程函数库时,需要在编译器命令中添加 -lpthread 选项(现在推荐 -pthread选项,会提供全套线程支持不只有线程库)。因为该线程库并非标准 C 语言库,部分旧版编译器无法自动识别此库,而新版编译器已原生支持。

1.2 线程创建

pthread_create函数

  1. 创建线程需要用到函数 pthread_create,它的第一个参数需要传递一个用来存储线程 ID 变量的地址;第二个参数用于指定线程属性,这里暂不研究;第三个参数是一个回调函数,新线程创建成功后会立即执行该函数。这个函数的返回值为 void* 类型,参数也是 void* 类型,void* 表示通用指针,可以接收任意类型指针,使用时进行强制类型转换即可;第四个参数是一个指针,会作为参数传递给第三个参数对应的线程函数
  2. pthread_t 就是 Linux 下用来表示线程 ID 的一个数据类型,相当于线程的 "身份证号"。第一个参数的类型就得是 pthread_t。就跟进程 ID 类型是 pid_t 一样。
  3. pthread 系列函数的返回值规则统一:执行成功返回 0,执行失败则返回对应的非 0 错误码,不会设置 errno。
cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>

void* Routine(void* args)
{
    std::string name = static_cast<const char*>(args);
    while (true)
    {
        printf("new thread running..., pid : %d\n", getpid());
        sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, Routine, (void*)"thread-new");

    while (true)
    {
        printf("main thread running..., pid : %d, tid : 0x%lx\n", getpid(), tid);
        sleep(1);
    }

    return 0;
}
  1. 通过上面的测试代码可以看到,主线程和从线程的 pid 完全相同,说明它们同属于一个进程。同时,我们也成功将 pthread_create 的第四个参数传递给了线程函数 Routine 的形参,进一步印证了该函数的功能。5. 但这里也出现了一个新问题:为什么 pthread_t 类型的 tid 数值很大,和我们常见的 LWP 完全不同?LWP 是内核层面的轻量级进程,而 tid 是用户层封装后的标识,就像我们使用的 FILE 结构体内部封装了文件描述符一样。这个问题我们将留到下一篇博客中详细解答。
  2. 给从线程所要执行的函数传参时,pthread_create 支持通过 void 通用指针传递任意类型的数据,包括基本数据类型、自定义结构体,甚至是自定义类对象 / 类指针*。我们可以将自定义类的实例(或指针)传递给线程,让从线程基于这个类对象去执行对应的成员方法、处理专属任务。

线程资源共享的体现

cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
//#include <cstdio>

void *Routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        printf("new thread running..., name: %s\n", name.c_str());
        sleep(1);
    }
}
int main()
{
    const int num = 10;
    for (int i = 0; i < 10; i++)
    {
        pthread_t tid;
        char threadname[64];
        snprintf(threadname, sizeof threadname, "thread-%d", i + 1);
        int n = pthread_create(&tid, nullptr, Routine, (void *)threadname);
        (void)n;
    }

    while (true)
    {
        printf("main thread running...\n");
        sleep(1);
    }

    return 0;
}
  1. 这段代码,我本来是想要创建10个线程,并传递给它们不同的参数用来标识线程,然后让它们都去执行对应的Routine函数,可是为什么程序执行之后却只能看到线程4和线程10呢?线程1、2、3等等线程怎么看不到?这本质上就是线程之间的资源共享导致的问题:同一进程内的所有线程共享该进程的虚拟地址空间,主线程栈上的 threadname 数组对所有新线程都是可见的。我创建线程时传递的不是线程名的拷贝,而只是这个数组的地址,在线程 1、2、3 还没来得及执行对应的 Routine 函数时,主线程就已经快速跑完多轮循环,反复往同一个 threadname 里写入新内容,把之前的线程名覆盖掉了。等到这些线程真正执行函数去读取数据时,threadname 里的值已经被主线程改成了后面的内容(比如 thread-4、thread-10),所以前面的线程号就看不到了。
  2. 要解决这个问题,就必须给每个线程申请专属的独立内存空间,比如用 new 为每个线程动态分配一块专属的字符串缓冲区,避免所有线程共用主线程循环中被反复覆盖的栈数组。同时,主线程循环里的 pthread_t tid 变量也会被不断覆盖,导致无法正确保存所有线程 ID。因此我们可以使用 vector 数组来统一存储每一个线程 ID,保证每个 ID 都被独立保存下来。通过给每个线程分配专属动态内存 + 用容器保存所有线程 ID,就能彻底解决因为线程间资源共享、内存复用与覆盖而导致的参数错误问题。
cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <vector>
#include <unistd.h>
//#include <cstdio>

void *Routine(void *args)
{
    std::string name = static_cast<const char *>(args);
    while (true)
    {
        printf("new thread running..., name: %s\n", name.c_str());
        sleep(1);
    }
}
int main()
{
    const int num = 10;
    std::vector<pthread_t> tids;
    for (int i = 0; i < 10; i++)
    {
        pthread_t tid;
        //char threadname[64];
        //snprintf(threadname, sizeof threadname, "thread-%d", i + 1);
        char* threadname = new char[64];
        sprintf(threadname, "thread-%d", i + 1);
        int n = pthread_create(&tid, nullptr, Routine, (void *)threadname);
        (void)n;
        tids.push_back(tid);
    }

    for (auto& tid : tids)
        printf("tid: 0x%lx\n", tid);

    while (true)
    {
        printf("main thread running...\n");
        sleep(1);
    }

    return 0;
}
  1. 但是还是会存在一个问题,也就是多个线程并发执行同一函数时的线程安全问题。多个线程同时进入并执行同一段函数代码,如果函数内部访问了全局变量、静态变量等共享资源,而又没有做同步保护,线程调度的随机性就会让对共享数据的读写操作被打断、穿插执行,最终引发数据不一致、逻辑错乱等问题

1.3 线程终止

  1. 如果只需要终止某个线程不终止整个进程,有以下三种方法:
  1. 线程执行完毕后正常 return,该方法不适用于主线程------ 在 main 函数中 return 会直接终止整个进程。
  2. 线程调用 pthread_exit 终止自己,该方法适用于所有线程(包括主线程):主线程调用 pthread_exit 只会退出自身,不会终止进程,其他线程仍可继续运行。
  3. 任意线程可以调用 pthread_cancel 向同一进程内的其他任意线程(含主线程)发送取消请求来终止线程。注意:该操作仅终止目标线程本身,不会导致整个进程终止(只要进程内还有其他线程存活)。
  1. pthread_exit 的用法其实跟 return 非常像,可以就把它的参数理解为 return 返回的那个值。在子线程里面两个方法等价,在主线程里面 return 会终止整个进程,而 pthread_exit 不会终止整个进程,只是会终止主线程。
  1. 对于 pthread_cancel 函数,只需要传入目标线程的 ID,就能向其发送取消请求以终止线程。但在实际开发中,一般不会直接使用 pthread_cancel 来终止线程。
  2. 从线程不能够使用 exit 函数,因为 exit 函数是用来终止整个进程而不是单个线程的。
  3. 任意一个线程出现异常,会导致整个进程挂掉。

1.4 线程等待

  1. 线程函数的返回值需要由等待它的线程,使用线程等待函数 pthread_join 来获取。
    pthread_join 的第一个参数需要传入目标线程的线程 ID,第二个参数是一个输出型参数,
    由于线程返回值是 void * 类型,想要接收这个返回值,就需要传入一个 void* 变量的地址也就是上面看到的void**类型,让函数通过这个地址来修改传入的变量的值,从而拿到线程的返回值
  2. 子线程必须被主线程调用 pthread_join 等待,因为子线程执行完毕后会进入类似进程中僵尸进程的状态,占用内核资源,需要由主线程来回收。而通过 pthread_join 拿回线程返回值只是次要目的
    以下是一段测试代码:
cpp 复制代码
#include <iostream>
#include <pthread.h>
#include <unistd.h>

void* Routine(void* args)
{
    std::string name = static_cast<const char*>(args);
    printf("new thread running, name: %s, tid: 0x%lx, 即将退出\n", name.c_str(), pthread_self());
    //pthread_exit((void*)12);
    return (void*)12;
}
int main()
{
    pthread_t tid;
    pthread_create(&tid, nullptr, Routine, (void*)"thread-new");
    printf("新线程的id: 0x%lx\n", tid);
    void* retal = nullptr;
    pthread_join(tid, &retal);
    while (true)
    {
        std::cout << "返回值:" << (long long)retal << std::endl;
        printf("main thread running..., tid: 0x%lx\n", pthread_self());
        sleep(2);
    }
    return 0;
}


  1. 能够看到我们成功用 pthread_join 函数拿到了从线程执行的 routine 函数的返回值。这里再介绍一个函数 pthread_self,它类似进程中的 getpid,可以拿到当前调用它的线程自身的 TID
  2. 主线程通过 pthread_join 等待子线程的方式为阻塞等待。调用后主线程会一直阻塞挂起,直到目标子线程退出,才会继续向下执行。
  3. 为什么 pthread_join 函数不像 waitpid 函数一样,可以获取子线程的退出信号与退出状态?它不需要判断子线程是否发生异常吗?这是因为:同一进程内的线程共享地址空间,不具备独立性。一旦某个子线程出现异常(比如段错误、除零错误),整个进程都会直接被操作系统终止,主线程根本来不及执行 pthread_join 去等待。而 waitpid 能够获取子进程的退出信息,是因为进程之间相互独立,子进程异常只会导致自身退出,不会影响父进程,父进程才有机会通过 waitpid 收集退出状态。它只用关心正常情况。

1.5 分离线程

  1. 默认情况下,新创建的子线程是 joinable 的,该线程退出后,主线程需要对其进行等待(pthread_join)回收,否则资源无法释放,会造成内存泄漏。
  2. 但如果不关心子线程的返回值,那么 join 就是一种负担,这个时候我们就可以高速系统,该子线程执行完毕之后自动释放资源就好。也就是让线程被分离。
  1. 线程库中有一个函数 pthread_detach,专门用于将线程设置为分离状态。分离状态(detached)与可等待状态(joinable) 是线程的两种互斥状态,同一线程只能处于其中一种。使用时只用传入目标线程的 tid 就行。

  2. 主线程也可以被分离,但没什么用。


今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!

相关推荐
csdn2015_2 小时前
springboot controller 参数非必填
java·spring boot·后端
独断万古他化2 小时前
抽奖系统性能负载测试:基于 JMeter 的梯度加压与本地缓存优化全流程
java·redis·jmeter·缓存·压力测试·测试·负载测试
云烟成雨TD2 小时前
Spring AI 1.x 系列【22】深度拆解 ToolCallbackProvider 生命周期与调用链路
java·人工智能·spring
RNEA ESIO2 小时前
Spring Boot应用关闭分析
java·spring boot·后端
代码AC不AC2 小时前
【Linux】关于 mmap 文件映射
linux·mmap 文件映射
me8322 小时前
【Linux】解决Docker-Compose拉取Jenkins时失败问题。
linux·docker·jenkins
Ashore11_2 小时前
用户中心项目—需求分析
java
johnrui2 小时前
springboot接口限流操作
java·spring boot·后端
kaoa0002 小时前
Linux入门攻坚——73、运维OS Provisioning阶段工具之PXE、Cobbler
linux·运维