【Linux多线程】线程控制与分离线程(POSIX库的理解与相关函数的使用)

发布于:2024-08-21 ⋅ 阅读:(138) ⋅ 点赞:(0)

0. 前言

对于Linux多线程,本文介绍POSIX库以及相关接口函数的使用,在不同的部分有相应的接口使用,以及分离线程的意义和接口函数。

1. POSIX线程库

POSIX线程库( pthreads) 提供了一套用于多线程编程的接口。它包括线程创建、管理、同步等功能。

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

下面将介绍PISIX线程库中的相关接口函数:


1.1 创建线程

pthread_create()

  • 功能:创建一个新的线程
  • 原型
    cpp 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 <stdio.h>
#include <pthread.h>

void *thread_function(void *arg) {
    printf("Hello from the thread! Argument: %d\n", *(int *)arg);
    return NULL;
}

int main() {
    pthread_t thread;
    int arg = 42;

    // 创建新线程
    if (pthread_create(&thread, NULL, thread_function, &arg) != 0) {
        perror("pthread_create failed");
        return 1;
    }

    // 等待线程结束
    pthread_join(thread, NULL);

    return 0;
}


1.2 进程ID和线程ID

  • Linux目前的线程实现是 Native POSIX Thread Libaray(NPTL)
  • 在这种实现下,线程又被称为轻量级进程(Light Weighted Process), 每一个用户态的线程,在内核中都对应一个调度实体,也拥有自己的进程描述符(task_struct结构体)
    • 线程存在前,一个进程对应内核里的一个进程描述符,对应一个进程ID。
    • 引入线程后,一个用户进程下管理N个用户态线程,每个线程作为独立的调度实体在内核态都有自身的进程描述符,进程和内核的描述符变成了1:N关系,
  • POSIX标准又要求进程内的所有线程调用getpid函数时返回相同的进程ID,如何解决上述问题呢? —— 线程组
struct task_struct {
...
pid_t pid;
pid_t tgid;
...
struct task_struct *group_leader;
...
struct list_head thread_group;
...
};

① 线程组

  • 多线程的进程,又被称为线程组;
  • 线程组内的每个线程在内核中都存在一个进程描述符(task_struct)与之对应。进程描述符结构体中的pid,表面上看对应的是进程ID,其实对应的是线程ID;进程描述符中的tgid(Thread Group ID),该值对应的是用户层面的进程ID

在这里插入图片描述

和进程ID一样,线程ID是pid_t类型的变量,而且是用来唯一标识线程的一个整型变量。

如何查看一个线程的ID?

  1. 使用 ps 命令

ps 命令可以列出系统中的进程和线程。使用 ps 命令加上适当的选项可以显示线程ID。

ps -eLf
  • -e:显示所有进程
  • -L:显示线程
  • -f:显示完整格式

输出中的 LWP 列显示线程ID,PID 列显示进程ID。

如下图:

  PID   LWP TTY      STAT   TIME COMMAND
  1000  1000 ?        Ss     0:00 /sbin/init
  1000  1001 ?        S      0:00  \_ /sbin/init
  1000  1002 ?        S      0:00  \_ /sbin/init
  1010  1010 ?        Ss     0:00 /usr/sbin/sshd
  1010  1011 ?        S      0:00  \_ /usr/sbin/sshd
  1020  1020 ?        Ss     0:00 /usr/bin/python3
  1020  1021 ?        S      0:00  \_ /usr/bin/python3
  1020  1022 ?        S      0:00  \_ /usr/bin/python3

  1. 函数调用

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

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

线程组内的第一个线程,在用户态被称为主线程(main thread); 在内核中被称为group leader,内核在创建第一个线程时,会将线程组的ID的值设置成第一个线程的线程ID,group_leader指针则指向自身,既主线程的进程描述符。

线程组内存在一个线程ID等于进程ID,而该线程即为线程组的主线程。

/* 线程组ID等于线程ID,group_leader指向自身 */
p->tgid = p->pid;
p->group_leader = p;
INIT_LIST_HEAD(&p->thread_group);

线程组内的第一个线程,在用户态被称为主线程(main thread),其线程组ID总是和主线程的线程组ID一致,无论是主线程直接创建线程,还是创建出来的线程再次创建线程,都如此。

if ( clone_flags & CLONE_THREAD )
p->tgid = current->tgid;
if ( clone_flags & CLONE_THREAD ) {
P->group_lead = current->group_leader;
list_add_tail_rcu(&p->thread_group, &p->group_leader->thread_group);
}

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

在这里插入图片描述


1.3 线程ID 与 进程地址空间布局

  • pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中;该线程ID和上文的id不同。
  • 上文的线程ID属于进程调度的范畴。由于线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
  • pthread_ create函数第一个参数指向一个虚拟内存单元
    • 该内存单元的地址为新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后续操作就是根据该线程ID来操作的。
  • 线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID

pthread_t 的类型取决于其实现。根据NPTL实现, pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址

