在 Linux
操作系统的多线程编程领域中,合理的同步机制是确保数据一致性和线程安全的关键。自旋锁(Spin Lock
)作为一种轻量级的同步手段,在特定的应用场景下,提供了与传统互斥锁不同的性能优势。本文将深入探讨自旋锁的内部原理、实现方式,并结合 C
语言案例,展示其在实际编程中的应用。
自旋锁是一种特殊的锁机制,它不会使线程让出 CPU
给其他线程,进入阻塞状态等待锁的释放,而是让线程在获取锁失败时持续进行“自旋”尝试,即不断循环检查锁的状态,直到成功获取锁或达到一定的尝试次数。这种机制的核心思想是通过快速尝试获取锁来减少线程切换的开销,提高并发性能。
自旋锁适用于短时间内的竞争情况,因为长时间无法获取锁时,线程会持续忙等,消耗 CPU
资源。因此,在设计自旋锁时通常会设置一个最大尝试次数,超过该次数后线程会放弃自旋,选择其他策略(如阻塞)等待锁的释放。
自旋锁的基本原理相对简洁直观。当一个线程尝试获取一个已经被其他线程占用的自旋锁时,该线程不会像传统的锁那样立即进入阻塞状态并等待被唤醒,而是会在一个紧凑的循环中持续不断地尝试获取该锁。这种持续尝试的行为就如同线程在“自旋”,故而得名自旋锁。
CPU
资源,因为它通过执行无用的任务(如空循环)来保持活动状态。CPU
资源,一些自旋锁实现会包含一个参数来限定最多持续尝试的次数。超出该次数后,自旋锁会放弃当前时间片,等待下一次机会。CPU
资源。CPU
系统中,当多个线程需要短暂地访问共享资源时。CPU
系统上,自旋锁通常没有优势,因为它会阻止其他线程运行,而锁又不会被其他线程释放。在 Linux
内核中,自旋锁的实现通常依赖于底层的原子操作来保证其高效性和正确性。原子操作是指在执行过程中不会被中断或干扰的操作,确保了对锁状态的操作是完整和一致的。
POSIX
自旋锁(spinlock
)的函数集,它们定义在
头文件中。POSIX
自旋锁是一种用于同步线程的低级机制,当线程试图获取一个已经被其他线程持有的锁时,它会“自旋”(即忙等待)而不是被阻塞(进入睡眠状态)。这适用于锁持有时间较短的场景,以避免线程切换的开销。
以下是POSIX
自旋锁(spinlock
)函数集的清晰归纳和详细描述:
pthread_spin_init
初始化一个自旋锁。
int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
返回值:成功时返回 0
,失败时返回错误码。
lock
:指向要初始化的自旋锁的指针。pshared
:控制锁是在进程内共享(PTHREAD_PROCESS_PRIVATE
)还是在多个进程间共享(PTHREAD_PROCESS_SHARED
)。在多进程共享时,需要确保所有进程都可以访问该锁。pthread_spin_destroy
销毁一个自旋锁。
int pthread_spin_destroy(pthread_spinlock_t *lock);
返回值:成功时返回 0
,失败时返回错误码。
lock
:指向要销毁的自旋锁的指针。pthread_spin_lock
尝试获取一个自旋锁。如果锁已经被其他线程持有,则调用线程会忙等待,直到锁变得可用。
int pthread_spin_lock(pthread_spinlock_t *lock);
返回值:成功时返回 0
,失败时返回错误码。
lock
:指向要获取的自旋锁的指针。pthread_spin_trylock
尝试获取一个自旋锁,但不会阻塞。如果锁已经被其他线程持有,则立即返回错误。
int pthread_spin_trylock(pthread_spinlock_t *lock);
返回值:如果成功获取锁则返回 0
,如果锁已经被其他线程持有则返回 EBUSY
,失败时返回其他错误码。
lock
:指向要尝试获取的自旋锁的指针。pthread_spin_unlock
释放一个自旋锁,允许其他线程获取它。
int pthread_spin_unlock(pthread_spinlock_t *lock);
返回值:成功时返回 0
,失败时返回错误码。
lock
:指向要释放的自旋锁的指针。在使用这些函数时,要注意处理可能出现的错误情况,并且确保在所有获取了自旋锁的代码路径中都有相应的解锁操作,以避免死锁。此外,由于自旋锁在无法获取锁时会持续消耗 CPU
资源,因此它们通常只适用于锁持有时间非常短的场景。如果锁可能被持有较长时间,使用互斥锁(mutexes
)或条件变量(condition variables
)等机制可能更为合适。
下面是一个简单的 C
语言实现自旋锁加解锁的示例代码:
#include
#include
#include
// 定义一个自旋锁结构体
typedef struct {
pthread_spinlock_t lock;
} SpinLock;
// 初始化自旋锁
void spin_lock_init(SpinLock *lock) {
pthread_spin_init(&lock->lock, PTHREAD_PROCESS_PRIVATE);
}
// 销毁自旋锁
void spin_lock_destroy(SpinLock *lock) {
pthread_spin_destroy(&lock->lock);
}
// 加锁
void spin_lock(SpinLock *lock) {
pthread_spin_lock(&lock->lock);
}
// 解锁
void spin_unlock(SpinLock *lock) {
pthread_spin_unlock(&lock->lock);
}
// 共享变量
volatile int shared_var = 0;
// 线程函数,演示自旋锁的使用
void *thread_func(void *arg) {
SpinLock *lock = (SpinLock *)arg;
// 假设每个线程将共享变量增加 1000 次
for (int i = 0; i < 1000; ++i) {
// 加锁
spin_lock(lock);
// 在此执行需要保护的代码
shared_var++; // 修改共享变量
// 解锁
spin_unlock(lock);
}
// 每个线程结束后可以输出一个消息
printf("Thread %ld finished incrementing shared_var.\n", (long)pthread_self());
return NULL;
}
int main() {
pthread_t t1, t2;
SpinLock lock;
// 初始化自旋锁
spin_lock_init(&lock);
// 创建两个线程
pthread_create(&t1, NULL, thread_func, &lock);
pthread_create(&t2, NULL, thread_func, &lock);
// 等待线程结束
pthread_join(t1, NULL);
pthread_join(t2, NULL);
// 输出最终的共享变量值
printf("Final value of shared_var: %d\n", shared_var);
// 销毁自旋锁
spin_lock_destroy(&lock);
return 0;
}
编译并运行程序,结果如下:
[root@localhost spinlock]# gcc my_own.c -o my_own -lpthread
[root@localhost spinlock]# ./my_own
Thread 139781180962560 finished incrementing shareValue
Thread 139781172569856 finished incrementing shareValue
Final shareValue is: 2000
上面的代码是一个使用 POSIX
自旋锁(pthread_spinlock_t
)来保护共享资源(一个全局整数变量shared_var
)的示例。我将为您逐步解析这段代码:
#include
#include
#include
这里引入了三个头文件:stdio.h
用于输入输出功能,pthread.h
用于 POSIX
线程(也称为 pthreads
)的 API
,stdbool.h
用于布尔数据类型。
typedef struct {
pthread_spinlock_t lock;
} SpinLock;
定义了一个名为 SpinLock
的结构体,其中包含一个 pthread_spinlock_t
类型的成员lock
,用于表示自旋锁。
【拓展】在这段代码中,虽然直接使用
pthread_spinlock_t
定义锁是可行的,但选择将其封装在一个名为Spinlock
的结构体中可能是出于以下考虑:
代码封装和模块化:通过将自旋锁封装在一个结构体中,你可以更容易地将其作为一个独立的模块来处理。这有助于保持代码的整洁性和可维护性。当你需要在其他代码中使用自旋锁时,你可以简单地引入这个结构体,而不需要记住所有的 pthread_spin_*
函数和它们的参数。扩展性:通过封装,你可以在未来更容易地为自旋锁添加更多的功能或属性,而不需要更改使用它的代码。例如,你可能想添加一些调试信息,或者记录锁的获取和释放次数,或者实现一个更复杂的锁策略。所有这些都可以通过简单地修改 Spinlock
结构体和相关的函数来实现。封装细节:封装可以帮助隐藏一些实现细节,只向用户暴露必要的接口。在这个例子中,用户只需要知道如何初始化、锁定、解锁和销毁一个 Spinlock
,而不需要关心底层的pthread_spin_*
函数是如何工作的。抽象和类型安全:使用 Spinlock
结构体而不是裸的pthread_spinlock_t
增加了类型安全性。这意味着用户不能意外地将一个普通的整数或其他类型的变量传递给需要Spinlock
的函数,从而减少了出错的可能性。易于理解和使用:对于不熟悉 POSIX 线程库的人来说, Spinlock
结构体和相关的函数可能更容易理解和使用。它们提供了一个更高级别的抽象,隐藏了底层的复杂性。总之,虽然在这个特定的例子中,封装可能看起来有些多余,但在更复杂的系统或库中,封装通常是一个很好的做法,可以帮助提高代码的可读性、可维护性和可扩展性。
自旋锁初始化、销毁、加锁和解锁函数: 这些函数提供了对自旋锁的基本操作。
spin_lock_init
:初始化自旋锁。spin_lock_destroy
:销毁自旋锁。spin_lock
:加锁操作,确保同一时间只有一个线程可以访问被保护的资源。spin_unlock
:解锁操作,允许其他线程访问被保护的资源。共享变量定义:
volatile int shared_var = 0;
定义了一个全局的 volatile
整数变量 shared_var
。这里的 volatile
关键字告诉编译器不要对该变量的访问进行优化,因为该变量的值可能会在任何时候被外部因素(如另一个线程)改变。但请注意,volatile
并不能保证线程安全,它只是提供了对编译器优化的提示。真正的线程安全是通过自旋锁来实现的。
void *thread_func(void *arg) {
// ...
}
这是一个线程函数,它接受一个指向 SpinLock
结构体的指针作为参数。在这个函数中,线程会尝试获取自旋锁,然后增加 shared_var
的值,并最后释放自旋锁。这个过程会重复1000
次。
int main() {
// ...
}
在主函数中,首先创建了一个 SpinLock
的实例 lock
,并调用 spin_lock_init
来初始化它。然后,创建了两个线程 t1
和 t2
,它们都执行 thread_func
函数,并将 lock
作为参数传递。这两个线程会并发地运行,并尝试修改 shared_var
的值。由于有自旋锁的保护,这两个线程会安全地访问和修改 shared_var
。最后,主线程等待两个线程结束,并销毁自旋锁。
【拓展】在上面的代码中,将
lock
作为参数传递给线程函数thread_func
的原因是为了确保每个线程都能访问到同一个自旋锁实例,以便正确地同步对共享资源(即shared_var
)的访问。具体来说,如果你在创建线程时没有将
lock
作为参数传递,而是直接在main
函数中声明了一个SpinLock
类型的局部变量lock
,并且每个线程都试图操作这个局部变量,那么实际上每个线程都会操作它自己的lock
副本(因为局部变量是在栈上分配的,每个线程有自己的栈)。这样一来,自旋锁就失去了其同步多个线程访问共享资源的能力,因为每个线程都认为自己拥有了对共享资源的独占访问权。通过将
lock
的地址作为参数传递给线程函数,你可以确保所有线程都引用同一个SpinLock
实例。这样,当一个线程成功获得自旋锁时,其他尝试获得该锁的线程将会被阻塞(或自旋),直到第一个线程释放锁为止。这就实现了对共享资源shared_var
的线程安全访问。因此,将lock
作为参数传递是确保多线程环境中同步机制正确工作的关键。
注意:在实际的多线程编程中,还需要考虑其他因素,如错误处理、线程优先级、同步原语的选择(互斥锁、读写锁、条件变量等)以及线程安全的数据结构等。此外,虽然这个示例使用了
volatile
关键字,但在实际的线程安全编程中,通常不建议依赖volatile
来确保线程安全。
自旋锁本身的设计是为了避免线程在尝试获取锁时被阻塞,而是让它持续“自旋”(即循环检查锁的状态),从而减少了线程切换的开销。然而,如果自旋锁的使用不当,也可能会导致类似死锁的情况,尽管这种情况在实际中并不常见,因为自旋锁通常用于短时间内的线程阻塞场景。
不过,我可以提供一个模拟自旋锁可能导致“类似死锁”的 C
语言案例,这里的“类似死锁”指的是两个或多个线程因为某种原因而无法继续执行下去,但严格意义上并不完全符合死锁的定义(即循环等待条件)。假设有两个线程(线程 A
和线程 B
)试图访问一个共享资源,并使用自旋锁来保护这个资源。但是,由于某种原因(例如编程错误),线程 A
在持有自旋锁的情况下进入了无限循环,而无法释放锁,导致线程 B
永远无法获取到锁,从而进入了一个无法继续执行的状态。
#include
#include
#include
pthread_spinlock_t spin_lock;
void* thread_A(void* arg) {
pthread_spin_lock(&spin_lock);
printf("Thread A acquired the lock.\n");
// 模拟线程A进入无限循环,无法释放锁
while (1) {
// do something, but never releases the lock
usleep(100000); // 休眠一段时间,但锁不会被释放
}
// 注意:由于上面的无限循环,以下代码永远不会被执行
pthread_spin_unlock(&spin_lock);
printf("Thread A released the lock (but this will never happen).\n");
return NULL;
}
void* thread_B(void* arg) {
// 线程B尝试获取锁
while (pthread_spin_trylock(&spin_lock) != 0) {
printf("Thread B is spinning, waiting for the lock...\n");
usleep(100000); // 休眠一段时间后再尝试获取锁
}
printf("Thread B acquired the lock (but this will never happen if thread A never releases it).\n");
// ... 执行一些操作后释放锁
pthread_spin_unlock(&spin_lock);
return NULL;
}
int main() {
pthread_t thread_id_A, thread_id_B;
// 初始化自旋锁
pthread_spin_init(&spin_lock, 0);
// 创建线程A和线程B
pthread_create(&thread_id_A, NULL, thread_A, NULL);
pthread_create(&thread_id_B, NULL, thread_B, NULL);
// 等待线程A和线程B结束(但在这个例子中,线程B永远不会结束)
pthread_join(thread_id_A, NULL);
pthread_join(thread_id_B, NULL); // 这行代码实际上永远不会执行到,因为线程B永远不会结束
// 销毁自旋锁(但在这个例子中,这行代码也不会执行到)
pthread_spin_destroy(&spin_lock);
return 0;
}
编译并执行程序,结果如下:
[root@localhost spinlock]# gcc dead_lock.c -o dead_lock -lpthread
[root@localhost spinlock]# ./dead_lock
Thread A acquired the lock.
Thread B is spinning, waiting for the lock...
Thread B is spinning, waiting for the lock...
Thread B is spinning, waiting for the lock...
Thread B is spinning, waiting for the lock...
Thread B is spinning, waiting for the lock...
Thread B is spinning, waiting for the lock...
Thread B is spinning, waiting for the lock...
Thread B is spinning, waiting for the lock...
Thread B is spinning, waiting for the lock...
^C
在上述示例中,我们进行了如下操作:
A
持有自旋锁并进入了一个无限循环,导致它永远无法释放锁。B
尝试获取锁,但由于线程 A
永远不释放锁,所以线程 B
永远无法获取到锁,进入了一个无法继续执行的状态。注意:
pthread_join(thread_id_B, NULL);
这行代码不会执行到的原因是线程B
永远不会主动退出,因为它在尝试获取自旋锁时进入了一个无限循环(由于线程A
持有锁并且永远不会释放它)。在
POSIX
线程(pthreads
)中,pthread_join
函数会阻塞调用它的线程,直到指定的线程终止。但是,如果那个线程永远不会终止(如本例中的线程B
),那么pthread_join
将永远阻塞,并且主线程(在这个例子中是执行main
函数的线程)将不会继续执行,包括pthread_join(thread_id_B, NULL);
及之后的任何代码。
要避免自旋锁死锁,一个常见的策略是确保所有线程都按照固定的顺序来获取锁。下面是一个避免自旋锁死锁的示例案例:
假设我们有两个线程 A
和 B
,它们都需要访问两个共享资源 Resource1
和 Resource2
。每个资源都有一个与之关联的自旋锁(spinlock1
和 spinlock2
)。
错误的实现(可能导致死锁):
线程 A
可能首先尝试获取 Resource1
的锁(spinlock1
),然后尝试获取 Resource2
的锁(spinlock2
)。同样地,线程 B
可能首先尝试获取 Resource2
的锁(spinlock2
),然后尝试获取 Resource1
的锁(spinlock1
)。如果线程 A
获得了 spinlock1
而线程 B
获得了 spinlock2
,它们将陷入死锁状态,因为每个线程都在等待另一个线程释放它所持有的锁。
避免死锁的正确实现:
为了确保不会发生死锁,我们可以让所有线程都按照相同的顺序来请求锁。在这个例子中,我们可以规定所有线程都首先尝试获取 spinlock1
,然后尝试获取 spinlock2
。
#include
#include
pthread_spinlock_t spinlock1;
pthread_spinlock_t spinlock2;
int Resource1 = 0;
int Resource2 = 0;
void* thread_a(void* arg) {
pthread_spin_lock(&spinlock1);
printf("Thread A: Got spinlock1\n");
// 临界区代码(对Resource1的操作)
Resource1 += 100; // 对Resource1进行累加操作
pthread_spin_lock(&spinlock2);
printf("Thread A: Got spinlock2\n");
// 临界区代码(对Resource2的操作)
Resource2 += 100; // 对Resource2进行累加操作
pthread_spin_unlock(&spinlock2);
pthread_spin_unlock(&spinlock1);
return NULL;
}
void* thread_b(void* arg) {
// 注意:与 thread_a 相同的锁顺序
pthread_spin_lock(&spinlock1);
printf("Thread B: Got spinlock1\n");
// 临界区代码(对Resource1的操作)
Resource1 += 200; // 对Resource1进行累加操作
pthread_spin_lock(&spinlock2);
printf("Thread B: Got spinlock2\n");
// 临界区代码(对Resource2的操作)
Resource2 += 200; // 对Resource2进行累加操作
pthread_spin_unlock(&spinlock2);
pthread_spin_unlock(&spinlock1);
return NULL;
}
int main() {
pthread_t ta, tb;
// 初始化自旋锁
pthread_spin_init(&spinlock1, PTHREAD_PROCESS_PRIVATE);
pthread_spin_init(&spinlock2, PTHREAD_PROCESS_PRIVATE);
pthread_create(&ta, NULL, thread_a, NULL);
pthread_create(&tb, NULL, thread_b, NULL);
pthread_join(ta, NULL);
pthread_join(tb, NULL);
// 销毁自旋锁
pthread_spin_destroy(&spinlock1);
pthread_spin_destroy(&spinlock2);
printf("Final Resource1 value: %d\n", Resource1);
printf("Final Resource2 value: %d\n", Resource2);
return 0;
}
这段代码展示了如何在 C
语言中使用 POSIX
线程(pthreads
)库来创建和管理两个线程(thread_a
和 thread_b
),以及如何使用自旋锁(spinlock
)来保护对共享资源(Resource1
和 Resource2
)的访问,避免并发冲突。
头文件引入:
#include
#include
引入了标准输入输出头文件 stdio.h
和 POSIX
线程库头文件 pthread.h
。
全局变量定义:
pthread_spinlock_t spinlock1;
pthread_spinlock_t spinlock2;
int Resource1 = 0;
int Resource2 = 0;
定义了两个自旋锁 spinlock1
和 spinlock2
,以及两个整数型共享资源 Resource1
和 Resource2
,它们初始化为 0
。
线程函数定义: 定义了两个线程函数thread_a
和thread_b
,它们具有相同的结构:
这两个线程函数展示了如何按照相同的顺序获取两个自旋锁,以避免死锁。
spinlock1
。Resource1
。spinlock2
。Resource2
。spinlock2
和spinlock1
。主函数 main
:
spinlock1
和spinlock2
的实例,并指定它们为进程私有(PTHREAD_PROCESS_PRIVATE
)。这意味着这些锁仅在当前进程中有效。pthread_create
创建了两个线程ta
和tb
,分别运行thread_a
和thread_b
函数。pthread_join
等待两个线程完成。spinlock1
和spinlock2
。Resource1
和Resource2
的值。关键点:
CPU
资源。thread_a
和thread_b
都首先获取spinlock1
,然后获取spinlock2
。Resource1
或Resource2
,从而避免了数据竞争和不一致。main
函数的末尾,使用pthread_spin_destroy
销毁了自旋锁,释放了它们占用的资源。这是一个好习惯,可以防止资源泄漏。编译和执行程序,结果如下:
[root@localhost spinlock]# gcc spin_lock_avoid.c -o spin_lock_avoid -lpthread
[root@localhost spinlock]# ./spin_lock_avoid
Thread A: Got spinlock1
Thread A: Got spinlock2
Thread B: Got spinlock1
Thread B: Got spinlock2
Final Resource1 value: 300
Final Resource2 value: 300
根据上面程序运行结果可知,线程 A
和线程 B
对共享资源的访问及操作符合预期结果。
这段代码是一个很好的示例,展示了如何在多线程环境中使用自旋锁来保护共享资源。在这个示例中,由于线程 A
和线程 B
都按照相同的顺序(先 spinlock1
后 spinlock2
)来获取锁,因此它们不可能陷入死锁状态。即使一个线程先获取了一个锁,另一个线程也会等待,直到第一个线程释放了所有锁,然后再尝试获取它们。这样就避免了死锁的发生。
在 C
语言中,如果你想要让自旋锁在无法获取锁时让出 CPU
,你不能直接修改自旋锁的实现来达到这个目的,因为自旋锁的设计初衷就是不让出 CPU
,而是持续检查锁的状态。但是,你可以通过结合其他机制来模拟这种行为。
一种常见的方法是使用条件变量(pthread_cond_t
)与互斥锁(pthread_mutex_t
)来代替自旋锁,当互斥锁无法获取时,线程可以等待条件变量并释放 CPU
。但是,如果你仍然想要使用自旋锁并模拟让出 CPU
的行为,你可以使用 sched_yield()
函数。
sched_yield()
函数是 POSIX
提供的一个函数,它允许当前线程放弃 CPU
的剩余时间片,使其他线程有机会运行。在自旋锁的实现中,你可以在循环中添加 sched_yield()
来模拟让出 CPU
。
以下是一个简单的示例:
#include
#include
#include
#include
#include
pthread_spinlock_t spinlock;
void *thread_func(void *arg) {
long tid = (long)arg;
while (1) {
if (pthread_spin_trylock(&spinlock) == 0) {
// 锁已获取,执行临界区代码
printf("Thread %ld acquired the spinlock\n", tid);
// 假设在临界区有一些工作要做,但这里只是模拟
// 模拟临界区执行时间(非常短,以便快速释放锁)
usleep(100); // 休眠100微秒
pthread_spin_unlock(&spinlock);
break; // 假设线程获取到锁后就会退出
} else {
// 没有获取到锁,让出CPU
printf("Thread %ld failed to acquire the spinlock, yielding...\n", tid);
sched_yield();
// 为了模拟和观察效果,我们可以添加一个非常短的延时
usleep(10); // 休眠10微秒,仅为了观察效果,实际应用中可能不需要
}
}
return NULL;
}
int main() {
pthread_t thread1, thread2;
// 初始化自旋锁
if (pthread_spin_init(&spinlock, 0) != 0) {
perror("pthread_spin_init");
exit(EXIT_FAILURE);
}
// 锁定自旋锁,以确保至少有一个线程会立即失败
pthread_spin_lock(&spinlock);
// 创建两个线程
if (pthread_create(&thread1, NULL, thread_func, (void *)1) != 0) {
perror("pthread_create thread1");
exit(EXIT_FAILURE);
}
if (pthread_create(&thread2, NULL, thread_func, (void *)2) != 0) {
perror("pthread_create thread2");
exit(EXIT_FAILURE);
}
// 在主线程中保持自旋锁一段时间,以模拟其他线程频繁尝试获取锁但失败的情况
usleep(100); // 让其他线程有足够的时间尝试获取锁并调用sched_yield()
// 释放自旋锁,允许一个线程获取并退出
pthread_spin_unlock(&spinlock);
// 等待线程结束
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
// 销毁自旋锁
pthread_spin_destroy(&spinlock);
return 0;
}
这段代码主要展示了如何使用 POSIX
自旋锁(spinlock
)在多线程环境中保护临界区。在上述示例中,我们进行了如下操作:
程序通过引入以下头文件来获取必要的函数和数据类型支持:
#include
#include
#include
#include
#include
pthread.h:这是 POSIX
线程库的主要头文件,它包含了用于多线程编程的函数和数据类型,如 pthread_t
(线程 ID
的类型)、pthread_spin_init()
(初始化自旋锁)、pthread_spin_trylock()
(尝试获取自旋锁)等。
sched.h:这个头文件提供了与线程调度相关的函数和宏,如 sched_yield()
,该函数允许一个线程主动放弃 CPU
的使用权,使得其他线程有机会运行。
stdio.h:标准输入输出库,包含了用于输出调试信息和结果的函数,如 printf()
。
stdlib.h:标准库,包含了一些通用的函数,如 exit()
,用于程序异常退出。
unistd.h:Unix
标准库,提供了 POSIX
操作系统 API
的访问,包括 usleep()
(尽管该函数已在新版本的 POSIX
标准中被弃用,建议使用 nanosleep()
代替)。
程序定义了一个全局的自旋锁变量spinlock
。自旋锁是一种特殊的锁,它不同于传统的互斥锁,当线程尝试获取锁失败时,它不会阻塞等待,而是会持续检查锁的状态,直到成功获取为止。因此,自旋锁通常适用于等待时间较短的情况。
thread_func
该函数是线程的执行体,每个线程都会执行这个函数。
pthread_spin_trylock()
函数获取自旋锁。如果成功获取到锁(返回值为0),则进入临界区执行特定的操作(例如打印消息和休眠)。执行完毕后,使用pthread_spin_unlock()
释放锁。0
值),则调用 sched_yield()
函数来提示操作系统当前线程愿意放弃 CPU
的使用权,以便其他线程有机会运行。这是一个非阻塞性的操作,不会阻塞当前线程的执行。sched_yield()
后可能会使用usleep(10)
进行短暂的休眠(虽然这种休眠在实际应用中可能不是必要的,因为它会阻塞线程一小段时间)。main
主函数是程序的入口点,它负责程序的初始化、线程的创建、等待线程结束以及资源的清理。
pthread_spin_init()
函数初始化自旋锁 spinlock
。spinlock
,以确保至少有一个线程会立即尝试获取锁但失败。pthread_create()
函数创建两个线程 thread1
和 thread2
,并传递一个唯一的线程 ID
(1
和 2
)作为参数给线程函数 thread_func
。usleep(100)
使主线程休眠 100
毫秒,以便其他线程有机会尝试获取锁并执行相应的操作。spinlock
,允许之前尝试获取锁但失败的线程进入临界区执行操作。pthread_join()
函数等待两个线程结束,确保主线程在所有线程执行完毕后才退出。pthread_spin_destroy()
函数销毁自旋锁 spinlock
,释放相关资源。注意事项
sched_yield()
函数是一个提示性的系统调用,它告诉操作系统当前线程愿意放弃CPU
的使用权,但操作系统可能会选择忽略这个提示。CPU
资源。usleep()
函数在新版本的 POSIX
标准中已被弃用,建议使用nanosleep()
函数来替代它进行更精确的线程休眠。在这个示例中,两个线程尝试获取自旋锁。如果无法获取锁,它们会调用 sched_yield()
来让出 CPU
。注意,这并不意味着线程会立即放弃 CPU
并立即切换到其他线程,但它确实会向调度器表明当前线程愿意放弃剩余的 CPU
时间片。
另外,你可以选择性地添加 usleep()
或其他延时函数来进一步减少 CPU
的使用率。但是,请注意,添加过多的延时可能会导致线程响应变慢。
编译和执行程序,结果如下:
[root@localhost spinlock]# ./spin_lock_giveup_cpu
Thread 1 failed to acquire the spinlock, yielding...
Thread 2 failed to acquire the spinlock, yielding...
Thread 1 failed to acquire the spinlock, yielding...
Thread 2 acquired the spinlock
Thread 1 failed to acquire the spinlock, yielding...
Thread 1 failed to acquire the spinlock, yielding...
Thread 1 acquired the spinlock
自旋锁是一种高效且轻量级的线程同步机制,它通过让线程在获取锁失败时持续自旋尝试来提高并发性能。在多处理器系统中和对响应时间要求较高的场景中,自旋锁具有显著的优势。然而,我们也需要注意到自旋锁可能带来的 CPU
资源消耗问题,因此在使用时需要结合实际情况进行权衡和选择。