Linux中的线程

Linux中线程的概念和控制

概念

  • 线程是进程的执行的,线程是在进程的地址空间内运行的。线程的执行粒度比进程更细。

  • 任何执行流执行,都要有资源。线程就是利用了进程的进程地址空间资源。并且线程中的大部分资源都是共享的

  • 线程是操作系统调度的基本单位。而进程是承担操作系统分配资源的基本实体。

    • 进程包含了线程,用来描述管理线程的结构是线程控制块(TCB,Thread Control Block)。在Linux中,线程和进程的管理都通过任务结构(task_struct)来实现,因此,Linux的线程和进程使用同样的机制进行管理。

    • 由于这种设计,Linux没有单独的TCB结构,而是复用了PCB结构来管理线程。因此,虽然Linux有线程的概念,但线程和进程在内核层面并没有本质区别。

线程的独立结构

  • 栈空间:每个线程都有自己的栈,用于保存函数调用信息、本地变量等。栈是线程私有的。

  • 线程局部存储(Thread Local Storage, TLS):每个线程可以有自己的线程局部存储区,用于存储线程特有的数据。

  • 线程 ID(TID):每个线程有自己唯一的线程标识符。

线程的共享部分

  • 进程的虚拟地址空间

    • 所有线程共享同一个进程的虚拟地址空间,因此它们可以访问相同的全局变量、堆内存和已映射的文件。线程之间可以通过共享内存区进行数据交换。
  • 全局变量和静态变量

    • 线程可以共享进程中的全局变量和静态变量。这意味着一个线程修改了全局或静态变量,其他线程也会看到相同的变化,也因此可能要设置锁。
  • 堆内存

    • 进程的所有线程都共享堆内存。这意味着一个线程分配的动态内存(通过 mallocnew 等)可以被其他线程访问。
  • 文件描述符(File Descriptors)

    • 进程打开的文件描述符(包括套接字)在所有线程之间共享。一个线程可以读写另一个线程打开的文件或套接字。
  • 当前目录(Current Working Directory)

    • 所有线程共享进程的当前工作目录。这意味着如果一个线程更改了进程的当前目录(例如使用 chdir 函数),其他线程也会受到影响。
  • 信号处理器

    • 线程共享同一个信号处理器设置。如果进程中的一个线程接收到信号,所有线程都可能被中断,或者根据信号的类型和设置,某个特定线程会处理该信号。
  • 进程标识符(PID)

    • 虽然每个线程都有一个唯一的线程标识符(TID),但它们共享同一个进程标识符(PID)。
  • 用户 ID 和组 ID

    • 线程共享进程的用户 ID(UID)、有效用户 ID(EUID)和组 ID(GID)。这决定了线程对系统资源的访问权限。
  • 进程间通信(IPC)机制

    • 线程可以共享进程的 IPC 机制,如管道、消息队列和共享内存段。

共享数据的不一致问题

如果有若干个执行流对一个共享数据的修改操作没有结束就被切走。那么可能导致共享数据的不一致。

因此在对共享数据进行访问的时候,我们需要保证只有一个执行流访问,即对进程进行加锁

创建线程

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

  • 线程的创建对应的函数是pthread_create(),线程不是一个完全由内核实现的机制,它是由内核态和用户态合作完成的。

    • pthread_create()不是一个系统调用。
    • thread:返回线程ID。
    • attr:设置线程的属性,attr为NULL表示使用默认属性。
    • start_routine:是个函数地址,线程启动后要执行的函数。
    • arg:传给线程启动函数的参数,也可以传递类对象。
    • 返回值:成功返回0;失败返回错误码。

如何获取线程的返回值?

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

  • pthread_join 函数用于等待指定的线程终止,并获取其返回值。retval 是一个二级指针,其作用是存放线程返回值的指针。

  • 在线程函数中,可以通过 pthread_exit 或者直接返回一个指针类型的值来设置线程的返回值。在调用 pthread_join 时,retval 指向的内存会被更新为线程的返回值。例如,在系统调用内,*retval = return_val 的方式,将线程的返回值拷贝给 retval 指向的变量。

如何让线程执行不同的代码?

由于每个函数地址都不一样,那么只要让每个函数让不同的线程去跑,就已经实现了线程的分离。

