Linux多线程同步机制--自旋锁(SpinLock)

原创 Linux二进制 2024-07-03 08:20

引言

在 Linux 操作系统的多线程编程领域中,合理的同步机制是确保数据一致性和线程安全的关键。自旋锁(Spin Lock)作为一种轻量级的同步手段,在特定的应用场景下,提供了与传统互斥锁不同的性能优势。本文将深入探讨自旋锁的内部原理、实现方式,并结合 C 语言案例,展示其在实际编程中的应用。

自旋锁的概念

自旋锁是一种特殊的锁机制,它不会使线程让出 CPU 给其他线程,进入阻塞状态等待锁的释放,而是让线程在获取锁失败时持续进行“自旋”尝试,即不断循环检查锁的状态,直到成功获取锁或达到一定的尝试次数。这种机制的核心思想是通过快速尝试获取锁来减少线程切换的开销,提高并发性能。

自旋锁适用于短时间内的竞争情况,因为长时间无法获取锁时,线程会持续忙等,消耗 CPU资源。因此,在设计自旋锁时通常会设置一个最大尝试次数,超过该次数后线程会放弃自旋,选择其他策略(如阻塞)等待锁的释放。

自旋锁的原理

自旋锁的基本原理相对简洁直观。当一个线程尝试获取一个已经被其他线程占用的自旋锁时,该线程不会像传统的锁那样立即进入阻塞状态并等待被唤醒,而是会在一个紧凑的循环中持续不断地尝试获取该锁。这种持续尝试的行为就如同线程在“自旋”,故而得名自旋锁。

  1. 锁定机制
  • 当一个执行单元(如线程)尝试访问被自旋锁保护的共享资源时,它必须先尝试获取锁。
  • 如果锁未被其他执行单元持有,该执行单元将立即获得锁并访问共享资源。
  • 如果锁已被其他执行单元持有,该执行单元将不会进入睡眠状态,而是会持续地循环检测(即“自旋”)锁的状态,直到锁被释放。
  • “自旋”行为
    • “自旋”一词来源于该执行单元在循环中等待锁释放的行为,就像在原地“旋转”一样。
    • 自旋锁在等待期间会持续消耗 CPU 资源,因为它通过执行无用的任务(如空循环)来保持活动状态。
    • 为了避免过多占用 CPU 资源,一些自旋锁实现会包含一个参数来限定最多持续尝试的次数。超出该次数后,自旋锁会放弃当前时间片,等待下一次机会。
  • 与互斥锁的区别
    • 互斥锁在资源被占用时会使线程进入睡眠状态,而自旋锁则不会。
    • 对于短暂的锁定,自旋锁的效率更高,因为它避免了线程切换和唤醒的开销。
    • 但如果自旋时间较长,自旋锁可能会浪费大量 CPU 资源。
  • 适用场景
    • 自旋锁适用于保持锁时间非常短的情况,例如在多核/多 CPU 系统中,当多个线程需要短暂地访问共享资源时。
    • 在单核/单 CPU 系统上,自旋锁通常没有优势,因为它会阻止其他线程运行,而锁又不会被其他线程释放。

    在 Linux 内核中,自旋锁的实现通常依赖于底层的原子操作来保证其高效性和正确性。原子操作是指在执行过程中不会被中断或干扰的操作,确保了对锁状态的操作是完整和一致的。

    自旋锁函数原型

    POSIX 自旋锁(spinlock)的函数集,它们定义在  头文件中。POSIX 自旋锁是一种用于同步线程的低级机制,当线程试图获取一个已经被其他线程持有的锁时,它会“自旋”(即忙等待)而不是被阻塞(进入睡眠状态)。这适用于锁持有时间较短的场景,以避免线程切换的开销。

    以下是POSIX 自旋锁(spinlock)函数集的清晰归纳和详细描述:

    1. 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)等机制可能更为合适。

    自旋锁实战

    1、自旋锁加解锁

    下面是一个简单的 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)的示例。我将为您逐步解析这段代码:

    1. 头文件引入
    #include   
    #include   
    #include 

    这里引入了三个头文件:stdio.h用于输入输出功能,pthread.h 用于 POSIX 线程(也称为 pthreads )的 APIstdbool.h 用于布尔数据类型。

    1. 自旋锁结构体定义
    typedef struct {      
        pthread_spinlock_t lock;  
    } SpinLock;

    定义了一个名为 SpinLock 的结构体,其中包含一个 pthread_spinlock_t 类型的成员lock,用于表示自旋锁。

    拓展】在这段代码中,虽然直接使用 pthread_spinlock_t 定义锁是可行的,但选择将其封装在一个名为 Spinlock 的结构体中可能是出于以下考虑:

    1. 代码封装和模块化:通过将自旋锁封装在一个结构体中,你可以更容易地将其作为一个独立的模块来处理。这有助于保持代码的整洁性和可维护性。当你需要在其他代码中使用自旋锁时,你可以简单地引入这个结构体,而不需要记住所有的 pthread_spin_* 函数和它们的参数。
    2. 扩展性:通过封装,你可以在未来更容易地为自旋锁添加更多的功能或属性,而不需要更改使用它的代码。例如,你可能想添加一些调试信息,或者记录锁的获取和释放次数,或者实现一个更复杂的锁策略。所有这些都可以通过简单地修改 Spinlock 结构体和相关的函数来实现。
    3. 封装细节:封装可以帮助隐藏一些实现细节,只向用户暴露必要的接口。在这个例子中,用户只需要知道如何初始化、锁定、解锁和销毁一个 Spinlock,而不需要关心底层的 pthread_spin_* 函数是如何工作的。
    4. 抽象和类型安全:使用 Spinlock 结构体而不是裸的 pthread_spinlock_t 增加了类型安全性。这意味着用户不能意外地将一个普通的整数或其他类型的变量传递给需要 Spinlock 的函数,从而减少了出错的可能性。
    5. 易于理解和使用:对于不熟悉 POSIX 线程库的人来说,Spinlock 结构体和相关的函数可能更容易理解和使用。它们提供了一个更高级别的抽象,隐藏了底层的复杂性。

    总之,虽然在这个特定的例子中,封装可能看起来有些多余,但在更复杂的系统或库中,封装通常是一个很好的做法,可以帮助提高代码的可读性、可维护性和可扩展性。

    1. 自旋锁初始化、销毁、加锁和解锁函数: 这些函数提供了对自旋锁的基本操作。

    • spin_lock_init:初始化自旋锁。
    • spin_lock_destroy:销毁自旋锁。
    • spin_lock:加锁操作,确保同一时间只有一个线程可以访问被保护的资源。
    • spin_unlock:解锁操作,允许其他线程访问被保护的资源。
  • 共享变量定义

  • volatile int shared_var = 0;

    定义了一个全局的 volatile 整数变量 shared_var 。这里的 volatile 关键字告诉编译器不要对该变量的访问进行优化,因为该变量的值可能会在任何时候被外部因素(如另一个线程)改变。但请注意,volatile 并不能保证线程安全,它只是提供了对编译器优化的提示。真正的线程安全是通过自旋锁来实现的。

    1. 线程函数
    void *thread_func(void *arg) {      
        // ...  
    }

    这是一个线程函数,它接受一个指向 SpinLock 结构体的指针作为参数。在这个函数中,线程会尝试获取自旋锁,然后增加 shared_var 的值,并最后释放自旋锁。这个过程会重复1000 次。

    1. 主函数
    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 来确保线程安全。

    2、自旋锁死锁

    自旋锁本身的设计是为了避免线程在尝试获取锁时被阻塞,而是让它持续“自旋”(即循环检查锁的状态),从而减少了线程切换的开销。然而,如果自旋锁的使用不当,也可能会导致类似死锁的情况,尽管这种情况在实际中并不常见,因为自旋锁通常用于短时间内的线程阻塞场景。

    不过,我可以提供一个模拟自旋锁可能导致“类似死锁”的 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

    在上述示例中,我们进行了如下操作:

    1. 在这个例子中,线程 A 持有自旋锁并进入了一个无限循环,导致它永远无法释放锁。
    2. 线程 B 尝试获取锁,但由于线程 A 永远不释放锁,所以线程 B 永远无法获取到锁,进入了一个无法继续执行的状态。
    3. 这是一个模拟的“类似死锁”的情况,实际上在编写代码时应该避免这种情况的发生。正确的做法是在适当的时候释放锁,或者使用其他同步机制来避免线程之间的死锁或长时间阻塞。

    注意pthread_join(thread_id_B, NULL); 这行代码不会执行到的原因是线程 B 永远不会主动退出,因为它在尝试获取自旋锁时进入了一个无限循环(由于线程 A 持有锁并且永远不会释放它)。

    在 POSIX 线程(pthreads)中,pthread_join 函数会阻塞调用它的线程,直到指定的线程终止。但是,如果那个线程永远不会终止(如本例中的线程 B),那么 pthread_join 将永远阻塞,并且主线程(在这个例子中是执行 main 函数的线程)将不会继续执行,包括 pthread_join(thread_id_B, NULL); 及之后的任何代码。

    3、固定顺序加锁避免死锁

    要避免自旋锁死锁,一个常见的策略是确保所有线程都按照固定的顺序来获取锁。下面是一个避免自旋锁死锁的示例案例:

    假设我们有两个线程 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)的访问,避免并发冲突。

    1. 头文件引入:

      #include   
      #include 

      引入了标准输入输出头文件 stdio.h 和 POSIX 线程库头文件 pthread.h

    2. 全局变量定义:

      pthread_spinlock_t spinlock1;  
      pthread_spinlock_t spinlock2;  
      int Resource1 = 0;  
      int Resource2 = 0;

      定义了两个自旋锁 spinlock1 和 spinlock2,以及两个整数型共享资源 Resource1 和 Resource2,它们初始化为 0

    3. 线程函数定义: 定义了两个线程函数thread_athread_b,它们具有相同的结构:

      这两个线程函数展示了如何按照相同的顺序获取两个自旋锁,以避免死锁。

    • 锁定spinlock1
    • 访问并修改Resource1
    • 锁定spinlock2
    • 访问并修改Resource2
    • 解锁spinlock2spinlock1
  • 主函数 main:

    • 创建了两个自旋锁spinlock1spinlock2的实例,并指定它们为进程私有(PTHREAD_PROCESS_PRIVATE)。这意味着这些锁仅在当前进程中有效。
    • 使用pthread_create创建了两个线程tatb,分别运行thread_athread_b函数。
    • 使用pthread_join等待两个线程完成。
    • 销毁自旋锁spinlock1spinlock2
    • 打印出最终的Resource1Resource2的值。

    关键点:

    • 自旋锁(Spinlocks): 自旋锁是一种特殊的锁,当线程尝试获取一个已经被其他线程持有的锁时,该线程会忙等待(即“自旋”),直到锁被释放。这适用于等待时间较短的场景,因为线程在等待期间会消耗 CPU 资源。
    • 锁顺序: 在多线程编程中,确保所有线程都以相同的顺序获取锁是很重要的,这样可以避免死锁。在此示例中,thread_athread_b都首先获取spinlock1,然后获取spinlock2
    • 线程同步: 通过使用自旋锁,我们确保了同一时间只有一个线程可以访问和修改Resource1Resource2,从而避免了数据竞争和不一致。
    • 资源释放: 在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)来获取锁,因此它们不可能陷入死锁状态。即使一个线程先获取了一个锁,另一个线程也会等待,直到第一个线程释放了所有锁,然后再尝试获取它们。这样就避免了死锁的发生。

    4、线程获取不到自旋锁,让出CPU

    在 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)在多线程环境中保护临界区。在上述示例中,我们进行了如下操作:

    1. 头文件引入

    程序通过引入以下头文件来获取必要的函数和数据类型支持:

    #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.hUnix 标准库,提供了 POSIX 操作系统 API 的访问,包括 usleep()(尽管该函数已在新版本的 POSIX 标准中被弃用,建议使用 nanosleep()代替)。

    1. 全局变量

    程序定义了一个全局的自旋锁变量spinlock。自旋锁是一种特殊的锁,它不同于传统的互斥锁,当线程尝试获取锁失败时,它不会阻塞等待,而是会持续检查锁的状态,直到成功获取为止。因此,自旋锁通常适用于等待时间较短的情况。

    1. 线程函数 thread_func

    该函数是线程的执行体,每个线程都会执行这个函数。

    • 函数首先尝试使用pthread_spin_trylock()函数获取自旋锁。如果成功获取到锁(返回值为0),则进入临界区执行特定的操作(例如打印消息和休眠)。执行完毕后,使用pthread_spin_unlock()释放锁。
    • 如果线程尝试获取锁失败(返回非 0 值),则调用 sched_yield() 函数来提示操作系统当前线程愿意放弃 CPU 的使用权,以便其他线程有机会运行。这是一个非阻塞性的操作,不会阻塞当前线程的执行。
    • 为了模拟和观察线程切换的效果,函数在调用 sched_yield() 后可能会使用usleep(10) 进行短暂的休眠(虽然这种休眠在实际应用中可能不是必要的,因为它会阻塞线程一小段时间)。
    1. 主函数 main

    主函数是程序的入口点,它负责程序的初始化、线程的创建、等待线程结束以及资源的清理。

    • 使用 pthread_spin_init() 函数初始化自旋锁 spinlock
    • 在主线程中先锁定自旋锁 spinlock,以确保至少有一个线程会立即尝试获取锁但失败。
    • 使用 pthread_create() 函数创建两个线程 thread1 和 thread2,并传递一个唯一的线程 ID1 和 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 资源消耗问题,因此在使用时需要结合实际情况进行权衡和选择。


    Linux二进制 Linux编程、内核模块、网络原创文章分享,欢迎关注"Linux二进制"微信公众号
    评论
    • 光伏逆变器是一种高效的能量转换设备,它能够将光伏太阳能板(PV)产生的不稳定的直流电压转换成与市电频率同步的交流电。这种转换后的电能不仅可以回馈至商用输电网络,还能供独立电网系统使用。光伏逆变器在商业光伏储能电站和家庭独立储能系统等应用领域中得到了广泛的应用。光耦合器,以其高速信号传输、出色的共模抑制比以及单向信号传输和光电隔离的特性,在光伏逆变器中扮演着至关重要的角色。它确保了系统的安全隔离、干扰的有效隔离以及通信信号的精准传输。光耦合器的使用不仅提高了系统的稳定性和安全性,而且由于其低功耗的
      晶台光耦 2024-12-02 10:40 56浏览
    • 光耦合器作为关键技术组件,在确保安全性、可靠性和效率方面发挥着不可或缺的作用。无论是混合动力和电动汽车(HEV),还是军事和航空航天系统,它们都以卓越的性能支持高要求的应用环境,成为现代复杂系统中的隐形功臣。在迈向更环保技术和先进系统的过程中,光耦合器的重要性愈加凸显。1.混合动力和电动汽车中的光耦合器电池管理:保护动力源在电动汽车中,电池管理系统(BMS)是最佳充电、放电和性能监控背后的大脑。光耦合器在这里充当守门人,将高压电池组与敏感的低压电路隔离开来。这不仅可以防止潜在的损坏,还可以提高乘
      腾恩科技-彭工 2024-11-29 16:12 117浏览
    • 在电子技术快速发展的今天,KLV15002光耦固态继电器以高性能和强可靠性完美解决行业需求。该光继电器旨在提供无与伦比的电气隔离和无缝切换,是现代系统的终极选择。无论是在电信、工业自动化还是测试环境中,KLV15002光耦合器固态继电器都完美融合了效率和耐用性,可满足当今苛刻的应用需求。为什么选择KLV15002光耦合器固态继电器?不妥协的电压隔离从本质上讲,KLV15002优先考虑安全性。输入到输出隔离达到3750Vrms(后缀为V的型号为5000Vrms),确保即使在高压情况下,敏感的低功耗
      克里雅半导体科技 2024-11-29 16:15 119浏览
    • 戴上XR眼镜去“追龙”是种什么体验?2024年11月30日,由上海自然博物馆(上海科技馆分馆)与三湘印象联合出品、三湘印象旗下观印象艺术发展有限公司(下简称“观印象”)承制的《又见恐龙》XR嘉年华在上海自然博物馆重磅开幕。该体验项目将于12月1日正式对公众开放,持续至2025年3月30日。双向奔赴,恐龙IP撞上元宇宙不久前,上海市经济和信息化委员会等部门联合印发了《上海市超高清视听产业发展行动方案》,特别提到“支持博物馆、主题乐园等场所推动超高清视听技术应用,丰富线下文旅消费体验”。作为上海自然
      电子与消费 2024-11-30 22:03 71浏览
    • By Toradex胡珊逢简介嵌入式领域的部分应用对安全、可靠、实时性有切实的需求,在诸多实现该需求的方案中,QNX 是经行业验证的选择。在 QNX SDP 8.0 上 BlackBerry 推出了 QNX Everywhere 项目,个人用户可以出于非商业目的免费使用 QNX 操作系统。得益于 Toradex 和 QNX 的良好合作伙伴关系,用户能够在 Apalis iMX8QM 和 Verdin iMX8MP 模块上轻松测试和评估 QNX 8 系统。下面将基于 Apalis iMX8QM 介
      hai.qin_651820742 2024-11-29 15:29 150浏览
    • 国产光耦合器因其在电子系统中的重要作用而受到认可,可提供可靠的电气隔离并保护敏感电路免受高压干扰。然而,随着行业向5G和高频数据传输等高速应用迈进,对其性能和寿命的担忧已成为焦点。本文深入探讨了国产光耦合器在高频环境中面临的挑战,并探索了克服这些限制的创新方法。高频性能:一个持续关注的问题信号传输中的挑战国产光耦合器传统上利用LED和光电晶体管进行信号隔离。虽然这些组件对于标准应用有效,但在高频下面临挑战。随着工作频率的增加,信号延迟和数据保真度降低很常见,限制了它们在电信和高速计算等领域的有效
      腾恩科技-彭工 2024-11-29 16:11 106浏览
    • 《高速PCB设计经验规则应用实践》+PCB绘制学习与验证读书首先看目录,我感兴趣的是这一节;作者在书中列举了一条经典规则,然后进行详细分析,通过公式推导图表列举说明了传统的这一规则是受到电容加工特点影响的,在使用了MLCC陶瓷电容后这一条规则已经不再实用了。图书还列举了高速PCB设计需要的专业工具和仿真软件,当然由于篇幅所限,只是介绍了一点点设计步骤;我最感兴趣的部分还是元件布局的经验规则,在这里列举如下:在这里,演示一下,我根据书本知识进行电机驱动的布局:这也算知行合一吧。对于布局书中有一句:
      wuyu2009 2024-11-30 20:30 88浏览
    • 最近几年,新能源汽车愈发受到消费者的青睐,其销量也是一路走高。据中汽协公布的数据显示,2024年10月,新能源汽车产销分别完成146.3万辆和143万辆,同比分别增长48%和49.6%。而结合各家新能源车企所公布的销量数据来看,比亚迪再度夺得了销冠宝座,其10月新能源汽车销量达到了502657辆,同比增长66.53%。众所周知,比亚迪是新能源汽车领域的重要参与者,其一举一动向来为外界所关注。日前,比亚迪汽车旗下品牌方程豹汽车推出了新车方程豹豹8,该款车型一上市就迅速吸引了消费者的目光,成为SUV
      刘旷 2024-12-02 09:32 60浏览
    • 艾迈斯欧司朗全新“样片申请”小程序,逾160种LED、传感器、多芯片组合等产品样片一触即达。轻松3步完成申请,境内免费包邮到家!本期热荐性能显著提升的OSLON® Optimal,GF CSSRML.24ams OSRAM 基于最新芯片技术推出全新LED产品OSLON® Optimal系列,实现了显著的性能升级。该系列提供五种不同颜色的光源选项,包括Hyper Red(660 nm,PDN)、Red(640 nm)、Deep Blue(450 nm,PDN)、Far Red(730 nm)及Ho
      艾迈斯欧司朗 2024-11-29 16:55 157浏览
    • RDDI-DAP错误通常与调试接口相关,特别是在使用CMSIS-DAP协议进行嵌入式系统开发时。以下是一些可能的原因和解决方法: 1. 硬件连接问题:     检查调试器(如ST-Link)与目标板之间的连接是否牢固。     确保所有必要的引脚都已正确连接,没有松动或短路。 2. 电源问题:     确保目标板和调试器都有足够的电源供应。     检查电源电压是否符合目标板的规格要求。 3. 固件问题: &n
      丙丁先生 2024-12-01 17:37 57浏览
    • 国产光耦合器正以其创新性和多样性引领行业发展。凭借强大的研发能力,国内制造商推出了适应汽车、电信等领域独特需求的专业化光耦合器,为各行业的技术进步提供了重要支持。本文将重点探讨国产光耦合器的技术创新与产品多样性,以及它们在推动产业升级中的重要作用。国产光耦合器创新的作用满足现代需求的创新模式新设计正在满足不断变化的市场需求。例如,高速光耦合器满足了电信和数据处理系统中快速信号传输的需求。同时,栅极驱动光耦合器支持电动汽车(EV)和工业电机驱动器等大功率应用中的精确高效控制。先进材料和设计将碳化硅
      克里雅半导体科技 2024-11-29 16:18 157浏览
    • 学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习笔记&记录学习习笔记&记学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&学习学习笔记&记录学习学习笔记&记录学习学习笔记&记录学习学习笔记&
      youyeye 2024-11-30 14:30 63浏览
    我要评论
    0
    点击右上角,分享到朋友圈 我知道啦
    请使用浏览器分享功能 我知道啦