如何知道一个锁到底被哪个线程占用?

4,931 阅读4分钟

一、问题背景

在多线程环境下调试或定位问题时,有时我们会发现某重要线程被卡住在等待某个锁上,但具体是哪个线程或哪段代码拿了锁(特别是代码逻辑比较复杂、线程较多的情况下)又无法一下看出来。

这时我们就需要在调试器里把所有线程点一遍,看他们当前的调用栈,然后再对照源代码看各线程的调用栈上有没有哪个方法是加了这把锁的。

在源码不太熟的情况下,这是个非常费时费力的工作。

二、解决思路

参考上一篇关于dispatch_semaphore优先级反转里讲的,对于某几种(常用的)锁,操作系统会记录其持有线程(owner)是谁的信息。

那么究竟是怎么记的呢?我们能不能直接从锁的实例拿到这个owner信息呢?

三、实验和结果

基于关于dispatch_semaphore优先级反转这篇贴子里描述的,记录了 owner信息的最底层的锁有两种:pthread_mutexos_unfair_lockNSLockNSRecursiveLock是基于pthread_mutex实现的,它们也比较常用,所以下面我们把这几种锁挨个试验一下:

1. pthread_mutex互斥锁

示例代码:

//生成 mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//加锁
pthread_mutex_lock(&mutex);

网上搜索一下它的源码,发现在pthread_mutex_t结构体里有一个__data.__owner字段存的是相关的信息。

Xcode中断点调试了一下,发现加完锁后,mutex0x18偏移处的确放了当前线程的tid,也即可以从锁实例直接定位其owner线程。

注:看内存可以在lldb中调用x/64x &mutex;查线程tid可以调用thread info命令。

所以pthread_mutex_t类型的变量mutexowner线程tid的取法为:

owner_tid = *(int *)((char *)&mutex + 0x18)

注:拿到tid以后可以调用lldb命令thread list来得到tid与常见的线程序号的对应关系

2. NSLockNSRecursiveLock

示例代码:

NSLock *lock = [[NSLock alloc] init];
[lock lock];
    
NSRecursiveLock *recLock = [[NSRecursiveLock alloc] init];
[recLock lock];

这两种锁内部实现是基于pthread_mutex_t的(可以用Hopper直接反编译 Foundation,也可以在Xcode里对-[NSLock lock]打断点看汇编)。

注:下文中附的是模拟器中的反汇编结果,其注释比较明确,真机的注释中的符号显示与实际不一致,容易误导读者。但实质上真机和模拟器上锁的内部实现是一致的,经测试取到的tid偏移也一样。

  • -[NSLock lock]反汇编结果如下:

  • -[NSRecursiveLock lock]反汇编结果如下:

其实现等价于:

// -[NSLock lock]
pthread_mutex_lock(object_getIndexedIvars(self)); //self 为 lock 实例

可以看出,可以直接对lock对象调用object_getIndexedIvars方法即可得到指向pthread_mutex_t实例的指针。

经测试,模拟器上和真机上调用object_getIndexedIvars(lock)的结果都是(char *)lock + 0x10,所以:

NSLock*NSRecursiveLock*类型的变量lockowner线程 id 的取法为:

owner_tid = *(int *)((char *)lock + 0x10 + 0x18);

3. os_unfair_lock

示例代码:

os_unfair_lock unfairlock = OS_UNFAIR_LOCK_INIT;
os_unfair_lock_lock(&unfairlock);

使用同样的方法,经过实验验证,os_unfair_lock类型的变量其 owner 线程取法为:

owner_id = *(unsigned int *)((char *)&unfairlock);

注意: 这里为os_unfair_lock取到的owner_id与实际线程的tid并不一致,系统库中比较锁的owner与当前线程是否一致时是取当前线程tsd中的3号元素(pthread_getspecific(3)),跟这个(owner_id | 1)比较。

//判断 lock owner 是否是当前线程
void
os_unfair_lock_assert_owner(os_unfair_lock_t lock)
{
	_os_unfair_lock_t l = (_os_unfair_lock_t)lock;

        //取当前线程的 id 信息
	os_lock_owner_t self = _os_lock_owner_get_self();
        //取 lock 的 owner 信息
	os_ulock_value_t current = os_atomic_load2o(l, oul_value, relaxed);
	if (unlikely(OS_ULOCK_IS_NOT_OWNER(current, self, 0))) {
		__LIBPLATFORM_CLIENT_CRASH__(current, "Assertion failed: "
				"Lock unexpectedly not owned by current thread");
	}
}

static inline os_lock_owner_t
_os_lock_owner_get_self(void)
{
	os_lock_owner_t self;
	self = (os_lock_owner_t)_os_tsd_get_direct(__TSD_MACH_THREAD_SELF); // 同 pthread_getspecific(3)
	return self;
}

至于怎么从owner_id得到tid还需要进一步调研,不过目前来说写个 lldb 脚本跑一下各线程的pthread_getspecific(3)也能定位出来 tid 和 owner_id 的对应关系。

四、结论及展望

综上可知,通过简单的取偏移办法,就能在运行时定位到某个锁当前的owner线程。

后续可以写一个lldb脚本实现一个owner_tid命令,支持直接在lldb命令行里调用,调试起来就更方便了。

另外,卡顿和死锁检测模块也可以使用这个owner信息,帮助识别卡顿的根本原因。

注:本文主要是方案调研,笔者只在一两个版本的模拟器和真机上进行了试验来验证可行性。理论上这几个结构体应该不怎么会变,不过如果要真正投入线上使用,还是请在支持的系统版本上进行完整验证后再使用。