线程的分离

线程分离是指在多线程编程中,主线程创建了一个子线程后,子线程与主线程脱离关系,成为独立的线程。

线程分离的作用:

  1. 避免资源泄露:通常,主线程在子线程完成时调用 pthread_join 来回收资源。如果不调用 pthread_join 而子线程结束了,线程的资源将保持未释放的状态,导致资源泄露。通过线程分离,子线程完成后会自动释放资源。

  2. 无需等待子线程完成:对于一些任务,主线程不关心子线程何时完成或完成状态,那么可以将子线程设置为分离状态,主线程继续执行其他工作,无需等待子线程完成。

1. 在创建线程时设置分离属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <pthread.h>

void* thread_function(void* arg) {
// 子线程执行的代码
return nullptr;
}

int main() {
pthread_t thread;
pthread_attr_t attr;

pthread_attr_init(&attr); // 初始化线程属性对象
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 设置线程分离状态

pthread_create(&thread, &attr, thread_function, nullptr); // 创建线程
pthread_attr_destroy(&attr); // 销毁线程属性对象

// 主线程不需要等待子线程完成
// pthread_join(thread, nullptr); // 不需要这个

return 0;
}

2. 在线程创建后,将其设置为分离状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <pthread.h>

void* thread_function(void* arg) {
// 子线程执行的代码
return nullptr;
}

int main() {
pthread_t thread;

pthread_create(&thread, nullptr, thread_function, nullptr); // 创建线程
pthread_detach(thread); // 设置线程为分离状态

// 主线程不需要等待子线程完成
// pthread_join(thread, nullptr); // 不需要这个

return 0;
}

注意事项:

  • 一旦线程被分离,主线程不能再对该线程使用 pthread_join,否则会导致错误。
  • 分离线程主要适用于那些不需要主线程关注其执行结果的子线程。

线程分离对于资源管理非常有用,尤其是在需要创建大量线程的应用中,避免了主线程对每个子线程的 pthread_join 调用。

线程的切换

线程的切换比起进程切换更加轻量。

Cache:CPU进行线程切换的时候,上下文会切换,但是Cache不会被重启缓存。只有在进程切换的时候,Cache数据才会被重新缓存。其中Cache中的数据叫做热数据

其中,刚启动的线程叫做主线程。

线程异常

  • 单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃-。

  • 线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。

线程退出

  • 线程执行完毕之后return val
  • 使用pthread_exit()

线程用途

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

clone和pthread线程库

Linux中自带的用于创建“轻量级进程”的接口clone()比较复杂且要求权限较高,于是我们使用被线程库pthread(动态库)封装过的接口,使用起来更加方便安全。

线程库需要维护多个线程的属性集合,因此线程库中具有多个线程控制块(tcb)

其中,每一个线程都需要有自己的栈结构,用来保证每个线程的执行流独立。因此,除了主线程,其它线程的独立栈,都在共享区,已载入的pthread库中

tcb和pthread_t

pthread_t

pthread_t 是 POSIX 线程库定义的线程标识符类型,其具体类型取决于实现。在 Linux 的 NPTL (Native POSIX Thread Library) 实现中,pthread_t 类型的线程 ID 通常为指针,对应于进程地址空间上的一个地址。这些实现细节对用户代码来说是透明的,用户只需要使用**pthread_t 类型的变量来表示和操作线程 ID**。

线程的局部存储

独立的栈结构,保证线程之间执行流独立。但实际上它们都还是处于同一个地址空间中的。
- 如果一个线程想要一个私有的全局变量。那么需要在声明全局变量的时候,需要使用__thread(eg.__thread int num = 5)。这种技术即叫线程的局部存储,但是只能用来修饰内置类型,不能用来修饰自定义类型。

线程函数中的void *

  • 线程中回调函数的返回值和参数类型为 void * 是为了提供最大的灵活性和通用性。

  • 参数类型为 void *

    1. 我们可以在创建线程时传递任何类型的数据指针。例如,可以传递一个指向整数、结构体或数组的指针。
    2. 类型转换( static_cast等方法):在线程函数内部,参数可以被转换回其原始类型并进行操作。这允许在线程函数中处理多种类型的数据。
  • 返回值类型为 void *

    1. void * 作为返回类型允许线程函数返回任意类型的结果。线程函数可以将结果存储在堆内存中,并返回一个指向该结果的指针。
    2. 线程回收:当线程结束时,调用 pthread_join 函数可以接收线程函数的返回值。使用 void ** 类型的指针作为 pthread_join 的第二个参数,可以接收并处理线程函数的返回值。

