活锁方案与自旋锁

问题

如何设置获取互斥量时的等待时间?
如果等待超时,如何避免死锁?

避免死锁 -- 设置等待超时

解决方案:

1、尝试获取第 1 个互斥量:

  • 若成功,则转 2 执行;若失败,则等待;

2、尝试在规定时间内获取第 2 个互斥量:

  • 若成功,则执行临界区代码
  • 若失败,则释放第 1 个互斥量,休眠后转 1 执行

互斥量获取超时 API 函数

"活锁" 解决方案实现

活锁解决方案

test-1.c

#define _GNU_SOURCE     /* To get pthread_getattr_np() declaration */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <time.h>

pthread_mutex_t m1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t m2 = PTHREAD_MUTEX_INITIALIZER;

int g_count = 0;

void live_lock(pthread_mutex_t* pm[], int n)
{
    int r = -1;
    int i = 0;
    int j = 0;
    struct timespec tout = {0};
        
    while( r )
    {    
        for(i=0; i<n; i++)
        {
            clock_gettime(CLOCK_REALTIME, &tout);
                
            tout.tv_sec += 1;
                
            r = pthread_mutex_timedlock(pm[i], &tout);
                 
            if( r )
            {
                for(j=0; j<i; j++)
                {
                    pthread_mutex_unlock(pm[j]);
                }
                usleep(100 * 1000);
                break;
            }
        }
    }
}

void live_unlock(pthread_mutex_t* pm[], int n)
{
    int i = 0;
    
    while( i < n )
    {
        pthread_mutex_unlock(pm[i++]);
    }
}

void* thread_1(void* arg)
{  
    pthread_mutex_t* pm[] = {&m1, &m2};
    int n = sizeof(pm)/sizeof(*pm);
    
    while(1)
    {
        live_lock(pm, n);

        printf("%s : %d\n", __FUNCTION__, g_count++);
        
        usleep(100);
            
        live_unlock(pm, n);
    }
    
    pthread_detach(pthread_self());
    
    return NULL;
}

void* thread_2(void* arg)
{  
    pthread_mutex_t* pm[] = {&m2, &m1};
    int n = sizeof(pm)/sizeof(*pm);
    
    while(1)
    {
        live_lock(pm, n);

        printf("%s : %d\n", __FUNCTION__, g_count++);
        
        usleep(100);
            
        live_unlock(pm, n);
    }
    
    pthread_detach(pthread_self());
    
    return NULL;
}

int main()
{
    int r = 0;
    pthread_t t;
    
    pthread_create(&t, NULL, thread_1, NULL);
    pthread_create(&t, NULL, thread_2, NULL);
    
    printf("Hello World!\n");
    
    while( 1 )
    {
        sleep(1);
    }
    
    return 0;
}

live_lock() 函数是我们实现的活锁解决方案,这个函数支持任意把锁,我们这里设置的超时时间是 1s,如果 1s 内没有获取到锁,则释放已经获取到的锁

在 mian() 函数中,我们创建两个线程,thread_1 先获取互斥量 m1,再获取互斥量 m2,而 thread_2 先获取互斥量 m2,再获取互斥量 m1,他们获取互斥量存在一个环路,可能会导致死锁,而这里使用的活锁方案就可以避免死锁

程序运行结果如下图所示:

当 thread_1 获取到 互斥量 m1,thread_2 获取到互斥量 m2,其中一个互斥量会由于在超时时间内抢夺不到互斥锁而放弃之前获取到的锁,这样另一个线程就能获取到锁,访问临界区

思考

线程获取互斥量失败后究竟发生了什么?

线程会进入阻塞状态,让出 cpu 的执行权,等到互斥量被释放,线程才有机会得到执行

Linux 中的自旋锁

自旋锁也是一种用于保证临界区的原子性的机制

自旋锁与互斥量类似,在任何时刻,最多只能有一个持有者

自旋锁与互斥量在内部机制上不同:

  • 互斥量:
    • 如果已经被其他线程持有,则当前线程进入等待状态 (不占用处理器,进入等待队列)
  • 自旋锁:
    • 如果已经被其他线程持有,则当前线程一直循环查看自旋锁是否再次可持有

互斥量 vs 自旋锁

应用场景

  • 互斥量是一种普适的解决方案,通用性强,副作用小
  • 自旋锁是一种特定场景下的解决方案,通用性弱,副作用大

实现机制

  • 互斥量涉及线程上下文切换,因此在效率上存在不足 (消耗时间资源)
  • 线程一直尝试获取自旋锁,因此不会进入阻塞状态 (消耗处理器资源)

Linux 中的自旋锁 API 函数

自旋锁类型:

  • PTHREAD_PROCESS_PRIVATE => 进程内自旋锁 (同一进程中的线程可用)
  • PTHREAD_PROCESS_SHARED => 进程间自旋锁 (任意进程的任意线程可用)

下面的多线程程序有问题吗?

自旋锁实验

test-2.c

#define _GNU_SOURCE     /* To get pthread_getattr_np() declaration */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <time.h>

int g_count = 0;
pthread_mutex_t g_mutex;
pthread_spinlock_t g_spin;

void* thread_1(void* arg)
{  
    while(1)
    {
        pthread_spin_lock(&g_spin);
        // pthread_mutex_lock(&g_mutex);

        printf("%s : %d\n", __FUNCTION__, g_count++);
       
        usleep(100 * 1000);
        
        pthread_spin_unlock(&g_spin);
        // pthread_mutex_unlock(&g_mutex);
    }
    
    pthread_detach(pthread_self());
    
    return NULL;
}

void* thread_2(void* arg)
{  
    while(1)
    {
        pthread_spin_lock(&g_spin);
        // pthread_mutex_lock(&g_mutex);

        printf("%s : %d\n", __FUNCTION__, g_count++);
       
        usleep(100 * 1000);
        
        pthread_spin_unlock(&g_spin);
        // pthread_mutex_unlock(&g_mutex);
    }
    
    pthread_detach(pthread_self());
    
    return NULL;
}

int main()
{
    int r = 0;
    pthread_t t;  
    
    pthread_mutexattr_t mattr;
    
    pthread_mutexattr_init(&mattr);
    pthread_mutex_init(&g_mutex, &mattr);
    
    pthread_spin_init(&g_spin, PTHREAD_PROCESS_PRIVATE);
    
    pthread_create(&t, NULL, thread_1, NULL);
    pthread_create(&t, NULL, thread_2, NULL);
    
    printf("Hello World!\n");
    
    while( 1 )
    {
        // printf("%s : %d\n", __FUNCTION__, g_count);
        sleep(1);
    }
    
    return 0;
}

程序运行结果如下图所示:

程序正常正确的运行了,不会产生死锁

我们使用 echo $$ 命令,查看 shell 进程的 pid

shell 进程的 pid 为 2274

使用 sudo chrt -p -f 10 2274 命令,改变 shell 的调度策略为 FIFO,这样在 shell 上执行的进程的默认都是实时进程,进程调度策略为 FIFO

最后使用 taskset -c 0 ./a.out 命令,指定这个程序在 cpu0 上执行

程序运行结果如下图所示:

程序发生了死锁,a.out 程序 cpu 占用率快达到 100%

这是因为 thread_1 优先获取到执行权,获取到自旋锁,打印后,通过 usleep() 主动释放 cpu 资源,调度到 thread_2 时,thread_2 会一直获取不到自旋锁,原地自旋,一直消耗cpu 资源,由于进程的调度策略是 FIFO,只有 thread_2 主动释放 cpu 资源,cpu0 才能调度其他线程执行,由于 thread_2 一直无法获取到自旋锁,所以这个程序产生了死锁,并且 thread_2 一直在消耗 cpu 资源

我们使用互斥锁,将自旋锁的代码注释掉,互斥锁的代码打开,再重复进行一次上面的实验

程序没有发生死锁,并且 cpu 的占用率很低

这是因为 thread_2 获取不到互斥锁,会进入阻塞状态,让出 cpu 资源,从而 cpu0 可以调度 thread_1 执行

自旋锁使用细则

轻量级锁定,即:临界区相对短小,自旋锁持有时间非常短

同一线程不可重复获取自旋锁 (导致死锁)

如果只有一个单核处理器,不建议使用自旋锁

线程一旦获取自旋锁,则不能让出处理器使用权

即:线程 获取锁 到 释放锁 的时间内只有一个执行流

有没有一种可能

存在一种 "锁" 类型:

  • 效率高 => 不会轻易发生上下文切换,使得线程进入阻塞状态
  • 无死锁 => 不会无限制自旋,适合的时间让出处理器使用权

关于 PTHREAD_MUTEX_ADAPTIVE_NP 类型

一种特殊的互斥量,又名:自适应锁

自适应锁先以自旋的方式持续尝试获取目标锁

当超时未能获取目标锁,则让出处理器使用权,线程进入阻塞状态

自适应锁相对普通互斥量效率更高,相对自旋锁安全性更好

下面的多线程程序有问题吗?

自适应锁实验

test-3.c

#define _GNU_SOURCE     /* To get pthread_getattr_np() declaration */
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <memory.h>
#include <time.h>

int g_count = 0;
pthread_mutex_t g_mutex;

void* thread_1(void* arg)
{  
    while(1)
    {
        pthread_mutex_lock(&g_mutex);

        printf("%s : %d\n", __FUNCTION__, g_count++);
       
        usleep(100 * 1000);
        
        pthread_mutex_unlock(&g_mutex);
    }
    
    pthread_detach(pthread_self());
    
    return NULL;
}

void* thread_2(void* arg)
{  
    while(1)
    {
        pthread_mutex_lock(&g_mutex);

        printf("%s : %d\n", __FUNCTION__, g_count++);
       
        usleep(100 * 1000);
        
        pthread_mutex_unlock(&g_mutex);
    }
    
    pthread_detach(pthread_self());
    
    return NULL;
}

int main()
{
    int r = 0;
    pthread_t t;  
    
    pthread_mutexattr_t mattr;
    
    pthread_mutexattr_init(&mattr);
	pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_ADAPTIVE_NP);
    pthread_mutex_init(&g_mutex, &mattr);
    
    pthread_create(&t, NULL, thread_1, NULL);
    pthread_create(&t, NULL, thread_2, NULL);
    
    printf("Hello World!\n");
    
    while( 1 )
    {
        // printf("%s : %d\n", __FUNCTION__, g_count);
        sleep(1);
    }
    
    return 0;
}

第 56 行,将互斥锁的类型设置为自适应锁

程序运行结果如下图所示:

程序正常正确的运行了,不会发生死锁

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的死锁和活锁是多线程并发编程中的常见问题,这些问题可能会导致线程无法向前执行或陷入死循环。下面分别介绍Java中死锁和活锁的概念和案例。 1. 死锁 死锁是指两个或多个线程相互等待对方释放锁,从而导致所有线程都无法向前执行的状态。简单来说,当两个或多个线程都占有某些资源并且又想要获取对方占有的资源时,就会发生死锁。 下面是一个Java中的死锁例子: ```java public class DeadlockExample { private Object lock1 = new Object(); private Object lock2 = new Object(); public void method1() { synchronized (lock1) { System.out.println("Method 1: Holding lock 1..."); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println("Method 1: Holding lock 1 and lock 2..."); } } } public void method2() { synchronized (lock2) { System.out.println("Method 2: Holding lock 2..."); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { System.out.println("Method 2: Holding lock 1 and lock 2..."); } } } public static void main(String[] args) { DeadlockExample example = new DeadlockExample(); Thread t1 = new Thread(() -> example.method1()); Thread t2 = new Thread(() -> example.method2()); t1.start(); t2.start(); } } ``` 在这个例子中,两个线程t1和t2分别调用了method1和method2方法,这两个方法都需要获取lock1和lock2两个锁才能继续执行。由于t1占用了lock1并等待lock2,而t2占用了lock2并等待lock1,因此两个线程都无法释放自己占用的锁,从而陷入死锁状态。 2. 活锁 活锁是指线程们都在运行并尝试执行任务,但是由于某些条件始终无法满足,导致线程们一直在重试,但是最终无法完成任务。这种情况下,线程们看起来像是在不断地活动,但是实际上却没有任何进展。 下面是一个Java中的活锁例子: ```java public class LiveLockExample { private boolean isPolite; public LiveLockExample(boolean isPolite) { this.isPolite = isPolite; } public void bow(LiveLockExample partner) { while (true) { if (isPolite) { System.out.println("I'm polite, I'll bow first"); partner.bowBack(this); isPolite = false; break;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值