在当今信息技术日新月异的时代,多线程编程已经成为了日常开发中不可或缺的一部分。Linux作为一种广泛应用的操作系统,其对多线程编程的支持也相当完善。本文将会介绍关于Linux多线程相关的知识,其中包括了线程的概念、线程控制、线程分离等方面的内容。如果你希望提升自己的多线程编程能力,本文将为你提供实用的技术指导和详尽的知识储备。让我们一起来深入了解Linux多线程编程的奥秘吧!
在Linux中,线程是指在同一个进程内部并发执行的多个子任务。Linux将线程作为轻量级进程(LWP)来实现,每个线程共享相同的进程地址空间和其他资源,包括文件描述符、信号处理器和其他内核状态。由于线程之间共享进程的资源,因此线程之间的切换开销通常比进程之间的切换要小得多。
Linux使用POSIX线程库(pthread)来支持多线程编程。通过pthread库,开发人员可以方便地创建、控制和同步线程,实现多线程编程的各种功能。在Linux中,线程的创建和管理都是通过系统调用和pthread库来完成的,开发人员可以使用pthread_create()
函数创建新线程,并使用pthread_join()
函数等来等待线程的结束,后面我们会详细介绍。
在Linux中,线程与进程一样拥有自己的ID、寄存器上下文、栈和线程本地存储(TLS)。线程可以通过共享内存进行通信,也可以使用线程同步机制来协调彼此的操作。在多核处理器上,Linux内核会将不同的线程分配到不同的处理器核心上并行执行,以提高系统的性能和响应速度。
总的来说,Linux线程是在同一个进程内并发执行的多个子任务,通过共享进程的资源和使用pthread库来实现线程的创建和管理。在Linux环境下,充分利用线程可以提高程序的并发能力和性能表现。
Linux线程和进程的主要区别在于它们是操作系统对应不同的执行单元。
资源分配
切换开销
通信机制
并发能力
安全性
线程死锁:线程之间相互等待对方释放资源,导致所有线程都无法继续执行的情况。
竞态条件:多个线程同时访问共享的资源,导致意外的结果或者数据损坏。
内存泄漏:线程未正确释放动态分配的内存,导致系统资源耗尽。
线程间通信问题:线程之间的通信出现问题,例如数据丢失、阻塞等情况。
未捕获的异常:线程中的代码抛出未捕获的异常,导致线程意外终止。
⭕当一个线程出现严重的异常导致崩溃时,会触发进程内的异常处理机制。在大多数操作系统中,异常会导致信号被发送给进程,例如SIGSEGV(段错误)或SIGFPE(浮点异常)。默认情况下,这些信号会终止整个进程。
注意:线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
在Linux系统中,POSIX线程库(也称为pthread库)是一套用于多线程编程的标准接口。它基于POSIX标准(Portable Operating System Interface)定义了一组函数和数据类型,使得开发者可以方便地进行多线程程序的开发。
POSIX线程库的设计目标是提供一个可移植、高效和可靠的多线程编程接口。它已经成为类UNIX系统上标准的多线程编程接口,并在Linux系统中得到广泛应用。开发者可以使用POSIX线程库编写具有良好可移植性的多线程程序,无需关心底层操作系统的差异。
在Linux系统中,线程的创建是通过POSIX线程库(pthread库)提供的函数来实现的
pthread_create()
函数的使用需要包含pthread库的头文件pthread.h
#include
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
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
类型指针,开发者需要自行处理类型转换。
perror()
函数输出错误信息,也可以使用 strerror()
函数获取错误信息。#include
#include
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()
函数时传递给线程函数的参数必须是指向整型变量的指针,否则可能会出现不可预期的错误。
进程地址空间布局是指操作系统在内存中为每个进程分配的地址空间的布局方式。以下是典型的Linux进程地址空间布局:
代码段(Text Segment):
代码段存储了可执行程序的机器指令。它通常是只读的,并且在内存中只有一份,用于所有执行该程序的进程。
数据段(Data Segment):
数据段存储了全局变量和静态变量。它包括了初始化的数据和非初始化的BSS段(Block Started by Symbol)。数据段通常是可读写的。
堆(Heap):
堆是动态分配内存的区域。在运行时,通过调用malloc()
、calloc()
等函数分配堆内存。堆的大小不固定,可以根据需要动态增长或缩小。
栈(Stack):
栈用于存储函数调用、局部变量和函数参数等信息。每个线程都有自己的栈,用于保存线程特定的上下文信息。栈的大小通常是固定的。
共享库(Shared Libraries):
共享库存储了被多个进程共享的代码和数据。它们被加载到内存中,并映射到每个进程的地址空间中。
内核空间(Kernel Space):
内核空间是由操作系统内核使用的内存区域,不属于进程的地址空间。它包括操作系统内核的代码和数据结构。
线程ID(Thread ID)是操作系统分配给每个线程的唯一标识符。在不同的操作系统中,线程ID的表示方式和取值范围可能会有所不同。
pthread_self()
函数是一个POSIX线程库中的函数,用于获取当前线程的线程ID。它的原型如下:
pthread_t pthread_self(void);
该函数没有参数,返回类型为pthread_t
,即线程ID的类型。
使用pthread_self()
函数可以在多线程程序中获取当前线程的线程ID。每个线程在创建时都会被分配一个唯一的线程ID,可以通过该ID来标识和区分不同的线程。
下面是一个简单的示例代码,演示了如何使用pthread_self()
函数获取当前线程的线程ID:
#include
#include
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
类型匹配。在不同的系统和编译环境中,可能需要根据具体情况调整打印格式。
⭕线程等待是一种同步机制,会导致线程之间的阻塞和等待。在设计多线程程序时,需要合理地安排线程的执行顺序和等待关系,以避免死锁、饥饿等问题。pthread_join()
函数是一个POSIX线程库中的函数,用于等待指定的线程结束并回收其资源。
pthread_join()
函数的使用需要包含pthread库的头文件pthread.h
#include
int pthread_join(pthread_t thread, void **retval);
thread
参数是要等待的目标线程的线程ID,retval
参数用于接收目标线程的返回值(如果有)。pthread_join()
函数会阻塞调用线程,直到目标线程结束为止,并且可以获取目标线程的返回值。pthread_join()
函数的返回值表示线程的终止状态,具体取值如下:
value_ptr
指向的内存中,则返回 0。PTHREAD_CANCELED
。下面是一个简单的示例代码,演示了如何使用pthread_join()
函数等待子线程结束并获取其返回值:
#include
#include
#include
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
。另外,在多线程程序中,需要特别注意线程的安全退出和资源回收,以避免产生悬挂线程或资源泄漏的问题。
线程函数返回:线程函数执行完毕并从函数中返回,线程会自动终止。线程函数可以通过返回一个值来传递结果给线程的创建者。
调用 pthread_exit()
函数:线程可以显式地调用 pthread_exit()
函数来终止自己。这个函数接受一个参数作为线程的返回值,可以被其他线程通过调用 pthread_join()
函数获取。
取消线程:线程可以被其他线程取消。调用 pthread_cancel(pthread_t thread)
函数可以请求取消指定的线程。被取消的线程可以选择在适当的时机终止自己,或者忽略取消请求继续执行。
pthread_exit()
函数是用于终止当前线程并返回一个值的 POSIX 线程库函数。该函数的原型如下所示:
void pthread_exit(void* value_ptr);
value_ptr
:表示线程的返回值,可以是任意类型的指针。当线程调用 pthread_exit()
函数时,会将 value_ptr
指向的内容作为线程的返回值。pthread_exit()
函数允许线程在执行过程中随时退出,并返回一个值。这个返回值可以被其他线程通过调用 pthread_join()
函数获取,从而实现线程间的数据交互和结果传递。
下面是一个简单的示例,演示了如何在线程中使用 pthread_exit()
函数来结束线程并返回一个值:
#include
#include
#include
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()
函数获取了线程的返回值,并打印输出了该值。
pthread_cancel()
函数是 POSIX 线程库中用于取消指定线程的函数。该函数的原型如下所示:
int pthread_cancel(pthread_t thread);
thread
:表示要取消的线程的标识符。pthread_cancel()
函数会向指定线程发送一个取消请求,并尝试终止该线程的执行。但是,线程是否会被成功取消取决于多个因素,包括线程自身的取消状态和取消点的设置。
pthread_cancel()
函数只是发送一个取消请求并立即返回,并不能保证目标线程会立即终止。目标线程可以选择忽略取消请求或者在适当的取消点进行处理。
取消点(cancellation point)是线程中的一些特定位置,线程在这些位置上可以检查是否有取消请求,并决定是否终止自己的执行。常见的取消点包括 I/O 操作、阻塞的系统调用等。
如果目标线程成功响应了取消请求,并在取消点终止了执行,那么取消状态将被设置为已取消。可以通过调用 pthread_setcancelstate()
和 pthread_setcanceltype()
函数来控制线程的取消状态和取消类型。
下面是一个示例,演示了如何使用 pthread_cancel()
函数来取消线程的执行:
#include
#include
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()
函数取消线程时,应确保目标线程处于可取消状态,并且在适当的位置设置了取消点。否则,取消请求可能被忽略,导致线程无法正确终止。
“joinable” 和线程分离是两种不同的线程状态。
pthread_join()
函数来等待其终止,并获取其返回值(如果有)。pthread_detach()
函数是 POSIX 线程库中的一个函数,用于将指定线程设置为分离状态,即使该线程在终止时可以自动释放相关资源,而无需等待其他线程显式对其进行回收。
pthread_detach()
函数的使用需要包含pthread库的头文件pthread.h
#include
int pthread_detach(pthread_t thread);
thread
:表示要设置为分离状态的线程的标识符。pthread_detach()
函数的返回值为 0 表示调用成功,返回值为非零表示调用失败。失败的原因可能是参数不正确或者内部出现了错误。
注意:线程在被设置为分离状态之前,必须处于可连接状态。否则,pthread_detach()
函数将无法将其设置为分离状态,并返回一个错误码。
下面是一个示例,演示了如何使用 pthread_detach()
函数将线程设置为分离状态:
#include
#include
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()
函数进行回收。
线程是一种轻量级的执行单元,可以在一个进程内并发执行多个任务。线程有以下优点和缺点:
✅优点
✅缺点
感谢您对博主文章的关注与支持!如果您喜欢这篇文章,可以点赞、评论和分享给您的同学,这将对我提供巨大的鼓励和支持。另外,我计划在未来的更新中持续探讨与本文相关的内容。我会为您带来更多关于Linux以及C++编程技术问题的深入解析、应用案例和趣味玩法等。如果感兴趣的话可以关注博主的更新,不要错过任何精彩内容!
再次感谢您的支持和关注。我们期待与您建立更紧密的互动,共同探索Linux、C++、算法和编程的奥秘。祝您生活愉快,排便顺畅!