在这里插入图片描述


1.4 线程终止

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

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

下面简单介绍这两个函数:

pthread_exit()

  • 功能:线程终止
  • 原型
    cpp void pthread_exit(void *value_ptr);
  • 参数
    • value_ptr : value_ptr不要指向一个局部变量。
  • 返回值:无返回值,跟进程一样,线程结束的时候无法返回到它的调用者(自身)

注意:pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了

pthread_cancel()

功能:取消一个执行中的线程

  • 原型
    cpp int pthread_cancel(pthread_t thread);
  • 参数
    • thread:线程ID
  • 返回值:成功返回0;失败返回错误码

1.5 线程等待 为什么需要线程等待

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

pthread_join()

  • 功能:等待线程结束
  • 原型
    cpp 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参数

在这里插入图片描述

#include <iostream>
#include <pthread.h>
#include <unistd.h> // For sleep function

// 线程函数
void* threadFunction(void* arg) {
    int* threadId = static_cast<int*>(arg);
    std::cout << "Thread " << *threadId << " is running." << std::endl;
    sleep(2); // 模拟长时间运行的任务
    std::cout << "Thread " << *threadId << " is done." << std::endl;
    return nullptr;
}

int main() {
    const int numThreads = 3;
    pthread_t threads[numThreads];
    int threadIds[numThreads];

    // 创建线程
    for (int i = 0; i < numThreads; ++i) {
        threadIds[i] = i + 1;
        int ret = pthread_create(&threads[i], nullptr, threadFunction, &threadIds[i]);
        if (ret != 0) {
            std::cerr << "Error creating thread " << i << ": " << ret << std::endl;
            return 1;
        }
    }

    // 等待所有线程完成
    for (int i = 0; i < numThreads; ++i) {
        int ret = pthread_join(threads[i], nullptr);
        if (ret != 0) {
            std::cerr << "Error joining thread " << i << ": " << ret << std::endl;
            return 1;
        }
    }

    std::cout << "All threads have finished." << std::endl;
    return 0;
}

执行上面的代码,会有如下结果:

Thread 1 is running.
Thread 2 is running.
Thread 3 is running.
Thread 1 is done.
Thread 2 is done.
Thread 3 is done.
All threads have finished.

2. 分离线程 pthread_detach

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

pthread_detach

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

int pthread_detach(pthread_self());
int pthread_detach(pthread_t thread);

joinable和分离是冲突的,一个线程不能既是joinable又是分离的

示例代码:

#include <iostream>
#include <pthread.h>
#include <unistd.h> // For sleep function

// 线程函数
void* threadFunction(void* arg) {
    int* threadId = static_cast<int*>(arg);
    std::cout << "Thread " << *threadId << " is running." << std::endl;
    sleep(2); // 模拟长时间运行的任务
    std::cout << "Thread " << *threadId << " is done." << std::endl;
    return nullptr;
}

int main() {
    const int numThreads = 2;
    pthread_t threads[numThreads];
    int threadIds[numThreads];

    // 创建线程
    for (int i = 0; i < numThreads; ++i) {
        threadIds[i] = i + 1;
        int ret = pthread_create(&threads[i], nullptr, threadFunction, &threadIds[i]);
        if (ret != 0) {
            std::cerr << "Error creating thread " << i << ": " << ret << std::endl;
            return 1;
        }

        // 分别使线程在完成后自动清理资源
        if (i % 2 == 0) { // 对偶数线程调用 pthread_detach
            pthread_detach(threads[i]);
            std::cout << "Thread " << threadIds[i] << " is detached." << std::endl;
        }
    }

    // 等待其中一个线程完成
    int ret = pthread_join(threads[1], nullptr); // 只等待线程 2 完成
    if (ret != 0) {
        std::cerr << "Error joining thread 1: " << ret << std::endl;
        return 1;
    }

    std::cout << "Main thread has joined thread 2." << std::endl;

    // 等待一段时间以确保所有线程输出完成
    sleep(3);
    std::cout << "Main thread is done." << std::endl;

    return 0;
}

说明:

  1. 线程函数 (threadFunction):

    • 打印线程的开始和结束信息,并使用 sleep 模拟长时间运行的任务。
  2. 主函数 (main):

    • 创建两个线程。
    • 对偶数线程调用 pthread_detach,使这些线程在完成后自动清理资源。
    • 只对第二个线程调用 pthread_join,以等待其完成。
    • 程序在主线程结束前等待一段时间,确保所有线程的输出都能被看到。

运行结果:

Thread 1 is running.
Thread 2 is running.
Thread 1 is detached.
Thread 2 is done.
Main thread has joined thread 2.
Thread 1 is done.
Main thread is done.

对于这段代码,线程 1 被设置为分离状态(detached),因此主线程不需要调用 pthread_join 来等待它完成。主线程只等待线程 2 完成。线程 1 和线程 2 的输出顺序可能会有所不同,具体取决于调度。


网站公告

今日签到

点亮在社区的每一天
去签到