问题
如何设置获取互斥量时的等待时间?
如果等待超时,如何避免死锁?
避免死锁 -- 设置等待超时
解决方案:
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 行,将互斥锁的类型设置为自适应锁
程序运行结果如下图所示:
程序正常正确的运行了,不会发生死锁