Linux进/线程同步方式3——自旋锁

avatar
作者
筋斗云
阅读量:0

一、自旋锁的背景和定义

  • 自旋锁是为实现保护共享资源而提出的一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁还是自旋锁,在任何时刻,最多只能有一个保持者,也就是说,在任何时刻最多只能有一个执行单元获得锁。
  • 两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠。如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁(忙循环)。

二、自旋锁的适用情况

  • 自旋锁比较适用于锁使用者保持锁时间比较短的情况。也正是由于自旋锁使用者一般保持锁时间非常短,自旋锁的效率远高于互斥锁。
  • 信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进程上下文使用,而自旋锁适合于保持时间非常短的情况,它可以在任何上下文中使用。如果被保护的共享资源只在进程上下文中访问,使用信号量保护该共享资源非常合适,如果对共享资源的访问时间非常短,自旋锁也可以。
  • 自旋锁保持期间是抢占失效的,而信号量和读写信号量保持期间是可以被抢占的。
    • 单CPU非抢占内核下:自旋锁会退化成开关中断(因为单CPU且非抢占模式情况下,不可能发生进程切换,时钟只有一个进程处于临界区,自旋锁实际没什么用了)。当获取自旋锁时就是关闭中断,释放自旋锁就是开启中断。因此通过自旋锁可以防止中断打断进程运行。
    • 单CPU抢占内核下:自选锁会被当作一个设置抢占的开关以及开关中断的开关(因为单CPU不可能有并发访问临界区的情况,禁止抢占就可以保证临街区唯一被拥有)。中断是和上述同样的道理。
    • 多CPU下:此时才能完全发挥自旋锁的作用,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。
  • 操作是原子的,因为当有多个线程在自旋时,也只能有一个线程可以获得该锁。

三、自旋锁的几个重要特性

  1. 被自旋锁保护的临界区代码执行时不能进入休眠;
  2. 被自旋锁保护的临界区代码执行时不能被其他中断打断;
  3. 被自旋锁保护的临界区代码执行时,内核不能被抢占;
  4. 在自旋锁忙等待期间因为并没有进入临界区,所以内核还是可以抢占的。因此,等待自旋锁释放的进程有可能被更高优先级的进程所抢占。
  5. 存在两个问题:死锁和过多占用CPU资源。
    从这几个特性可以归纳出一个共性:被自旋锁保护的临界区代码执行时,它不能因为任何原因放弃处理器。

四、如何避免死锁

  1. 如果中断处理函数中也要获得自旋锁,那么驱动程序需要在拥有自旋锁时禁止中断;
  2. 自旋锁必须在尽可能短的时间内被持有;
  3. 避免某个获得锁的函数调用其他同样试图获取这个锁的函数,否则代码就会死锁;不论是信号量还是自旋锁,都不允许锁拥有者第二次获得这个锁,如果试图这么做,系统将挂起。

五、自旋锁相关的API函数

  1. 初始化
    当线程使用该函数初始化一个未初始化或被销毁过的自旋锁时有效。该函数会为自旋锁申请资源并且初始化为unlocked状态。
    int pthread_spin_init(pthread_spinlock_t *lock, int pshared);
    若成功,返回0;否则,返回错误编号。
    • pthread_spinlock_t:待初始化的自旋锁
    • pshared:
      • PTHREAD_PROCESS_SHARED:该自旋锁可以在多个进程中的线程之间共享(可以被其他进程中的线程看到)。
      • PTHREAD_PROCESS_PRIVATE:仅初始化本自旋锁的线程所在的进程内的线程才能够使用该自旋锁。
  2. 加锁
    用来获取指定的自旋锁,如果该自旋锁当前没有被其他线程所持有,则调用该函数的线程就可以获得该自旋锁,否则该函数在获得自旋锁之前不会返回,一直处于自旋等待状态。
    int pthread_spin_lock(pthread_spinlock_t *lock);
    若成功,返回0;否则,返回错误编号。
  3. 解锁
    释放线程正在持有的自旋锁。
    int pthread_spin_unlock(pthread_spinlock_t *lock);
    若成功,返回0;否则,返回错误编号。
  4. 销毁
    用来销毁指定的自旋锁并释放所有相关联的资源。
    int pthread_spin_destroy(pthread_spinlock_t *lock);
    若成功,返回0;否则,返回错误编号。
    • 在调用该函数之后如果没有调用pthread_spin_init重新初始化自旋锁,则任何尝试使用该锁的调用结果都是未定义的。
    • 如果调用该函数时自旋锁正在被使用或者自旋锁未被初始化则结果是未定义的。

