引言
多线程编程是现代软件开发中不可或缺的一部分。本文将深入讨论线程和进程的优缺点,线程异常和用途,线程安全问题,锁的概念,死锁现象,生产者消费者模型,BlockingQueue,POSIX信号量,环形队列以及在Linux环境下如何全面应用线程池。
站在内核角度看进程(承担系统资源分配的实体叫进程)
站在内核的角度看进程,进程是操作系统中的一个重要概念,内核对进程的管理涉及到多个方面:
总的来说,内核是操作系统的核心组件,负责对进程进行管理和调度,以实现多任务和多用户的并发执行。内核提供了一组接口和机制,使得进程可以与硬件和其他进程进行交互,同时保障系统的稳定性和安全性。
进程并不是通过task_struct来衡量的,除了task_struct之外,一个进程还要有进程地址空间、文件、信号等等,合起来称之为一个进程。
站在内核角度看线程(承担执行任务的实体叫线程,共享其所属进程的资源。)
站在内核的角度看线程,线程是在进程内部执行的轻量级任务单元。与进程相比,线程共享相同的地址空间和资源,因此线程之间的切换比进程更加高效。以下是内核在处理线程时的一些关键方面:
总体而言,内核在处理线程时需要考虑资源共享、同步、调度和上下文切换等方面,以确保线程能够有效地运行并协同工作。线程的引入提供了更细粒度的并发控制,使得程序可以更有效地利用多核处理器的优势。
线程ID、 一组寄存器 、 栈 、errno、 信号屏蔽字、 调度优先级
文件描述符表、每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)、当前工作目录、用户id和组id
字段 | 作用 |
---|---|
进程信息: | PCB 存储了一个进程的基本信息,如进程标识符(PID)、状态、程序计数器(PC)等。 |
寄存器集合: | PCB 包含了进程在执行过程中寄存器的内容,这些寄存器的状态用于在进程切换时保存和恢复现场。 |
内存管理信息: | PCB 记录了进程的地址空间信息,包括代码段、数据段、堆、栈等。 |
文件描述符表: | PCB 包含了进程打开的文件列表和相应的文件描述符。 |
进程调度信息: | 包括进程的优先级、调度状态等信息,用于操作系统的进程调度。 |
进程控制信息: | 包括父进程、子进程关系,信号处理信息等。 |
字段 | 作用 |
---|---|
线程信息: | TCB 包含了线程的标识符、状态(运行、就绪、阻塞等)等基本信息。 |
寄存器集合: | 类似于 PCB,TCB 也包含了线程在执行过程中寄存器的内容,用于保存和恢复现场。 |
栈指针: | TCB 记录了线程的栈指针,指向线程的执行栈。 |
调度信息: | 包括线程的优先级、调度状态等,用于线程调度。 |
线程私有存储: | 每个线程有自己的私有存储空间,TCB 记录了这部分存储的信息。 |
所属进程信息: | TCB 可能包含了所属进程的一些信息,如进程的 PID。 |
小结
对比两者可以发现,线程就是轻量级的进程
CPU能分清进程和线程吗?
站在CPU角度,实际是分不清线程和进程的,也不需要分清楚,因为操作系统只关心每个PCB(task_struct)的调度——也就是每个执行流的调度,对每个PCB进行管理即可。
Linux中存在线程吗?
综上可知,Linux下并不存在真正的多线程,而是用进程模拟的!只是线程的执行粒度比进程更细!
Linux中的线程系统调用是真正意义上的线程系统调用吗?
Linux中都没有真正意义上的线程了,那么自然也没有真正意义上的线程相关的系统调用了。但Linux 提供了系统调用来创建和管理这些轻量级进程(即线程),例如 clone() 系统调用。clone() 允许创建新的执行线程,并且可以选择与调用者共享内存空间、文件描述符和其他资源。这个系统调用提供了创建轻量级进程(线程)的功能。
特点
优点:
独立性: 进程之间相互独立,一个进程的错误不会影响其他进程的稳定性,提高了系统的可靠性。
安全性: 进程拥有独立的地址空间,一个进程无法直接访问另一个进程的数据,从而提高了系统的安全性。
并发性: 进程允许多个任务同时执行,提高了系统的并发性和处理能力。
灵活性: 进程可以独立开发、测试、调试和部署,提供了灵活性和可维护性。
资源共享: 进程之间可以通过进程间通信(IPC)机制来共享数据和信息,实现资源共享。
缺点:
资源消耗: 每个进程都需要独立的内存空间、文件描述符等系统资源,因此创建和维护进程会占用较多的系统资源。
切换开销: 进程切换需要保存和恢复进程的状态,这会引起一定的开销,特别是在多进程并发执行时。
复杂性: 进程之间的独立性和隔离性使得它们相对复杂,涉及到进程间通信和同步的问题。
通信困难: 进程之间的通信需要额外的机制,如管道、消息队列、共享内存等,这增加了系统的复杂性。
同步问题: 进程间的同步需要考虑临界区、互斥、信号量等问题,处理不当容易引发死锁和竞态条件。
特点
优点:
缺点:
fork 函数:
pid_t child_pid = fork();
if (child_pid == 0) {
// 子进程执行的代码
} else if (child_pid > 0) {
// 父进程执行的代码
} else {
// 创建进程失败的处理代码
}
vfork 函数:
pid_t child_pid = vfork();
if (child_pid == 0) {
// 子进程执行的代码
_exit(0); // 或者 exec 函数
} else if (child_pid > 0) {
// 父进程执行的代码
} else {
// 创建进程失败的处理代码
}
- 由来:在Linux中,站在内核角度没有真正意义上线程相关的接口,但是站在用户角度,当用户想创建一个线程时更期望使用thread_create这样类似的接口,而不是vfork函数,因此系统为用户层提供了原生线程库pthread。
- 意义: 原生线程库pthread的设计和实现旨在提供一套可移植、高性能、可扩展的多线程编程接口,使得开发者能够更容易地编写并发程序,而无需过多关注底层系统的细节。
pthread_create 函数:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
参数名 | 作用 |
---|---|
thread | 是用于存储新线程ID的指针。 |
attr | 是线程的属性,可以为NULL,表示使用默认属性。 |
start_routine | 是新线程的起始函数。 |
arg | 是传递给start_routine的参数。 |
#include <pthread.h>
void* thread_function(void* arg) {
// 新线程执行的代码
return NULL;
}
int main() {
pthread_t thread_id;
int result = pthread_create(&thread_id, NULL, thread_function, NULL);
if (result != 0) {
// 创建线程失败的处理代码
}
// 主线程执行的代码
pthread_join(thread_id, NULL); // 等待新线程结束
return 0;
}
对比之下,如果是在进程层面进行等待,通常会使用类似于wait或者waitpid的系统调用来等待子进程的结束。
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main() {
pid_t child_pid = fork();
if (child_pid == -1) {
// 处理 fork 失败的情况
perror("fork");
return 1;
}
if (child_pid == 0) {
// 子进程执行的代码
// ...
return 0;
} else {
// 父进程执行的代码
int status;
pid_t terminated_pid = waitpid(child_pid, &status, 0);
if (terminated_pid == -1) {
// 处理 waitpid 失败的情况
perror("waitpid");
return 1;
}
if (WIFEXITED(status)) {
// 子进程正常退出
printf("Child process %d exited with status %d\n", terminated_pid, WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
// 子进程因信号而结束
printf("Child process %d terminated by signal %d\n", terminated_pid, WTERMSIG(status));
}
}
return 0;
}
总的来说,线程和进程的等待机制有一些不同,主要是因为线程共享同一地址空间,而进程有独立的地址空间。在线程中,我们使用pthread_join等待线程的结束,而在进程中,可以使用waitpid等待子进程的结束。
常见异常:
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该进程内的所有线程也就随即退出
每当放假,就是抢票高峰期,我们用不同终端(手机,电脑)去网页上抢票也就是一个多线程应用案例
#include <iostream>
#include <thread>
#include <cerrno>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>
using namespace std;
// 如果多线程访问同一个全局变量,并对它进行数据计算,多线程会互相影响吗?
int tickets = 1000; // 在并发访问的时候,导致了我们数据不一致的问题!
void *getTickets(void *args)
{
(void)args;
while(true)
{
if(tickets > 0)
{
usleep(1000);
printf("%p: %d\n", pthread_self(), tickets);
tickets--;
}
else{
break;
}
}
return nullptr;
}
int main()
{
pthread_t t1,t2,t3;
// 多线程抢票的逻辑
pthread_create(&t1, nullptr, getTickets, nullptr);
pthread_create(&t2, nullptr, getTickets, nullptr);
pthread_create(&t3, nullptr, getTickets, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
}
结果:
分析结果,得出问题——为什么抢到了0张和-1张票呢???(下一章解答您心中疑问)