线程安全 详解

线程安全 详解 目录一、线程安全引例二、线程安全概念三、使用线程安全的函数-- strtok_r1. 线程安全性对比2. strtok_r 的工作原理3. 为什么 strtok_r 是线程安全的四、多线程中执行fork两个问题1.多线程中某个线程调用 fork()子进程会有和父进程相同数量的线程吗2.父进程被加锁的互斥锁 fork 后在子进程中是否已经加锁一、线程安全引例#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#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 替代 strtok1. 线程安全性对比2. strtok_r 的工作原理3. 为什么 strtok_r 是线程安全的每个线程都有自己的 ptr 指针保存在线程的栈空间分割状态不会互相干扰因为上下文信息是线程私有的避免了全局静态变量的使用程序执行流程主线程开始创建子线程两个线程并行执行子线程使用自己的ptr分割字母字符串主线程使用自己的ptr分割数字字符串交替输出由于sleep(1)两个线程交替执行线程同步主线程等待子线程结束程序退出四、多线程中执行fork两个问题1.多线程中某个线程调用 fork()子进程会有和父进程相同数量的线程吗答案不会。子进程只有一个线程即调用 fork() 的那个线程。#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 正在运行...父进程 PID1234, 仍然有三个线程子进程 PID1235, 只有调用fork的线程被复制子进程中的唯一线程正在运行...线程 1 正在运行... (父进程中的线程继续运行)线程 2 正在运行... (父进程中的线程继续运行)2.父进程被加锁的互斥锁 fork 后在子进程中是否已经加锁答案是的子进程会继承互斥锁的状态包括加锁状态。但这往往会导致问题示例说明#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() 等不安全函数总结