在 SoC上实现线程安全是多线程编程中非常关键的一个问题,特别是当涉及资源共享时。
线程安全的设计目的是避免多个线程同时访问共享资源时出现竞争条件、数据竞争等问题。
1
线程栈(Thread Stack)
在多线程系统中,每个线程都有自己独立的栈,用于存储局部变量、函数调用的返回地址等。
栈的独立性是线程安全的基础之一,因为栈是线程私有的,不会与其他线程共享。
因此,所有存储在栈上的数据,比如局部变量、函数参数等,默认是线程安全的。
细节分析:
栈的分配:SoC 上的多核处理器或多线程操作系统(如 Linux、RTOS)为每个线程分配独立的栈。在 Cortex-M 系列的 ARM 处理器上,栈指针(SP)用于跟踪每个线程的栈顶位置。每个线程的栈是从操作系统堆栈池或系统内存中分配的。
中断和线程栈:在实时操作系统中,如果在中断期间发生线程切换,处理器需要保存当前线程的栈指针,并加载下一个线程的栈指针。这一过程称为“上下文切换”,它依赖于栈的独立性以确保线程安全。
以下示例中local_var 是局部变量,位于栈中,因此不同线程各自拥有自己的 local_var,不会造成资源竞争。
void thread_function() {
int local_var = 0; // 局部变量保存在当前线程的栈中
local_var += 1;
printf("Thread local variable: %d\n", local_var);
}
2
可重入函数(Reentrant Function)
可重入函数是指多个线程可以同时调用而不会引发竞争条件的函数。
为了实现可重入,函数必须:
不使用静态数据(除非有线程安全的保护机制)。
不依赖共享资源,或者对共享资源进行保护(如通过锁定机制)。
不返回指向静态或全局数据的指针。
细节分析:
静态变量的危险性:静态变量在全局内存中分配,所有线程共享该变量,因此多个线程同时访问或修改静态变量时会导致数据竞争。如果需要使用静态数据,必须通过锁或其他同步机制来保护它。
递归函数问题:递归函数需要特别注意,如果递归函数使用全局或静态数据,那么它在不同线程递归调用时就会出现问题。
以下示例中non_reentrant_function 中的 static_var 是共享的,多个线程访问时会产生竞态条件;而 reentrant_function 仅使用局部变量,是线程安全的。
int non_reentrant_function(int a) {
static int static_var = 0; // 静态变量导致线程不安全
static_var += a;
return static_var;
}
int reentrant_function(int a, int b) {
return a + b; // 不使用任何共享资源,线程安全
}
3
线程安全函数(Thread-Safe Function)
为了让函数在并发环境下是线程安全的,常用的方式是使用同步机制来保护共享资源。
典型的同步机制包括:
互斥锁(Mutex):防止多个线程同时访问共享资源。
读写锁(Read-Write Lock):允许多个线程同时读,但写操作是排他的。
信号量(Semaphore):控制访问共享资源的线程数量。
自旋锁(Spinlock):当需要保护的代码执行时间很短时,使用自旋锁可以避免线程休眠,提高效率。
细节分析:
互斥锁的开销:在资源竞争较少的情况下,互斥锁可能会导致性能下降,特别是在 SoC 系统上,因为线程切换和上下文切换的开销较大。在嵌入式系统中,自旋锁有时会被用来代替互斥锁,以减少因线程休眠带来的额外开销。
自旋锁的使用场景:自旋锁适用于在多核处理器上,线程在短时间内可以完成任务而不需要休眠的场景。自旋锁会让线程忙等待,直到获取锁,但会消耗 CPU 资源。
以下示例通过 pthread_mutex_lock 和 pthread_mutex_unlock 对共享变量 shared_resource 进行加锁和解锁操作,确保只有一个线程可以在同一时间修改 shared_resource。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int shared_resource = 0;
void* thread_safe_function(void* arg) {
pthread_mutex_lock(&mutex); // 加锁保护共享资源
shared_resource += 1;
printf("Shared resource: %d\n", shared_resource);
pthread_mutex_unlock(&mutex); // 解锁
return NULL;
}
4
一次性初始化(One-Time Initialization)
在多线程环境中,有时某个共享资源或全局变量只需要初始化一次,比如一个全局配置或共享对象。
为了确保初始化操作的线程安全,可以使用 pthread_once 来保证只初始化一次。
细节分析:
双重检查锁定(Double-Checked Locking):为了减少加锁开销,有时会使用双重检查锁定,先检查是否已经初始化,如果没有再加锁并初始化。双重检查锁定是为了避免不必要的锁定开销,特别是在初始化后频繁读取全局变量的场景中。
内存屏障问题:在某些多核处理器上,编译器和 CPU 可能会重排指令,导致初始化操作和赋值顺序不一致。为了解决这个问题,内存屏障可以确保在多核系统中操作的顺序一致。
以下示例pthread_once 确保无论有多少个线程,initialize_resource 函数都只会被调用一次,保证了线程安全的初始化。
pthread_once_t once_control = PTHREAD_ONCE_INIT;
void initialize_resource() {
printf("Resource initialized.\n");
}
void* thread_function(void* arg) {
pthread_once(&once_control, initialize_resource); // 保证初始化只发生一次
printf("Thread is running.\n");
return NULL;
}
5
线程特有数据(Thread-Specific Data)
线程特有数据(Thread-Specific Data, TSD)允许每个线程拥有自己的数据副本,而这些数据不会被其他线程访问。
可以使用 pthread_key_create、pthread_setspecific 和 pthread_getspecific 来管理线程特有数据。
每个线程都有独立的存储空间来保存它的特有数据,因此不会引发竞态条件。
细节分析:
线程局部存储的工作原理:在实现上,TSD 通常使用哈希表或数组进行存储,每个线程都有自己的独立副本,操作系统或线程库负责在上下文切换时保存和恢复线程的特有数据。
销毁线程特有数据:当线程退出时,应该确保特有数据得到适当销毁。这通常可以通过设置销毁函数(destructor)来自动释放资源。
以下示例每个线程都通过 pthread_setspecific 设置自己的特有数据,并通过 pthread_getspecific 获取数据,保证了每个线程的数据是独立的,线程安全。
pthread_key_t tls_key;
void destructor(void* value) {
free(value); // 销毁线程特定的数据
}
void* thread_function(void* arg) {
int* thread_data = (int*)malloc(sizeof(int));
*thread_data = 1;
pthread_setspecific(tls_key, thread_data); // 设置当前线程的特有数据
printf("Thread-specific data: %d\n", *(int*)pthread_getspecific(tls_key));
return NULL;
}
int main() {
pthread_key_create(&tls_key, destructor); // 创建 TLS key 并设置销毁函数
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, thread_function, NULL);
pthread_create(&thread2, NULL, thread_function, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_key_delete(tls_key); // 删除 TLS key
return 0;
}
6
线程局部存储(Thread Local Storage, TLS)
线程局部存储是一种更方便的方式,允许每个线程有一组私有的全局变量副本,避免多个线程之间的共享。
大多数编译器提供了内置支持,例如在 GCC 中,__thread 关键字可以用于声明线程局部变量。
细节分析:
TLS 的实现机制:在 SoC 系统或嵌入式操作系统中,TLS 通常通过 CPU 的上下文切换机制来实现。每个线程的上下文不仅包括寄存器,还包括 TLS 的数据段。TLS 的数据结构通常存储在线程控制块(Thread Control Block, TCB)中,每个线程拥有独立的 TCB。
以下示例__thread 关键字声明的 tls_var 是线程局部变量,每个线程都有自己的副本,因此是线程安全的。
__thread int tls_var = 0;
void* thread_function(void* arg) {
tls_var += 1;
printf("TLS variable: %d\n", tls_var);
return NULL;
}
SoC 上的线程安全设计需要在多个层面加以考虑,从栈隔离到复杂的同步机制(如互斥锁、信号量等),不同机制有不同的应用场景。
线程特有数据和 TLS 提供了更灵活的数据管理方式,使得每个线程能够独立存储和访问它的特有数据,避免数据竞争问题。