目录
一、什么是线程安全?
线程安全是指在多线程环境下,某个函数、类、方法或数据结构能够正确地处理并响应各个线程的并发访问,而不会造成数据的混乱或破坏。一个线程安全的程序在并发情况下能够保持数据的一致性和稳定性。实现线程安全通常需要采用同步机制,如互斥锁、信号量、读写锁等来保护共享数据的访问。
简而言之,无论程序的调度顺序如何,都能得到正确的结果,这时就叫做线程安全。
二、为什么需要线程安全?
在多线程编程中,如果多个线程同时访问共享的资源(如变量、数据结构等),就可能会引发一些问题,例如竞态条件(Race Condition)、死锁(Deadlock)、数据不一致等。线程安全的概念是指在多线程环境中,能够确保共享资源在被多个线程同时访问时不会出现上述问题,保证程序的正确性和稳定性。
因此,为了确保在多线程环境中程序的正确性和稳定性,需要实现线程安全的操作。常见的实现线程安全的方法包括使用互斥锁(Mutex)、信号量(Semaphore)、使用原子操作等。通过这些方法可以避免多线程同时访问共享资源导致的问题,确保程序在多线程环境下的正确性和稳定性。
三、如何实现线程安全?
实现线程安全的方法主要包括以下几种:
-
互斥锁(Mutex):使用互斥锁可以确保在同一时刻只有一个线程可以访问共享资源,其他线程需要等待。线程在访问共享资源前会先尝试获取互斥锁,如果获取失败就会被阻塞,直到互斥锁被释放。一旦线程访问结束,释放互斥锁,其他线程就可以获取互斥锁继续访问共享资源。
-
信号量(Semaphore):信号量是一种更加通用的同步机制,可以控制多个线程的并发访问数量。通过设置信号量的值,限制可以同时访问共享资源的线程数量。
-
原子操作:原子操作是不可中断的操作,确保了操作的完整性。在多线程环境下,原子操作可以避免竞态条件的发生。
-
读写锁(ReadWrite Lock):读写锁允许多个线程同时读取共享资源,但在有线程要写入共享资源时,所有的读取操作会被阻塞,提高了读取的效率。
-
条件变量(Condition Variables):允许线程在等待特定条件满足时被唤醒。通常与互斥锁一起使用。
-
使用线程安全的数据结构:某些编程语言或库提供了线程安全的数据结构,例如Java中的ConcurrentHashMap、C++中的std::mutex等,可以直接使用这些线程安全数据结构来避免并发访问问题。
通过以上方法的结合使用,可以有效实现线程安全,避免多线程环境下出现竞态条件、死锁和数据不一致等问题。
四、线程安全函数
4.1 strtok() 函数
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
void* fun(void* arg)
{
char buff[]="a b c d e f";
char* s=strtok(buff," ");
while(s!=NULL)
{
printf("fun s=%s\n",s);
sleep(1);
s=strtok(NULL," ");
}
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,fun,NULL);
char buff[]="1 2 3 4 5 6";
char* s=strtok(buff," ");
while(s!=NULL)
{
printf("main s=%s\n",s);
sleep(1);
s=strtok(NULL," ");
}
pthread_join(id,NULL);
exit(0);
}
运行结果:
线程分割abcd 主线程分割1 2 3 4
使用 strtok 函数来分割 buff。strtok 函数的第一个参数是要分割的字符串,第二个参数是分隔符。strtok 函数会继续从上次停止的地方开始分割字符串,直到没有更多的分隔符。
线程启动后给strtok传的是abcd 所以之后不管是主函数或者其他函数打印都会是abcd
我们期望可以打印出1 2 3 4 5 6 和a b c d e f
得出结论 strtok函数不能在多线程中使用 内部有一个指针记录分割到哪里,但是strtok函数只有一个记录的 后传进来的会把前面传入的覆盖掉
4.2 strtok_r() 函数
char *strtok_r(char *str, const char *delim, char **saveptr);
函数参数:
str
: 要分解的字符串,第一次调用时传入要被分解的字符串,之后传入NULL。delim
: 分隔符,用于指定分解字符串的分隔符。saveptr
: 用于保存每次调用后的状态,需要传递指向指针的指针。在第一次调用时,传入一个指向NULL指针的指针,后续调用时保持不变。这个指针保存了strtok_r
在上一次调用后剩余的未处理部分,以便下一次调用时继续处理。
strtok_r
函数在第一次调用时,在str
中找到第一个不包含delim
中任何字符的子字符串,并返回指向该子字符串的指针。之后每次调用时,将从上一次返回的位置开始,继续查找下一个符合条件的子字符串。当没有符合条件的子字符串时,返回NULL。这个函数是线程安全版本的
strtok
函数,相比strtok
多了一个额外的参数saveptr
,可以在多线程环境下避免竞态条件,因为每个线程都有自己独立的状态保存。
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<pthread.h>
void* fun(void* arg)
{
char buff[]="a b c d e f";
char* ptr=NULL;
char* s=strtok_r(buff," ",&ptr);
while(s!=NULL)
{
printf("fun s=%s\n",s);
sleep(1);
s=strtok_r(NULL," ",&ptr);
}
}
int main()
{
pthread_t id;
pthread_create(&id,NULL,fun,NULL);
char buff[]="1 2 3 4 5 6";
char* ptr=NULL;
char* s=strtok_r(buff," ",&ptr);
while(s!=NULL)
{
printf("main s=%s\n",s);
sleep(1);
s=strtok_r(NULL," ",&ptr);
}
pthread_join(id,NULL);
exit(0);
}
运行结果:
线程安全的版本 加入各自线程用各自线程的记录指针,不让覆盖掉出现冲突
strtok_r 是线程安全的,它允许在多线程环境中安全地分割字符串,因为它不使用静态缓冲区来存储状态,通过一个额外的参数来保存上次分割的位置。相反,其他 strtok 函数在处理多个字符串时可能会导致问题,因为它们使用静态缓冲区来存储状态。这意味着在多线程环境中,strtok 可能会导致竞争条件和未定义行为,即线程是不安全的。
4.3 strtok_r()函数+fork()
输出显示了进程ID(PID)和字符串分割的结果。
pid=22494
表示父进程的PID,pid=222496
表示子进程的PID。线程的PID与父进程相同,因为它是父进程创建的线程。