实验代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>

// 线程函数
void* thread_func(void* arg) {
int* num = (int*)arg;
int* result = (int*)malloc(sizeof(int));
*result = (*num) * 2; // 计算结果
pthread_exit((void*)result);
}

int main() {
pthread_t thread;
int arg = 21;
int* retval;

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

// 等待线程终止并获取返回值
if (pthread_join(thread, (void**)&retval) != 0) {
perror("pthread_join");
return 1;
}

printf("Thread returned: %d\n", *retval);
free(retval); // 释放分配的内存

return 0;
}

线程相关的函数

函数 原型 描述
pthread_create int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); 创建一个新的线程。
pthread_exit void pthread_exit(void *retval); 终止调用线程,并返回一个值。
pthread_join int pthread_join(pthread_t thread, void **retval); 等待一个线程终止,获取返回值(默认是阻塞等待的)。
pthread_detach int pthread_detach(pthread_t thread); 分离线程,使其在终止时自动释放资源。
pthread_self pthread_t pthread_self(void); 获取调用线程的线程ID。
pthread_equal int pthread_equal(pthread_t t1, pthread_t t2); 比较两个线程ID是否相等。
pthread_cancel int pthread_cancel(pthread_t thread); 向线程发送取消请求(返回值会被设置为-1)。
pthread_once int pthread_once(pthread_once_t *once_control, void (*init_routine)(void)); 确保某个初始化函数仅执行一次。
pthread_key_create int pthread_key_create(pthread_key_t *key, void (*destructor)(void*)); 创建线程特定数据的键。
pthread_key_delete int pthread_key_delete(pthread_key_t key); 删除线程特定数据的键。
pthread_setspecific int pthread_setspecific(pthread_key_t key, const void *value); 设置线程特定数据的值。
pthread_getspecific void *pthread_getspecific(pthread_key_t key); 获取线程特定数据的值。
pthread_mutex_init int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr); 初始化互斥量。
pthread_mutex_destroy int pthread_mutex_destroy(pthread_mutex_t *mutex); 销毁互斥量。
pthread_mutex_lock int pthread_mutex_lock(pthread_mutex_t *mutex); 加锁互斥量。
pthread_mutex_trylock int pthread_mutex_trylock(pthread_mutex_t *mutex); 尝试加锁互斥量(非阻塞)。
pthread_mutex_unlock int pthread_mutex_unlock(pthread_mutex_t *mutex); 解锁互斥量。
pthread_cond_init int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr); 初始化条件变量。
pthread_cond_destroy int pthread_cond_destroy(pthread_cond_t *cond); 销毁条件变量。
pthread_cond_wait int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); 等待条件变量。
pthread_cond_signal int pthread_cond_signal(pthread_cond_t *cond); 发信号通知一个等待条件变量的线程。
pthread_cond_broadcast int pthread_cond_broadcast(pthread_cond_t *cond); 发信号通知所有等待条件变量的线程。
pthread_attr_init int pthread_attr_init(pthread_attr_t *attr); 初始化线程属性对象。
pthread_attr_destroy int pthread_attr_destroy(pthread_attr_t *attr); 销毁线程属性对象。
pthread_attr_setdetachstate int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate); 设置线程分离状态属性。
pthread_attr_getdetachstate int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate); 获取线程分离状态属性。
pthread_attr_setschedpolicy int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy); 设置线程调度策略属性。
pthread_attr_getschedpolicy int pthread_attr_getschedpolicy(const pthread_attr_t *attr, int *policy); 获取线程调度策略属性。
pthread_attr_setschedparam int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param); 设置线程调度参数属性。
pthread_attr_getschedparam int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param); 获取线程调度参数属性。

Linux中的线程
https://weihehe.top/2024/07/25/线程/
作者
weihehe
发布于
2024年7月25日
许可协议