六、自旋锁和互斥锁对比

  • Mutex(互斥锁)
    • sleep-waiting类型的锁
    • 与自旋锁相比它需要消耗大量的系统资源来建立锁,随后当线程被阻塞时,线程的调度状态被修改,并且线程被加入等待线程队列;最后当锁可用时,在获取锁之前,线程会被从等待队列取出并更改其调度状态;但是在线程被阻塞期间,它不消耗CPU资源。
    • 互斥锁适用于那些可能会阻塞很长时间的场景。
      • 1、临界区有IO操作
      • 2、临界区代码复杂或者循环量大
      • 3、临界区竞争非常激烈
      • 4、单核处理器
  • Spin lock(自旋锁)
    • busy-waiting类型的锁
    • 对于自旋锁来说,它只需要消耗很少的资源来建立锁,随后当线程被阻塞时,它就会一直重复检查看锁是否可用了,也就是说当自旋锁处于等待状态时它会一直消耗CPU时间。
    • 自旋锁适用于那些仅需要阻塞很短时间的场景。

七、自旋锁和互斥锁运行实验对比

  • 互斥锁运行实验
    #include <stdio.h> #include <string.h> #include <pthread.h> #include <sys/time.h>  static unsigned int num = 0; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;  /*在sys/time.h中定义的*/ // typedef struct timeval // { //     long tv_sec;     //秒 //     long tv_usec;  //微秒 // }timeval;  __int64_t get_current_timestamp() {     struct timeval now = {0, 0};      gettimeofday(&now, NULL);     return now.tv_sec * 1000 * 1000 + now.tv_usec; }  void thread_proc() {     for(int i = 0; i < 1000000; ++i)     {         pthread_mutex_lock(&mutex);         ++num;         pthread_mutex_unlock(&mutex);     } }  int main() {     pthread_t  t1, t2;     __int64_t start = get_current_timestamp();     pthread_create(&t1, NULL, (void*)thread_proc, NULL);     pthread_create(&t2, NULL, (void*)thread_proc, NULL);     pthread_join(t1, NULL);     pthread_join(t2, NULL);     printf("num:%d\n", num);     __int64_t end = get_current_timestamp();     printf("cost:%ld\n", end - start);     pthread_mutex_destroy(&mutex);      return 0; } 
  • 自旋锁运行实验
    #include <stdio.h> #include <string.h> #include <pthread.h> #include <sys/time.h>  static unsigned int num = 0; pthread_spinlock_t spin_lock;  /*在sys/time.h中定义的*/ // typedef struct timeval // { //     long tv_sec;     //秒 //     long tv_usec;  //微秒 // }timeval;  __int64_t get_current_timestamp() {     struct timeval now = {0, 0};      gettimeofday(&now, NULL);     return now.tv_sec * 1000 * 1000 + now.tv_usec; }  void thread_proc() {     for(int i = 0; i < 1000000; ++i)     {         pthread_spin_lock(&spin_lock);         ++num;         pthread_spin_unlock(&spin_lock);     } }  int main() {     pthread_t  t1, t2;     pthread_spin_init(&spin_lock, PTHREAD_PROCESS_PRIVATE);     __int64_t start = get_current_timestamp();     pthread_create(&t1, NULL, (void*)thread_proc, NULL);     pthread_create(&t2, NULL, (void*)thread_proc, NULL);     pthread_join(t1, NULL);     pthread_join(t2, NULL);     printf("num:%d\n", num);     __int64_t end = get_current_timestamp();     printf("cost:%ld\n", end - start);     pthread_spin_destroy(&spin_lock);      return 0; } 
    在这里插入图片描述
    修改临界区的代码,使得临界区耗时更长:
    void thread_proc() {     for(int i = 0; i < 1000000; ++i)     {         pthread_spin_lock(&spin_lock);         for(int j = 0; j < 1000; ++j)         {              ++num;         }         pthread_spin_unlock(&spin_lock);     } } 
    在这里插入图片描述
    修改以后可以看出自旋锁运行效率慢一些,自旋锁虽然比互斥锁的性能更好(花费很少的CPU指令),但是它只适用于临界区运行时间很短的场景。

    广告一刻

    为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!