《多线程基础之互斥锁》
【互斥锁导读】互斥锁是大家使用最多的线程同步手段,但仅仅知道怎么用还是不够的?比如:面试官问你"互斥锁是属于内核层还是应用层的同步保护机制?性能怎样?","频繁加解锁,会有什么问题?","死锁了,应该如何排查?"
和锁相关的api,我就不介绍了,同学们只需要参考下锁使用的相关示例,就能很好掌握它的用法,好!现在直接上代码。
#include <pthread.h>
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
using namespace std;
pthread_mutex_t myMutex;
int resourceNo = 0;
void* workthread(void* param)
{
pthread_t threadNo = pthread_self();
while(true)
{
pthread_mutex_lock(&myMutex);
cout << "thread No: " << threadNo << " resource No: " << ++resourceNo << endl;
pthread_mutex_unlock(&myMutex);
sleep(1);
}
return NULL;
}
int main()
{
cout << getpid() << endl;
pthread_mutexattr_t mutexAttr;
pthread_mutexattr_init(&mutexAttr);
//设置互斥锁的类型,PTHREAD_MUTEX_ERRORCHECK表明是检错锁,
//如果加锁失败,接口会返回对应的错误码
pthread_mutexattr_settype(&mutexAttr,
PTHREAD_MUTEX_NORMAL | PTHREAD_MUTEX_ERRORCHECK);
pthread_mutex_init(&myMutex, &mutexAttr);
//创建5个线程
pthread_t threadID[5];
for (int i = 0; i < 5; ++i)
pthread_create(&threadID[i], NULL, workthread, NULL);
for (int i = 0; i < 5; ++i)
pthread_join(threadID[i], NULL);
//销毁锁资源
pthread_mutex_destroy(&myMutex);
pthread_mutexattr_destroy(&mutexAttr);
return 0;
}
运行结果如下,5个线程分别去争抢全局的锁对象myMutex,进而保证resourceNo的递增符合线程安全的要求。
互斥锁使用起来很轻便,但是其实是有性能问题的,因为一个线程去争取一把锁对象,势必要从用户态切换到内核态,释放锁资源后又从内核态切回用户态。其性能相对于信号量、自旋锁、临界区(windows)这些线程同步机制更低。
1、死锁问题
好,我们稍微修改下上述的示例代码,让程序运行的流程出现死锁,我们可以关注下进程如果出现了死锁,会有怎样的表现
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <iostream>
using namespace std;
pthread_mutex_t myMutexA;
pthread_mutex_t myMutexB;
int resourceBNo = 0;
int resourceANo = 0;
void* workthreadA(void* param)
{
pthread_t threadNo = pthread_self();
while(true)
{
pthread_mutex_lock(&myMutexB);
cout << "thread A: " << threadNo << " resource B No: " << ++resourceBNo << endl;
//线程A占用锁B,不释放锁,随后去争夺锁A
pthread_mutex_lock(&myMutexA);
cout << "thread A" << " resource A No: " << ++resourceANo << endl;
pthread_mutex_unlock(&myMutexA);
}
return NULL;
}
void* workThreadB(void* param)
{
pthread_t threadNo = pthread_self();
while (true)
{
pthread_mutex_lock(&myMutexA);
cout << "thread B: " << threadNo << " resource A No: " << ++resourceANo << endl;
//线程B占用锁A,不释放,随后去争夺锁B
pthread_mutex_lock(&myMutexB);
cout << "thread B " << " resource B No: " << ++resourceBNo << endl;
pthread_mutex_unlock(&myMutexB);
}
return NULL;
}
我们模拟死锁的场景,用gdb调试程序lock,果不其然,lock进程(进程号为9394,通过getpid()获取)卡住了,两个子线程分别争夺对方占用的锁。
使用top指令查看进程9394的状态如下:
但进程的状态是Sleep(任务字段S下面对应的值),让人有点意外,难道陷入死锁的进程,进程会陷入睡眠的状态?而且当前进程CPU消耗为0,物理内存占比也为0。看到线程最后卡死在__lll_lock_wait接口处,也就是说pthread_mutex_lock接口最终会走到__lll_lock_wait()接口处。
/* This function doesn't get included in libc. */
#if IS_IN (libpthread)
void __lll_lock_wait (int *futex, int private)
{
if (*futex == 2)
lll_futex_wait(futex, 2, private); /* Wait if *futex == 2.*/
while (atomic_exchange_acq (futex, 2) != 0)
lll_futex_wait (futex, 2, private); /* Wait if *futex == 2.*/
}
#endif
/* Wait while *FUTEXP == VAL for an lll_futex_wake call on FUTEXP. */
#define lll_futex_wait(futexp, val, private) \
lll_futex_timed_wait (futexp, val, NULL, private)
#define lll_futex_timed_wait(futexp, val, timeout, private) \
lll_futex_syscall (4, futexp, \
__lll_private_flag (FUTEX_WAIT, private), val, timeout)
初步看了下源码,__lll_lock_wait会陷入无限循环的等待中去,因为最终传递给lll_futex_timed_wait接口的超时时间为NULL,直到和锁相关的futex状态变成预定值,也即其它线程释放了这把锁,更改了futex的状态。当前线程才会停止休眠等待,继续执行。
2、频繁加锁问题
好,讨论下最后一个问题,频繁加锁会有什么问题,我们可以先用个小程序演示下。
#include <pthread.h>
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
int main()
{
pthread_mutex_t mymutex;
pthread_mutexattr_t mutex_attr;
pthread_mutexattr_init(&mutex_attr);
pthread_mutexattr_settype(&mutex_attr, PTHREAD_MUTEX_NORMAL);
pthread_mutex_init(&mymutex, &mutex_attr);
int ret = pthread_mutex_lock(&mymutex);
printf("ret = %d\n", ret);
ret = pthread_mutex_lock(&mymutex);
printf("ret = %d\n", ret);
pthread_mutex_destroy(&mymutex);
pthread_mutexattr_destroy(&mutex_attr);
return 0;
}
针对普通的锁,反复对同一把锁进行加锁操作,linux下的表现就是阻塞当前进程或者线程,程序卡死__lll_lock_wait()接口处。
windows下呢?是会卡死吗?笔者可以先剧透下,windows下频繁加解锁,会引起crt库崩溃,中断当前进程,感兴趣的同学可以去实验下。
所以锁底层的知识面还是很细的,需要深入剖析底层的源码,分析加解锁的流程,那么遇到锁一类的问题就能迎难而解了。