以下是一个简单的线程不安全函数的示例,以及如何通过加锁机制将其变为线程安全的函数:
#include <stdio.h>
#include <pthread.h> int val = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; void* routine(void* arg) { int i = 0; while (i < 400) { // 加锁 pthread_mutex_lock(&lock); int tmp = val; printf("thread is %u; val is: %d\n", pthread_self(), val); val = tmp + 1; // 解锁 pthread_mutex_unlock(&lock); i++; } return NULL;
} int main() { pthread_t a1, a2; pthread_create(&a1, NULL, routine, NULL); pthread_create(&a2, NULL, routine, NULL); pthread_join(a1, NULL); pthread_join(a2, NULL); return 0;
}
在上面的示例中,val
是一个全局变量,被两个线程共享。原始的代码是线程不安全的,因为两个线程可能会同时访问和修改val
,导致数据不一致。通过添加互斥锁(pthread_mutex_lock
和pthread_mutex_unlock
),可以确保在某一时刻只有一个线程能够访问和修改val
,从而使函数变为线程安全的。
一、定义
如果一个函数在同一时刻可以被多个线程安全地调用,即函数被多个并发线程反复调用时,它会一直产生正确的结果,那么这个函数就被称为线程安全的。
二、实现方法
- 局部变量:在C语言中,局部变量是在栈中分配的,任何未使用静态数据或其他共享资源的函数都是线程安全的。
- 加锁机制:对于使用全局变量或静态数据的函数,由于这些数据是共享的,因此需要通过加锁的方式来使函数实现线程安全。加锁可以确保在某一时刻只有一个线程能够访问共享数据,从而避免数据冲突。
- 线程局部存储:将变量本地化,以便每个线程都有自己的私有数据。这些变量跨子例程和其他代码边界保留它们的值,并且是线程安全的,因为它们对于每个线程都是本地的。
- 不可变对象:对象的状态在构造后无法更改,这意味着仅共享只读数据和获得固有的线程安全性。
三、特性
- 数据一致性:线程安全的函数能够确保多个线程在访问共享数据时不会造成数据不一致或数据污染。
- 无竞争条件:线程安全的函数通常还包括防止或限制不同形式竞争条件风险的设计步骤。
- 优化并发性能:线程安全的设计通常会考虑最大化并发性能,尽管有时不能完全提供无死锁保证。
四、与可重入性的关系
-
可重入函数:可以在任意时刻被中断,稍后再继续运行而不会丢失数据的函数。可重入性解决函数运行结果的确定性和可重复性。
-
区别与联系:
- 一个函数对于多个线程是可重入的,则这个函数是线程安全的。
- 一个函数是线程安全的,但并不一定是可重入的。可重入性要强于线程安全性。
- 可重入函数是线程安全函数的一个真子集。也就是说,如果函数是可重入的,就可以保证它是线程安全的,但有些不可重入的函数也可能是线程安全的(如系统库函数)。