线程安全是什么?
线程安全(Thread Safety)指的是,当多个线程并发执行某段代码时,不会出现竞态条件(Race Condition)等问题,程序能够按照预期正确运行。
一个线程安全的函数,即使在多线程环境下被多个线程同时调用,也能确保其执行的正确性,不会因为共享资源的并发访问而引发错误。
线程安全的定义
- 线程安全的代码:能够在多线程环境下并发执行,且无需额外的同步手段(如锁),程序行为仍能保持一致和正确。
- 线程不安全的代码:在多线程环境下执行时,可能会出现竞态条件或数据不一致问题,需要依赖额外的同步机制来确保其执行的正确性。
线程安全与线程不安全的函数
线程安全的函数特征:
-
无共享数据:函数内部没有对全局变量、静态变量或其他共享资源的访问,或对这些资源的访问是只读的。
-
使用锁机制保护共享数据:对共享资源的写操作使用锁(如互斥锁
mutex
)来确保同一时刻只有一个线程能修改数据。 -
局部变量:函数只操作局部变量,或者动态分配内存的变量,这些变量是线程私有的,不存在并发冲突。
-
函数的行为独立:每次调用的结果只依赖于函数的参数和返回值,不依赖外部状态。
线程不安全的函数特征:
-
共享数据访问:函数直接访问或修改共享数据(如全局变量或静态变量)而没有保护措施。
-
非原子操作:执行多个操作步骤,而这些步骤不能保证在多线程环境下原子化完成。
-
使用不可重入的函数:这些函数在执行时会改变全局状态,不能被安全地并发调用。
常见的线程安全函数
以下是一些通常被认为是线程安全的函数和操作:
- 标准 I/O 函数(某些平台下):例如
printf
和scanf
,在某些现代系统中是线程安全的,因为它们在内部使用锁来保护共享资源(例如标准输出设备)。 - 纯函数(Pure Functions):这些函数没有副作用,其输出仅依赖于输入参数,不会改变外部状态,例如数学函数
sqrt()
、sin()
等。 - 局部变量操作的函数:函数只使用局部变量,局部变量在栈中分配,在线程之间不共享。
- C++11 标准库中的线程安全容器:C++11 之后的标准库引入了一些线程安全的工具,如
std::mutex
、std::atomic
,这些可以帮助实现线程安全的代码。
线程安全示例:
#include <cmath>
// 线程安全,因为没有全局状态,只依赖参数
double compute_square_root(double x) {
return std::sqrt(x);
}
常见的线程不安全函数
以下是一些通常被认为是线程不安全的函数或操作:
-
操作全局或静态变量的函数:如果函数在多个线程间共享全局或静态变量,且没有保护这些共享资源的机制(如锁),就容易出现数据竞争和不一致问题。
-
使用不可重入函数:不可重入函数会改变一些全局状态,导致多线程访问时出现问题。常见的如
strtok()
、asctime()
、gmtime()
等 C 标准库函数。 -
操作共享资源的函数:如果一个函数对共享资源(如文件、设备、数据库等)进行操作而不加以保护,这些操作就可能在多线程环境下导致冲突。
-
C 标准库中的一些不安全函数:如
gethostbyname()
、localtime()
等,它们使用了内部的静态变量,多个线程访问时可能导致数据错误。
线程不安全示例:
#include <cstring>
// 线程不安全,因为 str 保存为静态变量,可能被多个线程同时访问
char* unsafe_strtok(char* str, const char* delim) {
static char* saved_str;
if (str) {
saved_str = str;
}
// strtok 操作共享的 saved_str
return std::strtok(saved_str, delim);
}
如何保护线程不安全的函数
对于线程不安全的函数,如果想在多线程环境中安全使用,可以通过同步机制来保护。常用的保护方法有:
-
使用互斥锁(mutex):
- 对共享数据加锁,确保同一时刻只有一个线程能够访问这些资源。
#include <iostream> #include <mutex> std::mutex mtx; int shared_data = 0; void thread_safe_function() { std::lock_guard<std::mutex> lock(mtx); // 自动上锁和解锁 // 访问共享数据 shared_data++; std::cout << "Shared data: " << shared_data << std::endl; }
-
使用
std::atomic
:- 对基本数据类型如
int
、bool
使用原子操作,避免使用锁。
#include <atomic> std::atomic<int> atomic_counter(0); void atomic_increment() { atomic_counter++; }
- 对基本数据类型如
-
使用线程局部存储(Thread Local Storage, TLS):
- 将变量声明为线程局部变量,确保每个线程都有独立的副本。
thread_local int thread_local_var = 0; // 每个线程有独立的变量副本
常见的线程安全函数与不安全函数对比
线程安全函数 | 线程不安全函数 |
---|---|
std::atomic 相关操作 |
strtok |
std::mutex 、std::lock_guard |
asctime |
fwrite 、fread (多数平台上安全) |
gmtime |
snprintf |
strtok_r (部分平台上) |
reentrant 系列函数(如 gmtime_r ) |
gethostbyname |
总结
- 线程安全的函数可以在多线程环境下并发执行而不会导致不一致性或竞态条件。
- 线程不安全的函数通常会在多线程环境下修改共享状态或使用全局变量,可能导致数据冲突。
- 如果需要在多线程中使用线程不安全的函数,常用的同步机制如锁、原子操作和线程局部存储可以帮助确保线程安全。