目录
[三、使用线程安全的函数-- strtok_r](#三、使用线程安全的函数-- strtok_r)
[1. 线程安全性对比](#1. 线程安全性对比)
[2. strtok_r 的工作原理](#2. strtok_r 的工作原理)
[3. 为什么 strtok_r 是线程安全的?](#3. 为什么 strtok_r 是线程安全的?)
[1.多线程中某个线程调用 fork(),子进程会有和父进程相同数量的线程吗?](#1.多线程中某个线程调用 fork(),子进程会有和父进程相同数量的线程吗?)
[2.父进程被加锁的互斥锁 fork 后在子进程中是否已经加锁?](#2.父进程被加锁的互斥锁 fork 后在子进程中是否已经加锁?)
一、线程安全引例
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <pthread.h>
/**
* 线程函数 - 由新创建的线程执行
* 功能:将一个包含字母的字符串按空格分割并逐个打印
* 参数:void *arg - 线程参数(本例中没有使用)
* 返回:void* - 线程退出码(本例中返回NULL)
*/
void* thread_fun(void *arg)
{
// 定义包含字母的字符串数组,用空格分隔
char buff[128] = {"a b c d e f g h w q"};
// strtok函数:第一次调用,分割字符串
// 第一个参数传入要分割的字符串,第二个参数指定分隔符(空格)
char *s = strtok(buff, " ");
// 循环处理每个分割后的单词
while(s != NULL) // 当还有单词时继续循环
{
// 打印当前分割出的字母
printf("thread:s=%s\n", s);
// 休眠1秒,模拟耗时操作,让线程调度更明显
sleep(1);
// 继续分割下一个单词
// strtok第二次及后续调用时,第一个参数传NULL,表示继续处理上次的字符串
s = strtok(NULL, " ");
}
// 线程函数结束,返回NULL
return NULL;
}
/**
* 主函数 - 程序入口
* 功能:创建线程,同时主线程自己也处理一个数字字符串
*/
int main()
{
pthread_t id; // 定义线程ID变量,用于存储新创建的线程标识符
// 创建新线程
// 参数1:线程ID指针,用于接收创建的线程ID
// 参数2:线程属性,NULL表示使用默认属性
// 参数3:线程函数指针,指定线程要执行的函数
// 参数4:传递给线程函数的参数,NULL表示不传递参数
pthread_create(&id, NULL, thread_fun, NULL);
// 主线程自己的数据:包含数字的字符串,用空格分隔
char str[128] = {"1 2 3 4 5 6 7 8 9 10"};
// 主线程也开始分割自己的字符串
char *s = strtok(str, " ");
// 主线程循环处理自己的数字
while(s != NULL)
{
// 打印主线程分割出的数字
printf("main:%s\n", s);
// 休眠1秒,让两个线程交替执行
sleep(1);
// 继续分割下一个数字
s = strtok(NULL, " ");
}
// 等待子线程结束
// 参数:要等待的线程ID,第二个参数为NULL表示不关心线程的返回值
// 如果不等待,主线程结束后进程会立即终止,子线程可能无法完成执行
pthread_join(id, NULL);
// 正常退出程序
exit(0);
}
程序执行流程:
-
程序启动:main函数开始执行
-
创建线程 :调用
pthread_create()创建子线程 -
并发执行:
-
主线程处理数字字符串"1 2 3 4 5 6 7 8 9 10"
-
子线程处理字母字符串"a b c d e f g h w q"
-
-
交替输出:两个线程都会休眠1秒,导致输出交替进行
-
线程同步 :主线程调用
pthread_join()等待子线程结束 -
程序退出:所有线程执行完毕,程序正常退出
因为strtok不是线程安全的。内部使用了全局变量(静态变量) 。也就是说,只要使用了全局变量或者静态变量的函数都不能在多线程中使用,这些函数都不是线程安全的.
二、线程安全概念
当多个线程同时访问一个共享资源 (如对象、变量、文件等)时,如果不需要额外的同步措施,程序仍然能够正确地执行 ,不会出现数据不一致或其他不可预知的结果,那么我们就说这个资源是线程安全的。
Brian Goetz 在《Java并发编程实战》中给出了更精确的定义:
当多个线程访问某个类时,这个类始终表现出正确的行为,那么就称这个类是线程安全的。
要保证线程安全需要做到:
- 对线程同步,保证同一时刻只有一个线程访问临界资源.
- 在多线程中使用线程安全的函数(可重入函数).
所谓线程安全的函数 指的是:如果一个函数能被多个线程同时调用且不发生竟态条件,则我们称它是线程安全的。
**三、使用线程安全的函数--**strtok_r
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <pthread.h>
/**
* 线程函数 - 由新创建的线程执行
* 功能:使用线程安全的strtok_r函数分割字母字符串
* 参数:void *arg - 线程参数(本例中没有使用)
* 返回:void* - 线程退出码
*/
void *thread_fun(void *arg)
{
// 定义包含字母的字符串数组,用空格分隔
char buff[128] = {"a b c d e f g h w q"};
// 定义指针变量ptr,用于保存strtok_r的上下文信息
// 每个线程拥有自己的ptr指针,实现线程安全
char *ptr = NULL;
// strtok_r函数:线程安全的字符串分割函数
// 参数1:要分割的字符串(第一次调用时传入)
// 参数2:分隔符(空格)
// 参数3:保存上下文的指针,用于后续继续分割
// 返回值:当前分割出的字符串片段
char *s = strtok_r(buff, " ", &ptr);
// 循环处理每个分割后的单词
while(s != NULL) // 当还有单词时继续循环
{
// 打印当前分割出的字母
printf("thread:s=%s\n", s);
// 休眠1秒,让线程调度更明显
sleep(1);
// 继续分割下一个单词
// 第一次参数传NULL,表示继续处理上次的字符串
// 传入同一个ptr指针,保持分割状态
s = strtok_r(NULL, " ", &ptr);
}
return NULL; // 线程结束
}
/**
* 主函数 - 程序入口
* 功能:创建线程,使用strtok_r分割数字字符串
*/
int main()
{
pthread_t id; // 定义线程ID变量
// 创建新线程执行thread_fun函数
// 参数1:线程ID指针,用于存储创建的线程ID
// 参数2:线程属性,NULL表示使用默认属性
// 参数3:线程函数指针
// 参数4:传递给线程函数的参数,NULL表示不传递参数
pthread_create(&id, NULL, thread_fun, NULL);
// 主线程的数据:包含数字的字符串,用空格分隔
char str[128] = {"1 2 3 4 5 6 7 8 9 10"};
// 主线程自己的ptr指针,用于保存分割状态
// 与子线程的ptr指针相互独立,互不干扰
char *ptr = NULL;
// 主线程开始分割自己的字符串
char *s = strtok_r(str, " ", &ptr);
// 主线程循环处理自己的数字
while(s != NULL)
{
// 打印主线程分割出的数字
printf("main:%s\n", s);
// 休眠1秒,让两个线程交替执行
sleep(1);
// 继续分割下一个数字
s = strtok_r(NULL, " ", &ptr);
}
// 等待子线程结束
// 如果不等待,主线程结束后进程会立即终止
pthread_join(id, NULL);
exit(0); // 正常退出程序
}
关键改进点:使用 strtok_r 替代 strtok
1. 线程安全性对比

2. strtok_r 的工作原理

3. 为什么 strtok_r 是线程安全的?
- 每个线程都有自己的 ptr 指针,保存在线程的栈空间
- 分割状态不会互相干扰,因为上下文信息是线程私有的
- 避免了全局静态变量的使用
程序执行流程:
-
主线程开始:创建子线程
-
两个线程并行执行:
-
子线程:使用自己的
ptr分割字母字符串 -
主线程:使用自己的
ptr分割数字字符串
-
-
交替输出 :由于
sleep(1),两个线程交替执行 -
线程同步:主线程等待子线程结束
-
程序退出
四、多线程中执行fork(两个问题)
1.多线程中某个线程调用 fork(),子进程会有和父进程相同数量的线程吗?
答案:不会。子进程只有一个线程,即调用 fork() 的那个线程。
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>
void *thread_func(void *arg) {
int thread_num = *(int*)arg;
while(1) {
printf("线程 %d 正在运行...\n", thread_num);
sleep(1);
}
return NULL;
}
int main() {
pthread_t tid1, tid2;
int num1 = 1, num2 = 2;
// 创建两个线程
pthread_create(&tid1, NULL, thread_func, &num1);
pthread_create(&tid2, NULL, thread_func, &num2);
sleep(2); // 让两个线程运行一会儿
pid_t pid = fork(); // 主线程调用 fork()
if (pid == 0) {
// 子进程
printf("子进程 PID=%d, 只有调用fork的线程被复制\n", getpid());
printf("子进程中的其他线程没有被复制!\n");
// 这里只能看到调用 fork 的线程
while(1) {
printf("子进程中的唯一线程正在运行...\n");
sleep(1);
}
} else {
// 父进程
printf("父进程 PID=%d, 仍然有三个线程\n", getpid());
wait(NULL); // 等待子进程结束
}
return 0;
}
执行结果:
线程 1 正在运行...
线程 2 正在运行...
线程 1 正在运行...
线程 2 正在运行...
父进程 PID=1234, 仍然有三个线程
子进程 PID=1235, 只有调用fork的线程被复制
子进程中的唯一线程正在运行...
线程 1 正在运行... (父进程中的线程继续运行)
线程 2 正在运行... (父进程中的线程继续运行)
2.父进程被加锁的互斥锁 fork 后在子进程中是否已经加锁?
答案:是的,子进程会继承互斥锁的状态,包括加锁状态。但这往往会导致问题!
示例说明:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#include <sys/wait.h>
pthread_mutex_t mutex;
void *thread_func(void *arg) {
// 线程1加锁互斥量
pthread_mutex_lock(&mutex);
printf("线程1: 已获得锁,准备 fork...\n");
pid_t pid = fork();
if (pid == 0) {
// 子进程
printf("子进程: 尝试加锁...\n");
// 这里会死锁!因为互斥量在子进程中仍然处于加锁状态
// 但没有任何线程可以解锁它(解锁线程在子进程中不存在)
pthread_mutex_lock(&mutex); // 死锁!
printf("子进程: 获得锁\n"); // 永远不会执行
pthread_mutex_unlock(&mutex);
exit(0);
} else {
// 父进程
printf("父进程: 解锁\n");
pthread_mutex_unlock(&mutex);
wait(NULL);
}
return NULL;
}
int main() {
pthread_t tid;
// 初始化互斥锁
pthread_mutex_init(&mutex, NULL);
// 创建线程
pthread_create(&tid, NULL, thread_func, NULL);
pthread_join(tid, NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
问题分析:
-
子进程继承锁状态:
-
当 fork() 时,子进程复制了父进程的整个内存空间
-
包括互斥锁的状态(加锁/解锁)
-
但只复制了调用 fork() 的线程
-
-
导致的问题:
-
如果 fork() 时某个互斥锁被其他线程锁定
-
子进程中这个互斥锁仍然处于锁定状态
-
但锁定它的线程在子进程中不存在
-
导致子进程中永远无法解锁这个互斥锁
-
最佳实践建议:
-
避免在多线程程序中调用 fork(),除非立即调用 exec()
-
如果必须使用 fork():
-
在 fork() 前确保所有互斥锁都是解锁状态
-
使用
pthread_atfork()注册处理函数 -
子进程中尽快调用 exec() 替换地址空间
-
-
异步信号安全函数:
-
fork() 处理程序中只能调用异步信号安全的函数
-
避免在 atfork 处理函数中调用 printf() 等不安全函数
-
总结:
