目录
01-线程的概念(通过“进程和线程”的区别来理解)
简单的理解
一个程序对应一个进程,一个进程中可以有多个线程。在Linux系统中,资源的分配是以进程为单位的,而调度的基本单位是线程。
如果在一个主函数创建了一个线程,那么新线程会立即开始执行线程函数thread_function,而主线程继续向下执行。具体的示例请看下面的“示例代码”。
如果在一个主函数创建了一个线程,那么新线程会立即开始执行线程函数thread_function,而主线程继续向下执行。具体的示例请看下面的“示例代码”。
详细的理解
在Linux系统中,进程和线程是两种基本的运行单位,它们有以下区别:
1. 定义与本质
- 进程:是一个程序在运行中的实例,是系统进行资源分配和调度的基本单位。
- 线程:是进程内的一个执行流,是CPU调度的基本单位,一个进程可以包含多个线程。
2. 资源占用
- 进程:
- 每个进程有独立的地址空间。
- 进程之间的内存空间彼此隔离,需通过**进程间通信(IPC)**进行数据共享(如管道、共享内存等)。
- 独立的文件描述符表、堆栈、全局变量等。
- 线程:
- 同一进程中的线程共享进程的地址空间。
- 线程之间共享全局变量、堆、文件描述符等资源。
- 每个线程有独立的栈和寄存器上下文。
3. 切换开销
- 进程:
- 进程切换需要保存和恢复整个进程上下文,包括地址空间切换、页表切换等,开销较大。
- 线程:
- 线程切换只需保存和恢复线程的寄存器上下文(无需地址空间切换),开销较小。
4. 通信方式
- 进程:
- 通信方式较复杂,需要使用管道、消息队列、共享内存、信号等机制。
- 线程:
- 同一进程内的线程可以直接通过共享的内存空间进行通信,速度较快。
5. 独立性
- 进程:
- 进程之间相对独立,一个进程的崩溃通常不会直接影响其他进程。
- 线程:
- 同一进程内的线程是相互关联的,一个线程的崩溃可能导致整个进程终止。
6. 调度
- 进程:
- 由操作系统内核调度,进程的调度涉及复杂的优先级和资源分配策略。
- 线程:
- 可以由用户级线程库或内核线程支持,调度通常更加轻量。
7. 使用场景
- 进程:
- 适合需要高度隔离、独立运行的场景(如守护进程、Web服务器的多个子进程)。
- 线程:
- 适合需要频繁数据共享、低延迟的场景(如并行计算、实时应用)。
8. 在Linux中实现
- 进程:使用
fork()系统调用创建新进程。 - 线程:使用POSIX线程(
pthread)库或其他多线程API创建线程。
对比总结
| 特性 | 进程 | 线程 |
|---|---|---|
| 地址空间 | 独立 | 共享 |
| 资源占用 | 高 | 低 |
| 通信方式 | IPC(进程间通信) | 共享内存 |
| 切换开销 | 高 | 低 |
| 独立性 | 强 | 弱 |
| 使用场景 | 隔离性要求高的任务 | 并发性要求高的任务 |
02-线程创建函数pthread_create()详解
在Linux系统中,线程的创建通常使用pthread_create函数,该函数是POSIX线程(Pthreads)库的一部分。以下是对pthread_create函数的详细介绍:
函数原型
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine)(void *), void *arg);
参数说明
-
pthread_t *thread- 用于存储创建的线程ID,类型为
pthread_t。 - 创建成功后,
*thread中将保存新线程的标识符,可以用于线程的后续操作(如pthread_join)。
- 用于存储创建的线程ID,类型为
-
const pthread_attr_t *attr- 指定线程的属性。可以为
NULL,表示使用默认属性。 - 通过
pthread_attr_init和相关函数可以设置线程属性,如分离状态、栈大小等。
- 指定线程的属性。可以为
-
void *(*start_routine)(void *)- 新线程的执行函数,类似于线程的入口函数。
- 函数原型是
void *function_name(void *arg),它接收一个void *类型的参数并返回一个void *类型的值。
-
void *arg- 传递给
start_routine的参数。 - 可以传递任何类型的指针(通常为结构体指针),若不需要参数可传
NULL。
- 传递给
返回值
- 0:线程创建成功。
- 非0值:线程创建失败,返回错误码。
- 如
EAGAIN表示系统资源不足,EINVAL表示参数无效。
- 如
线程函数(start_routine)说明
- 线程函数执行结束时,可以通过
return返回值(void *类型)来返回结果。 - 如果线程函数不需要返回值,可以直接使用
return NULL。 - 线程也可以通过
pthread_exit退出。
示例代码
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// 线程函数
void *thread_function(void *arg) {
int *num = (int *)arg;
printf("Thread ID: %ld, Arg: %d\n", pthread_self(), *num);
return NULL; // 线程结束
}
int main() {
pthread_t thread; // 定义线程ID
int arg = 42; // 传递给线程的参数
int result;
// 创建线程
result = pthread_create(&thread, NULL, thread_function, &arg);
if (result != 0) {
fprintf(stderr, "Error: pthread_create failed (code: %d)\n", result);
return EXIT_FAILURE;
}
// 等待线程结束
pthread_join(thread, NULL);
printf("Thread finished.\n");
return EXIT_SUCCESS;
}
上面这个示例代码的详解:
让我们详细分析一下示例代码的执行流程和可能的输出结果:
代码回顾
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
// 线程函数
void *thread_function(void *arg) {
int *num = (int *)arg;
printf("Thread ID: %ld, Arg: %d\n", pthread_self(), *num);
return NULL; // 线程结束
}
int main() {
pthread_t thread; // 定义线程ID
int arg = 42; // 传递给线程的参数
int result;
// 创建线程
result = pthread_create(&thread, NULL, thread_function, &arg);
if (result != 0) {
fprintf(stderr, "Error: pthread_create failed (code: %d)\n", result);
return EXIT_FAILURE;
}
// 等待线程结束
pthread_join(thread, NULL);
printf("Thread finished.\n");
return EXIT_SUCCESS;
}
执行流程
-
主线程启动:
- 主线程从
main()开始执行,定义了一个线程ID变量pthread_t thread,和一个整型变量arg初始化为42。
- 主线程从
-
创建线程:
- 主线程调用
pthread_create,创建一个新线程并指定:- 新线程的入口函数为
thread_function。 - 参数为指向
arg的指针。
- 新线程的入口函数为
- 如果线程创建成功,新线程会立即开始执行
thread_function,而主线程继续向下执行。(这句话是重点,表明了线程在程序中的执行顺序) - 如果线程创建成功,新线程会立即开始执行
thread_function,而主线程继续向下执行。(这句话是重点,表明了线程在程序中的执行顺序) - 如果线程创建成功,新线程会立即开始执行
thread_function,而主线程继续向下执行。(这句话是重点,表明了线程在程序中的执行顺序)
- 主线程调用
-
新线程运行:
- 新线程执行
thread_function:- 从参数指针
arg中读取值42并打印线程ID及参数值。 - 然后返回
NULL,结束线程执行。
- 从参数指针
- 新线程执行
-
主线程等待:
- 主线程调用
pthread_join,阻塞自身,等待新线程完成。 - 一旦新线程执行完毕,
pthread_join返回,主线程继续执行。
- 主线程调用
-
主线程完成:
- 主线程打印
"Thread finished.",并以EXIT_SUCCESS退出。
- 主线程打印
输出结果
假设代码正常执行,输出可能类似以下内容(线程ID会因环境不同而变化):
Thread ID: 140389948131072, Arg: 42
Thread finished.
关键点说明
-
线程ID
- 每个线程在系统中有一个唯一的线程ID(由
pthread_self()获取)。打印的ID值通常是一个长整型数字,具体数值依赖于系统实现。
- 每个线程在系统中有一个唯一的线程ID(由
-
执行顺序
- 虽然主线程和新线程是并发执行的,但由于主线程调用了
pthread_join,主线程会在pthread_join处等待新线程完成。因此,输出始终会是:- 先打印新线程的输出。
- 再打印主线程的
"Thread finished."。
- 虽然主线程和新线程是并发执行的,但由于主线程调用了
-
共享变量安全
- 这里
arg是一个普通的局部变量,其地址被传递给新线程。由于主线程不会修改它,因此不会发生数据竞争。
- 这里
-
返回值
- 新线程通过
return NULL结束,其返回值不会被主线程使用(pthread_join的第二个参数为NULL)。
- 新线程通过
变种情况
-
如果不调用
pthread_join:- 主线程可能会在新线程完成之前退出,导致新线程被系统强制终止,可能输出不完整。
-
多线程竞争:
- 如果多个线程共享一个变量(如
arg),可能需要引入同步机制(如互斥锁)以避免竞争。
- 如果多个线程共享一个变量(如
03-对互斥锁深入的理解及相关函数
互斥锁的简单理解是为了让同一段代码(临界区)不被多个线程同时调用的机制。
互斥锁的深入理解是(pthread_mutex_lock)的主要目的是为了保护共享资源,确保同一段代码(临界区)不会被多个线程同时执行,从而避免数据竞争和不一致性。
关键点:互斥锁的作用
-
保护共享资源:
- 如果多个线程同时访问或修改共享资源(如全局变量、文件、内存区域等),可能导致数据竞争和未定义行为。
- 互斥锁通过确保同一时刻只有一个线程能访问临界区,从而保证数据安全性。
-
临界区:
- 临界区是指一段需要互斥访问的代码(通常涉及共享资源的读写)。
- 通过互斥锁加锁和解锁,确保在进入临界区时,只有一个线程可以运行该段代码。
简单类比(一个形象具体的例子)
你可以把临界区想象成一间有钥匙的房间:
- 钥匙(互斥锁):一个线程进入房间(临界区)时,需要拿到钥匙;如果钥匙已经被其他线程拿走,当前线程必须等待。
- 房间(共享资源):一次只能让一个人(线程)进入,防止混乱。
互斥锁变量必须要先进行初始化才能使用
互斥锁变量的类型为pthread_mutex_t,一个互斥锁变量必须要进行初始化后才能使用,初始化有两种方式:
一是用函数pthread_mutex_init(),示例代码如下:
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);
二是用静态初始化宏PTHREAD_MUTEX_INITIALIZER初始化,示例代码如下:
// 使用静态初始化宏初始化互斥锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
二者的区别:
静态初始化:PTHREAD_MUTEX_INITIALIZER在编译时完成初始化,适用于全局型变量的互斥锁。
动态初始化:pthread_mutex_init在运行时显式调用,适用于在堆或局部变量中动态分配的互斥锁。
互斥锁的工作机制和最重要的两个相关函数
-
加锁(
pthread_mutex_lock):- 检查互斥锁是否可用:
- 如果锁可用,则当前线程获得锁并进入临界区。
- 如果锁已被其他线程持有,当前线程会阻塞,直到锁被释放。
- 检查互斥锁是否可用:
-
执行临界区代码:
- 当前线程独占临界区的访问权限。
- 其他线程无法进入,直到当前线程解锁。
-
解锁(
pthread_mutex_unlock):- 释放锁,允许其他阻塞的线程竞争获取锁。
利用互斥锁避免竞态条件的示例代码
假设我们有一个共享变量counter,多个线程同时对它进行递增操作:
未加锁(可能导致竞态条件):
#include <stdio.h>
#include <pthread.h>
int counter = 0;
void *increment(void *arg) {
for (int i = 0; i < 100000; i++) {
counter++; // 竞态条件:多个线程可能同时访问此语句
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("Final Counter Value: %d\n", counter); // 结果可能小于200000
return 0;
}
加锁(避免竞态条件):
#include <stdio.h>
#include <pthread.h>
int counter = 0;
pthread_mutex_t lock;
void *increment(void *arg) {
for (int i = 0; i < 100000; i++) {
pthread_mutex_lock(&lock); // 加锁
counter++; // 临界区
pthread_mutex_unlock(&lock); // 解锁
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&lock, NULL); // 初始化互斥锁
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&lock); // 销毁互斥锁
printf("Final Counter Value: %d\n", counter); // 结果为200000
return 0;
}
加锁的必要性
-
未加锁时的问题(竞态条件):
- 多个线程可能同时执行
counter++,导致指令重叠和错误结果。 - 例如,线程A和线程B都读取了
counter=0,分别执行counter++,最终结果可能是1而非期望的2。
- 多个线程可能同时执行
-
加锁后:
- 每次只有一个线程可以执行
counter++,其他线程会等待锁释放,从而保证计数器的更新是安全的。
- 每次只有一个线程可以执行
小结
互斥锁的目标并不仅仅是防止同一段代码被多个线程调用,而是防止共享资源的并发访问引发的不一致问题。
通过互斥锁,我们可以确保:
- 共享资源在同一时刻只被一个线程访问。
- 临界区代码的执行是线程安全的。
如果你的代码中存在对共享资源的并发访问,互斥锁是一个可靠的工具来保证正确性。
04-线程挂起(处于等待状态)的相关知识和相关函数pthread_cond_wait()的介绍
线程挂起(处于等待状态)的含义
某个线程在进入挂起状态时,它就暂停了执行,直到等待被唤醒,线程的挂起操作,使用函数pthread_cond_wait()来实现
函数pthread_cond_wait()的介绍及示例代码
在 Linux 系统中,pthread_cond_wait 是用于线程间同步的条件变量相关函数,它通常与互斥锁(pthread_mutex_t)配合使用,用来等待某个条件发生并挂起线程执行。
函数原型
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
参数说明
cond: 指向条件变量的指针,类型为pthread_cond_t。mutex: 指向互斥锁的指针,类型为pthread_mutex_t。它用来保护条件变量以及共享资源的访问。
工作原理
- 调用
pthread_cond_wait时,线程会先自动释放互斥锁,并进入等待状态。 - 其他线程可以通过
pthread_cond_signal或pthread_cond_broadcast唤醒一个或多个等待线程。 - 线程被唤醒后,
pthread_cond_wait会自动重新获取互斥锁,然后返回到调用者处继续执行。
返回值
- 返回
0表示成功。 - 返回错误码(非零)表示失败,例如:
EINVAL: 条件变量或互斥锁无效。EPERM: 调用线程未拥有互斥锁。
使用注意
- 配合互斥锁: 条件变量必须和互斥锁一起使用,以避免竞争条件。
- 避免虚假唤醒: 在等待时,需要用循环检查条件是否满足,以确保线程确实因条件满足而被唤醒。
示例代码
下面是一个生产者和消费者模型的示例代码,展示如何使用 pthread_cond_wait:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int buffer = 0; // 缓冲区
int ready = 0; // 条件标志
void *producer(void *arg) {
pthread_mutex_lock(&mutex);
buffer = 42; // 生产数据
ready = 1;
printf("Producer: Produced data %d\n", buffer);
pthread_cond_signal(&cond); // 唤醒等待的消费者
pthread_mutex_unlock(&mutex);
return NULL;
}
void *consumer(void *arg) {
pthread_mutex_lock(&mutex);
while (!ready) { // 避免虚假唤醒
pthread_cond_wait(&cond, &mutex);
}
printf("Consumer: Consumed data %d\n", buffer);
ready = 0;
pthread_mutex_unlock(&mutex);
return NULL;
}
int main() {
pthread_t prod, cons;
pthread_create(&prod, NULL, producer, NULL);
pthread_create(&cons, NULL, consumer, NULL);
pthread_join(prod, NULL);
pthread_join(cons, NULL);
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);
return 0;
}
这个示例代码的正常执行流程可以总结为生产者线程与消费者线程的协作过程,它们通过条件变量和互斥锁实现同步。以下是执行流程的详细说明:
1. 主线程启动
- 主线程初始化了一个互斥锁 (
mutex) 和一个条件变量 (cond)。 - 主线程创建了两个线程:
- 生产者线程 (
producer):负责生产数据并通知消费者。 - 消费者线程 (
consumer):负责等待并消费数据。
- 生产者线程 (
2. 消费者线程执行
消费者线程优先执行(因为线程调度是由操作系统控制的,我们假设消费者线程先获得了 CPU 时间片):
-
锁定互斥锁:
- 调用
pthread_mutex_lock(&mutex)。 - 获得互斥锁,进入临界区。
- 调用
-
检查条件:
- 因为初始状态下
ready == 0,不满足条件。 - 调用
pthread_cond_wait(&cond, &mutex)进入等待状态,此时这个线程相当于处于暂停状态,哪怕里面有while循环,while循环也得暂停执行。
- 因为初始状态下
-
释放互斥锁:
pthread_cond_wait内部会自动释放互斥锁,并使线程进入等待状态,直到被唤醒。
3. 生产者线程执行
生产者线程开始执行:
-
锁定互斥锁:
- 调用
pthread_mutex_lock(&mutex)。 - 获得互斥锁,进入临界区。
- 调用
-
生产数据:
- 修改
buffer = 42。 - 设置
ready = 1,表示数据已准备好。 - 打印
"Producer: Produced data 42"。
- 修改
-
通知消费者:
- 调用
pthread_cond_signal(&cond),唤醒一个正在等待cond的线程(消费者线程)。
- 调用
-
释放互斥锁:
- 调用
pthread_mutex_unlock(&mutex),退出临界区。
- 调用
4. 消费者线程被唤醒
生产者线程发出信号后,消费者线程被唤醒,执行以下操作:
-
重新获取互斥锁:
pthread_cond_wait被唤醒时,自动尝试重新获取互斥锁。- 一旦获取到互斥锁,线程继续执行后续代码。
-
检查条件:
- 再次检查
ready == 1,条件已满足。
- 再次检查
-
消费数据:
- 打印
"Consumer: Consumed data 42"。 - 重置
ready = 0,表示数据已消费。
- 打印
-
释放互斥锁:
- 调用
pthread_mutex_unlock(&mutex),退出临界区。
- 调用
5. 主线程等待并销毁资源
- 主线程通过
pthread_join等待生产者和消费者线程完成。 - 销毁互斥锁和条件变量,释放系统资源。
整体流程图
-
消费者线程启动:
- 锁定互斥锁 → 检查条件
ready == 0→ 进入等待状态。
- 锁定互斥锁 → 检查条件
-
生产者线程启动:
- 锁定互斥锁 → 生产数据 → 修改
ready→ 发出信号 → 释放互斥锁。
- 锁定互斥锁 → 生产数据 → 修改
-
消费者线程被唤醒:
- 重新获取互斥锁 → 检查条件
ready == 1→ 消费数据 → 释放互斥锁。
- 重新获取互斥锁 → 检查条件
关键点
-
线程间同步:
- 条件变量和互斥锁确保了生产者和消费者不会同时操作共享资源,避免竞争条件。
-
避免虚假唤醒:
- 消费者线程在
pthread_cond_wait返回后再次检查条件,确保状态正确。
- 消费者线程在
-
线程调度:
- 操作系统决定哪个线程先执行,但不影响正确性。
输出示例
Producer: Produced data 42
Consumer: Consumed data 42
05-主函数也算一个线程,它的特点是什么?它与别的线程的关系又是什么?
main 函数运行在程序的主线程中。虽然它本身不显式地创建线程,但它是操作系统为程序启动时默认分配的线程。
理解主线程
-
线程的本质:
- 一个线程是程序中的一个执行路径。
- 当你运行一个程序时,操作系统会创建一个主线程,用于执行程序的
main函数。 - 主线程是程序生命周期的起点。
-
主线程与其他线程:
- 在多线程程序中,
main线程可以创建其他线程(如使用pthread_create)。 - 即使程序有多个线程,
main函数仍在主线程中运行,并可以与其他线程协作。
- 在多线程程序中,
-
挂起的影响:
- 当
main调用的函数被阻塞时时,主线程也会挂起,等待该函数返回。 - 其他线程仍然可以独立运行,不受主线程挂起的影响。
- 当
主线程与多线程程序的关系
main是整个程序的入口,也是主线程的执行代码。- 主线程挂起并不影响其他线程的执行;它只是当前路径上的代码暂停运行,其他线程会继续独立运行。
- 如果主线程结束(例如
main函数执行完毕且返回),整个程序将结束,除非你设置了某些线程为detached或手动阻止主线程的退出。
06-编译源代码时链接器要加上pthread库
相关Makefile语句如下:
LDFLAGS := -lpthread
07-如何查看线程ID?
在Linux系统中可以用下面这条命令来查询:
ps -T

上面这幅截图中,利用ps命令列出了系统中所有的线程,PID列表示进程号,SPID表示进程号。
注意:一个程序的主线程的SPID等于进程的PID。
另外,最后一列是进程的命令行属性,在Centos-Linux系统中,每个正在运行的进程都包含一个命令行属性,记录了启动该进程时所使用的命令行。
如果你的Linux使用的是BusyBox工具集,那ps命令不支持 -T 参数,ps命令不加 -T参数的话不会显示线程,也就是说BusyBox工具集不支持ps命令显示线程,可以像下面这样做,先用ps命令获得你要查询的程序的线程号:


上面的2180进程是我们想查询线程的程序,我们再运行下面的命可获得其线程号:
ls /proc/2180/task/
原理:在 Linux 系统中,每个进程的线程信息都可以在 /proc//task/ 目录中找到。


1059

被折叠的 条评论
为什么被折叠?



