引言
在当今信息技术日新月异的时代,多线程编程已经成为了日常开发中不可或缺的一部分。Linux作为一种广泛应用的操作系统,其对多线程编程的支持也相当完善。本文将会介绍关于Linux多线程相关的知识,其中包括了线程的概念、线程控制、线程分离等方面的内容。如果你希望提升自己的多线程编程能力,本文将为你提供实用的技术指导和详尽的知识储备。让我们一起来深入了解Linux多线程编程的奥秘吧!
一、 Linux线程概念
1. 什么是线程
在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序列”。
一切进程至少都有一个执行线程。
线程在进程内部运行,本质是在进程地址空间内运行。
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程执行流。
2. 线程的概念
在Linux中,线程是指在同一个进程内部并发执行的多个子任务。Linux将线程作为轻量级进程(LWP)来实现,每个线程共享相同的进程地址空间和其他资源,包括文件描述符、信号处理器和其他内核状态。由于线程之间共享进程的资源,因此线程之间的切换开销通常比进程之间的切换要小得多。
Linux使用POSIX线程库(pthread)来支持多线程编程。通过pthread库,开发人员可以方便地创建、控制和同步线程,实现多线程编程的各种功能。在Linux中,线程的创建和管理都是通过系统调用和pthread库来完成的,开发人员可以使用pthread_create()函数创建新线程,并使用pthread_join()函数等来等待线程的结束,后面我们会详细介绍。
在Linux中,线程与进程一样拥有自己的ID、寄存器上下文、栈和线程本地存储(TLS)。线程可以通过共享内存进行通信,也可以使用线程同步机制来协调彼此的操作。在多核处理器上,Linux内核会将不同的线程分配到不同的处理器核心上并行执行,以提高系统的性能和响应速度。
总的来说,Linux线程是在同一个进程内并发执行的多个子任务,通过共享进程的资源和使用pthread库来实现线程的创建和管理。在Linux环境下,充分利用线程可以提高程序的并发能力和性能表现。
在这里插入图片描述
3. 线程与进程的区别
Linux线程和进程的主要区别在于它们是操作系统对应不同的执行单元。
资源分配
进程是资源分配的最小单位,每个进程都有自己的地址空间、全局变量、堆栈、文件描述符等系统资源。
线程是CPU调度的最小单位,多个线程可以共享同一个进程的资源,包括地址空间、全局变量、文件描述符等。
切换开销
因为线程共享进程资源,因此线程之间的切换开销比进程之间的切换要小得多。
线程的上下文切换只需要保存处理器寄存器和栈指针,而进程切换需要保存整个进程的上下文信息,包括内存映像、堆栈、寄存器等。
通信机制
进程之间通常使用IPC(Inter-Process Communication)机制来进行进程间通信,例如管道、消息队列、共享内存和信号量等。
线程之间可以通过共享内存、互斥锁、条件变量等同步机制来进行通信和协调。
并发能力
由于线程共享进程资源和较小的切换开销,因此线程可以更轻松地实现并发执行,提高程序的并发处理能力和性能表现。
安全性
线程共享进程资源,因此线程之间操作共享数据可能会引起竞态条件等并发问题,需要使用同步机制来协调线程的操作。
进程之间不共享地址空间,可以通过IPC机制来实现安全的进程间通信。
⭕进程和线程的关系如下图:
在这里插入图片描述
4. 线程异常
线程死锁:线程之间相互等待对方释放资源,导致所有线程都无法继续执行的情况。
竞态条件:多个线程同时访问共享的资源,导致意外的结果或者数据损坏。
内存泄漏:线程未正确释放动态分配的内存,导致系统资源耗尽。
线程间通信问题:线程之间的通信出现问题,例如数据丢失、阻塞等情况。
未捕获的异常:线程中的代码抛出未捕获的异常,导致线程意外终止。
⭕当一个线程出现严重的异常导致崩溃时,会触发进程内的异常处理机制。在大多数操作系统中,异常会导致信号被发送给进程,例如SIGSEGV(段错误)或SIGFPE(浮点异常)。默认情况下,这些信号会终止整个进程。
🚨注意:线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
二、Linux线程控制
1. POSIX线程库
在Linux系统中,POSIX线程库(也称为pthread库)是一套用于多线程编程的标准接口。它基于POSIX标准(Portable Operating System Interface)定义了一组函数和数据类型,使得开发者可以方便地进行多线程程序的开发。
POSIX线程库的设计目标是提供一个可移植、高效和可靠的多线程编程接口。它已经成为类UNIX系统上标准的多线程编程接口,并在Linux系统中得到广泛应用。开发者可以使用POSIX线程库编写具有良好可移植性的多线程程序,无需关心底层操作系统的差异。
2. 创建线程 pthread_create() 函数
在Linux系统中,线程的创建是通过POSIX线程库(pthread库)提供的函数来实现的
(1)头文件
pthread_create() 函数的使用需要包含pthread库的头文件pthread.h
#include <pthread.h>
1
(2)函数原型
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
1
2
(3)参数解释
thread:指向 pthread_t 类型的指针,用于存储新线程的标识符。在函数成功返回时,该指针被设置为新线程的标识符,可以用于后续操作。
attr:指向 pthread_attr_t 类型的指针,用于设置线程的属性,通常可以传入 nullptr,表示使用默认属性。如果需要设置线程的属性,可以使用 pthread_attr_init() 函数初始化属性对象,并使用 pthread_attr_setxxx() 函数设置属性。
start_routine:指向线程函数的指针,即新线程的执行体。该函数应该以 void* func(void*) 的形式定义,即带有一个 void 类型指针参数,返回一个 void 类型指针。线程函数的返回值将作为线程的退出状态,可以通过 pthread_join() 函数获取。
arg:传递给线程函数的参数,它必须是一个 void 类型指针,开发者需要自行处理类型转换。
(4)返回值
如果成功创建了新线程,则函数返回 0;
如果失败,则返回一个非零值,表示出现了错误。在出错的情况下,可以使用 perror() 函数输出错误信息,也可以使用 strerror() 函数获取错误信息。
(5)使用示例
#include <stdio.h>
#include <pthread.h>
void* thread_function(void* arg) {
int tid = *(int*)arg;
printf("This is thread %d.\n", tid);
pthread_exit(NULL);
}
int main() {
pthread_t tid[3];
int i, rc;
// 创建三个新线程
for (i = 0; i < 3; i++) {
rc = pthread_create(&tid[i], NULL, thread_function, (void*)&i);
if (rc != 0) {
fprintf(stderr, "Failed to create new thread.\n");
return 1;
}
}
// 等待所有新线程结束
for (i = 0; i < 3; i++) {
rc = pthread_join(tid[i], NULL);
if (rc != 0) {
fprintf(stderr, "Failed to join the thread.\n");
return 1;
}
}
printf("All threads exit.\n");
return 0;
}
通过上面的步骤,可以在 Linux 系统下成功创建并执行新的线程。需要注意的是,调用 pthread_create() 函数时传递给线程函数的参数必须是指向整型变量的指针,否则可能会出现不可预期的错误。
在这里插入图片描述
3. 线程ID及进程地址空间布局
(1)进程地址空间布局
进程地址空间布局是指操作系统在内存中为每个进程分配的地址空间的布局方式。以下是典型的Linux进程地址空间布局:
代码段(Text Segment):
代码段存储了可执行程序的机器指令。它通常是只读的,并且在内存中只有一份,用于所有执行该程序的进程。
数据段(Data Segment):
数据段存储了全局变量和静态变量。它包括了初始化的数据和非初始化的BSS段(Block Started by Symbol)。数据段通常是可读写的。
堆(Heap):
堆是动态分配内存的区域。在运行时,通过调用malloc()、calloc()等函数分配堆内存。堆的大小不固定,可以根据需要动态增长或缩小。
栈(Stack):
栈用于存储函数调用、局部变量和函数参数等信息。每个线程都有自己的栈,用于保存线程特定的上下文信息。栈的大小通常是固定的。
共享库(Shared Libraries):
共享库存储了被多个进程共享的代码和数据。它们被加载到内存中,并映射到每个进程的地址空间中。
内核空间(Kernel Space):
内核空间是由操作系统内核使用的内存区域,不属于进程的地址空间。它包括操作系统内核的代码和数据结构。
在这里插入图片描述
(2)线程ID pthread_self() 函数
线程ID(Thread ID)是操作系统分配给每个线程的唯一标识符。在不同的操作系统中,线程ID的表示方式和取值范围可能会有所不同。
pthread_self()函数是一个POSIX线程库中的函数,用于获取当前线程的线程ID。它的原型如下:
pthread_t pthread_self(void);
1
该函数没有参数,返回类型为pthread_t,即线程ID的类型。
使用pthread_self()函数可以在多线程程序中获取当前线程的线程ID。每个线程在创建时都会被分配一个唯一的线程ID,可以通过该ID来标识和区分不同的线程。
下面是一个简单的示例代码,演示了如何使用pthread_self()函数获取当前线程的线程ID:
#include <stdio.h>
#include <pthread.h>
void* thread_func(void* arg) {
pthread_t tid = pthread_self();
printf("Thread ID: %lu\n", tid);
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
pthread_join(tid, NULL);
return 0;
}
在上述示例中,主线程创建了一个新线程,并通过pthread_create()函数启动线程执行thread_func()函数。在thread_func()函数中,调用pthread_self()函数获取当前线程的线程ID,并将其打印输出。
🚨注意:线程ID的类型pthread_t可能是一个不透明的数据类型,具体实现取决于操作系统和编译器。在上述示例中,使用%lu格式指定符打印无符号长整型,以与pthread_t类型匹配。在不同的系统和编译环境中,可能需要根据具体情况调整打印格式。
在这里插入图片描述
4. 线程等待 pthread_join() 函数
⭕线程等待是一种同步机制,会导致线程之间的阻塞和等待。在设计多线程程序时,需要合理地安排线程的执行顺序和等待关系,以避免死锁、饥饿等问题。pthread_join()函数是一个POSIX线程库中的函数,用于等待指定的线程结束并回收其资源。
(1)头文件
pthread_join() 函数的使用需要包含pthread库的头文件pthread.h
#include <pthread.h>
1
(2)函数原型
int pthread_join(pthread_t thread, void **retval);
1
(3)参数解释
thread参数是要等待的目标线程的线程ID,
retval参数用于接收目标线程的返回值(如果有)。pthread_join()函数会阻塞调用线程,直到目标线程结束为止,并且可以获取目标线程的返回值。
(4)返回值
pthread_join() 函数的返回值表示线程的终止状态,具体取值如下:
如果线程的返回值已经被存放到 value_ptr 指向的内存中,则返回 0。
如果指定的线程在执行过程中被取消,则返回 PTHREAD_CANCELED。
如果调用该函数时出现错误,则返回相应的错误代码。
(5)使用示例
下面是一个简单的示例代码,演示了如何使用pthread_join()函数等待子线程结束并获取其返回值:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void* thread_func(void* arg) {
int* result = (int*)malloc(sizeof(int));
*result = 42;
printf("Thread is about to exit\n");
pthread_exit((void*)result); // 终止线程,并返回 result
}
int main() {
pthread_t tid;
void* ret_val;
pthread_create(&tid, NULL, thread_func, NULL);
pthread_join(tid, &ret_val); // 获取线程的返回值
if (ret_val) {
printf("Thread returned: %d\n", *((int*)ret_val));
free(ret_val); // 释放返回值对应的内存
} else {
printf("Thread returned NULL\n");
}
return 0;
}
在上面的示例中,我们创建了一个新线程,线程函数中使用了 pthread_exit() 函数来结束线程并返回一个整数值。在主线程中,我们通过 pthread_join() 函数等待线程的终止,并获取了线程的返回值。
🚨注意:pthread_join()函数会使调用线程进入阻塞状态,直到目标线程结束。如果不关心目标线程的返回值,也可以将retval参数设置为NULL。另外,在多线程程序中,需要特别注意线程的安全退出和资源回收,以避免产生悬挂线程或资源泄漏的问题。
在这里插入图片描述
5. 线程终止
(1)线程终止的三种方法
线程函数返回:线程函数执行完毕并从函数中返回,线程会自动终止。线程函数可以通过返回一个值来传递结果给线程的创建者。
调用 pthread_exit() 函数:线程可以显式地调用 pthread_exit() 函数来终止自己。这个函数接受一个参数作为线程的返回值,可以被其他线程通过调用 pthread_join() 函数获取。
取消线程:线程可以被其他线程取消。调用 pthread_cancel(pthread_t thread) 函数可以请求取消指定的线程。被取消的线程可以选择在适当的时机终止自己,或者忽略取消请求继续执行。
(2)pthread_exit() 函数
pthread_exit() 函数是用于终止当前线程并返回一个值的 POSIX 线程库函数。该函数的原型如下所示:
void pthread_exit(void* value_ptr);
1
value_ptr:表示线程的返回值,可以是任意类型的指针。当线程调用 pthread_exit() 函数时,会将 value_ptr 指向的内容作为线程的返回值。
pthread_exit() 函数允许线程在执行过程中随时退出,并返回一个值。这个返回值可以被其他线程通过调用 pthread_join() 函数获取,从而实现线程间的数据交互和结果传递。
下面是一个简单的示例,演示了如何在线程中使用 pthread_exit() 函数来结束线程并返回一个值:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void* thread_func(void* arg) {
int* result = (int*)malloc(sizeof(int));
*result = 42;
printf("Thread is about to exit\n");
pthread_exit((void*)result); // 终止线程,并返回 result
}
int main() {
pthread_t tid;
void* ret_val;
pthread_create(&tid, NULL, thread_func, NULL);
pthread_join(tid, &ret_val); // 获取线程的返回值
printf("Thread returned: %d\n", *((int*)ret_val));
free(ret_val); // 释放返回值对应的内存
return 0;
}
在上面的示例中,我们创建了一个新线程,线程函数中使用了 pthread_exit() 函数来结束线程并返回一个整数值。在主线程中,我们通过 pthread_join() 函数获取了线程的返回值,并打印输出了该值。
(3)pthread_cancel() 函数
pthread_cancel() 函数是 POSIX 线程库中用于取消指定线程的函数。该函数的原型如下所示:
int pthread_cancel(pthread_t thread);
1
thread:表示要取消的线程的标识符。
pthread_cancel() 函数会向指定线程发送一个取消请求,并尝试终止该线程的执行。但是,线程是否会被成功取消取决于多个因素,包括线程自身的取消状态和取消点的设置。
pthread_cancel() 函数只是发送一个取消请求并立即返回,并不能保证目标线程会立即终止。目标线程可以选择忽略取消请求或者在适当的取消点进行处理。
取消点(cancellation point)是线程中的一些特定位置,线程在这些位置上可以检查是否有取消请求,并决定是否终止自己的执行。常见的取消点包括 I/O 操作、阻塞的系统调用等。
如果目标线程成功响应了取消请求,并在取消点终止了执行,那么取消状态将被设置为已取消。可以通过调用 pthread_setcancelstate() 和 pthread_setcanceltype() 函数来控制线程的取消状态和取消类型。
下面是一个示例,演示了如何使用 pthread_cancel() 函数来取消线程的执行:
#include <stdio.h>
#include <pthread.h>
void* thread_func(void* arg) {
while (1) {
// 线程执行的逻辑
}
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
// 在主线程中取消子线程的执行
pthread_cancel(tid);
pthread_join(tid, NULL); // 等待线程结束
return 0;
}
在上面的示例中,我们创建了一个新线程,并在主线程中调用 pthread_cancel() 函数来取消子线程的执行。接着,我们使用 pthread_join() 函数等待子线程的终止。
🚨注意:在使用 pthread_cancel() 函数取消线程时,应确保目标线程处于可取消状态,并且在适当的位置设置了取消点。否则,取消请求可能被忽略,导致线程无法正确终止。
三、分离线程
1. joinable与线程分离
“joinable” 和线程分离是两种不同的线程状态。
“joinable” 状态的线程是指可以被其他线程显式等待和回收资源的线程。在 POSIX 线程库中,默认情况下,创建的线程是可连接状态(joinable)。可连接状态的线程需要使用 pthread_join() 函数来等待其终止,并获取其返回值(如果有)。
线程分离是指将线程属性设置为分离状态,使得线程在终止时可以自动释放相关资源,而无需等待其他线程显式对其进行回收。分离线程通常用于不需要获取线程返回值或进行线程同步的场景。
2. 分离线程 pthread_detach() 函数
pthread_detach() 函数是 POSIX 线程库中的一个函数,用于将指定线程设置为分离状态,即使该线程在终止时可以自动释放相关资源,而无需等待其他线程显式对其进行回收。
(1)头文件
pthread_detach() 函数的使用需要包含pthread库的头文件pthread.h
#include <pthread.h>
1
(2)函数原型
int pthread_detach(pthread_t thread);
1
(3)参数解释
thread:表示要设置为分离状态的线程的标识符。
(4)返回值
pthread_detach() 函数的返回值为 0 表示调用成功,返回值为非零表示调用失败。失败的原因可能是参数不正确或者内部出现了错误。
🚨注意:线程在被设置为分离状态之前,必须处于可连接状态。否则,pthread_detach() 函数将无法将其设置为分离状态,并返回一个错误码。
(5)使用示例
下面是一个示例,演示了如何使用 pthread_detach() 函数将线程设置为分离状态:
#include <stdio.h>
#include <pthread.h>
void* thread_func(void* arg) {
// 线程执行的逻辑
return NULL;
}
int main() {
pthread_t tid;
pthread_create(&tid, NULL, thread_func, NULL);
// 将线程设置为分离状态
pthread_detach(tid);
// 不需要使用 pthread_join() 函数进行回收
return 0;
}
在上面的示例中,我们创建了一个新线程,并使用 pthread_detach() 函数将其设置为分离状态。由于该线程已经处于分离状态,因此在主线程中无需使用 pthread_join() 函数进行回收。
四、线程的优缺点
线程是一种轻量级的执行单元,可以在一个进程内并发执行多个任务。线程有以下优点和缺点:
✅优点
并发执行:线程允许多个任务同时执行,提高了程序的运行效率和响应速度。可以充分利用多核处理器的计算能力。
共享数据:线程可以共享同一个进程的内存空间,方便数据之间的共享和通信。不同线程之间可以直接读取和修改同一块内存区域的数据,简化了多任务编程的复杂性。
资源高效:线程的创建和销毁消耗的资源相对较少,线程切换的开销也较小。相比于进程,线程更加轻量级。
逻辑清晰:使用线程可以将程序分解成多个独立的执行单元,每个线程负责不同的任务,使得程序的逻辑结构更加清晰、模块化。
✅缺点
同步和共享问题:多个线程访问共享数据时需要进行同步操作,以避免数据竞争和不一致的结果。需要使用锁、信号量等机制来保护共享资源,增加了编程的复杂性。
错误管理困难:线程共享同一进程的内存空间,一个线程对共享资源的错误操作可能会影响其他线程的正常执行,导致难以追踪和调试错误。
调试和测试复杂:由于线程并发执行,线程之间的交互和调试相对复杂。当程序出现问题时,需要仔细分析各个线程的执行顺序和交互情况,增加了调试和测试的难度。
资源竞争:多个线程同时访问共享资源时可能引发资源竞争问题,如死锁、饥饿等。需要合理设计和管理线程的同步和互斥机制,以避免资源竞争问题。
五、线程用途
合理的使用多线程,能提高CPU密集型程序的执行效率。
合理的使用多线程,能提高IO密集型程序的用户体验。(如生活中我们一边写代码一边下载开发工具,就是多线程运行的一种表现)