一. 线程的基本概念
线程是进程内的一个执行单元,它是调度和执行的基本单位。
1.1 Linux中的线程
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化。
1.2 线程的优点
- 创建一个新线程的代价要比创建一个新进程小得多。
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多。
- 线程占用的资源要比进程少很多。
- 能充分利用多处理器的可并行数量。
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务。
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
- IO密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
1.3 线程的缺点
- 一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失。
- 编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性很大,线程之间是缺乏保护的。
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
- 编写与调试一个多线程程序比单线程程序困难得多。
1.4 线程异常
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出。
二. 进程 VS 线程
2.1 进程和线程之间的特点
-
进程是资源分配的基本单位。
-
线程是调度的基本单位。
-
线程共享进程数据:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
- 全局变量和静态变量
- 进程地址空间
-
线程独有的数据:
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
2.2 进程和线程之间的关系
三. POSIX线程库
3.1 POSIX线程库的介绍
-
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头。
-
要使用这些函数库,要通过引入头文<pthread.h>。
-
链接这些线程函数库时要使用编译器命令的“-lpthread”选项。
g++ -o test test.cpp -lpthread
3.2 线程ID以及进程地址空间布局
-
pthread_ create函数会产生一个线程ID,存放在第一个参数指向的地址中。
-
线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程。
-
pthread_ create函数第一个参数指向一个虚拟内存单元,该内存单元的地址即为新创建线程的线程ID,
-
属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
-
线程库NPTL提供了pthread_ self函数,可以获得线程自身的ID:
pthread_t pthread_self(void);
pthread_t类型的线程ID,本质就是一个进程地址空间上的一个地址:
3.2 线程创建
pthread_create:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
参数说明:
- pthread_t *thread:指向一个类型为 pthread_t 的指针,用来存储新创建线程的标识符。如果线程成功创建,那么这个指针指向的变量会被设置成新线程的ID。
- const pthread_attr_t *attr:指向一个线程属性对象的指针,该对象包含了线程创建时的一些特殊属性设置。如果不需要特殊的属性设置,可以传入 NULL 使用默认属性。
- void (start_routine)(void):这是新线程启动后将要执行的函数地址。该函数应该接受一个 void 类型的参数,并返回一个 void* 类型的结果。
- void arg:这是一个传递给 start_routine 函数的参数,可以是任何类型的数据,但必须转换为 void 类型。
返回值:
- 如果线程创建成功,pthread_create() 函数返回 0。
- 如果失败,则返回一个非零的错误码。常见的错误码包括:
EAGAIN:系统资源不足,无法创建新的线程。
EINVAL:输入的属性对象无效。
EPERM:调用者没有权限创建线程。
例子:
#include <cstdio>
#include <cstdlib>
#include <pthread.h>
#include <unistd.h>// 新线程执行的函数
void* print_message_function(void* ptr) {char *message;message = (char *) ptr;printf("%s Begin!\n", message);sleep(3); // 模拟耗时操作printf("%s End!\n", message);pthread_exit(NULL);
}int main() {pthread_t thread1, thread2;const char *message1 = "This is Thread 1";const char *message2 = "This is Thread 2";// 创建线程int ret1 = pthread_create(&thread1, NULL, print_message_function, (void*)message1);if (ret1) {fprintf(stderr, "Error - pthread_create() return code: %d\n", ret1);exit(EXIT_FAILURE);}int ret2 = pthread_create(&thread2, NULL, print_message_function, (void*)message2);if (ret2) {fprintf(stderr, "Error - pthread_create() return code: %d\n", ret2);exit(EXIT_FAILURE);}// 等待两个线程结束pthread_join(thread1, NULL);pthread_join(thread2, NULL);printf("Threads completed successfully.\n");return 0;
}
3.3 线程终止
pthread_exit:
void pthread_exit(void *value_ptr);
参数说明:
- void *value_ptr:一个指向任意类型的指针,用于向等待该线程结束的其他线程传递退出状态或结果。通常情况下,如果线程正常结束,可以传递 NULL 或者线程计算出的结果。
例子:
#include <cstdio>
#include <pthread.h>void* thread_function(void *arg) {printf("Thread is running...\n");pthread_exit((void*)123); // 退出线程并返回值 123
}int main() {pthread_t thread;void *result;pthread_create(&thread, NULL, thread_function, NULL);pthread_join(thread, &result);printf("Thread returned value: %ld\n", (long)result); // 输出线程返回的值return 0;
}
pthread_cancel:
int pthread_cancel(pthread_t thread);
参数说明:
- pthread_t thread:要取消的线程的 ID。
例子:
#include <cstdio>
#include <pthread.h>
#include <unistd.h>void* thread_function(void *arg) {while (1) {printf("Thread is running...\n");sleep(1);}return NULL;
}int main() {pthread_t thread;pthread_create(&thread, NULL, thread_function, NULL);sleep(3); // 让主线程等待一段时间pthread_cancel(thread); // 请求取消线程pthread_join(thread, NULL); // 等待线程结束printf("Thread has been canceled.\n");return 0;
}
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
3.4 线程等待
pthread_join:
int pthread_join(pthread_t thread, void **value_ptr);
参数说明:
- pthread_t thread:要等待的线程的 ID。
- void **value_ptr:一个指向指针的指针,用于接收线程的退出状态或结果。如果不需要接收退出状态,可以传入 NULL。
返回值:
- 如果成功,pthread_join() 函数返回 0。
- 如果失败,返回一个非零的错误码。常见的错误码包括:
EINVAL:指定的线程 ID 无效。
ESRCH:指定的线程不存在。
EDEADLK:可能会发生死锁(例如,一个线程尝试等待自己)。
例子:
#include <cstdio>
#include <pthread.h>
#include <unistd.h>// 新线程执行的函数
void* thread_function(void* arg) {printf("Thread is running...\n");pthread_exit((void*)123);
}int main() {pthread_t thread;void *result;pthread_create(&thread, NULL, thread_function, NULL);// 等待线程结束pthread_join(thread, &result);// 输出线程返回的值printf("Thread returned value: %ld\n", (long)result);return 0;
}
线程为什么需要等待?
- 已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
- 创建新的线程不会复用刚才退出线程的地址空间。
为什么value_ptr是二级指针?
- 如果 value_ptr 是一级指针(即 void *value_ptr),那么 pthread_join() 只能修改传入的指针本身,而不能修改指针所指向的内存。
- 使用二级指针可以让 pthread_join() 修改指针所指向的内存,从而将线程的退出状态或结果赋值给调用者提供的变量。
3.5 分离线程
pthread_detach:
int pthread_detach(pthread_t thread);
参数说明:
- pthread_t thread:要分离的线程的 ID。
返回值:
- 如果成功,pthread_detach() 函数返回 0。
- 如果失败,返回一个非零的错误码。常见的错误码包括:
EINVAL:指定的线程 ID 无效。
ESRCH:指定的线程不存在。
EINVAL:线程已经是分离状态。
例子:
#include <cstdio>
#include <pthread.h>
#include <unistd.h>// 新线程执行的函数
void* thread_function(void* arg) {printf("Thread is running...\n");sleep(1); // 模拟耗时操作printf("Thread is about to exit...\n");pthread_exit(NULL); // 退出线程
}int main() {pthread_t thread;pthread_create(&thread, NULL, thread_function, NULL);// 将线程标记为分离状态pthread_detach(thread);// 主线程继续执行printf("Main thread continues...\n");sleep(3); // 给新线程一些时间来运行printf("Main thread exiting...\n");return 0;
}
为什么会有分离线程?
- 默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
- 如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
————————————————————
感谢大家观看,不妨点赞支持一下吧喵~
如有错误,随时纠正喵~