线程安全 详解

目录

一、线程安全引例

二、线程安全概念

[三、使用线程安全的函数-- strtok_r](#三、使用线程安全的函数-- strtok_r)

[1. 线程安全性对比](#1. 线程安全性对比)

[2. strtok_r 的工作原理](#2. strtok_r 的工作原理)

[3. 为什么 strtok_r 是线程安全的?](#3. 为什么 strtok_r 是线程安全的?)

四、多线程中执行fork(两个问题)

[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);
}

程序执行流程:

  1. 程序启动:main函数开始执行

  2. 创建线程 :调用pthread_create()创建子线程

  3. 并发执行

    • 主线程处理数字字符串"1 2 3 4 5 6 7 8 9 10"

    • 子线程处理字母字符串"a b c d e f g h w q"

  4. 交替输出:两个线程都会休眠1秒,导致输出交替进行

  5. 线程同步 :主线程调用pthread_join()等待子线程结束

  6. 程序退出:所有线程执行完毕,程序正常退出

因为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 指针,保存在线程的栈空间
  • 分割状态不会互相干扰,因为上下文信息是线程私有的
  • 避免了全局静态变量的使用

程序执行流程:

  1. 主线程开始:创建子线程

  2. 两个线程并行执行

    • 子线程:使用自己的 ptr 分割字母字符串

    • 主线程:使用自己的 ptr 分割数字字符串

  3. 交替输出 :由于 sleep(1),两个线程交替执行

  4. 线程同步:主线程等待子线程结束

  5. 程序退出

四、多线程中执行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;
}

问题分析:

  1. 子进程继承锁状态

    • 当 fork() 时,子进程复制了父进程的整个内存空间

    • 包括互斥锁的状态(加锁/解锁)

    • 但只复制了调用 fork() 的线程

  2. 导致的问题

    • 如果 fork() 时某个互斥锁被其他线程锁定

    • 子进程中这个互斥锁仍然处于锁定状态

    • 但锁定它的线程在子进程中不存在

    • 导致子进程中永远无法解锁这个互斥锁

最佳实践建议:

  1. 避免在多线程程序中调用 fork(),除非立即调用 exec()

  2. 如果必须使用 fork()

    • 在 fork() 前确保所有互斥锁都是解锁状态

    • 使用 pthread_atfork() 注册处理函数

    • 子进程中尽快调用 exec() 替换地址空间

  3. 异步信号安全函数

    • fork() 处理程序中只能调用异步信号安全的函数

    • 避免在 atfork 处理函数中调用 printf() 等不安全函数

总结:

相关推荐
cuguanren2 小时前
MuleRun vs OpenClaw vs 网页服务:云端安全与本地自由的取舍之道
安全·大模型·llm·agent·智能体·openclaw·mulerun
cramer_50h2 小时前
我的 网络安全资产暴露/攻击面管理系统
安全·web安全
Chengbei112 小时前
Chrome浏览器渗透利器支持原生扫描!JS 端点 + 敏感目录 + 原型污染自动化检测|VulnRadar
javascript·chrome·安全·web安全·网络安全·自动化·系统安全
hzhsec3 小时前
AI Security Agent:用自然语言做安全巡检,AI 自己跑命令
人工智能·安全·运维开发·ai编程
yuuki2332333 小时前
【Linux】开发工具链全解析:从 apt 到 gdb
linux·运维·服务器
木禾ali0th3 小时前
告别大模型“裸奔”:开源项目 ClawVault 架构与核心能力解析
算法·安全
wangjialelele3 小时前
C++11、C++14、C++17、C++20新特性解析(一)
linux·c语言·开发语言·c++·c++20·visual studio
²º²²এ松4 小时前
vs code连接ubuntu esp项目
linux·数据库·ubuntu
浪客灿心4 小时前
Linux进程信号
linux