BUG笔记:双检查锁解决多线程不安全
【问题背景】
项目中出现了挂死问题,gdb查看,表明是A函数中出现了内存越界错误,其中该A函数是被多个线程所调用,并且程序并不是立刻挂死,现象不易复现。
【问题原因】
该函数使用共享队列记录和存储数据,多个线程读取该队列获取数据。
其中一部分的代码为:
int sendTB(Myqueue * queue) { if(NULL == queue) return -3; if(queue->index >= queue->length ) return -1; if(shared_mutex_trylock(queue)) { return -2; } /*************存数据的一系列操作************/ queue->data[queue->index] = 10; queue->index ++; shared_mutex_unlock(queue); return 0; }
很显然这部分的代码的临界资源是共享队列queue,于是在临界资源的前后进行了加锁和解锁的机制,可悲剧还是发生了。
这部分代码乍一看没有什么问题,可实际上在多线程的环境下,依旧会出现问题,其原因在于,先判断了越界的情况后进行加解锁操作,这样会导致,任务在被锁阻塞之前,队列确实没有满,但是在进入临界区的时候,由于之前处理了任务,队列已经满容量,等本次任务执行的时候也没有一个判断就直接越界了。
【举个例子】
假设该共享队列的大小为5,其中已经被占用了4个,现在有两个线程同时都执行到检查越界的判断上(line:5),此时两个任务会抢着剩余的1个位置,却在加锁之前又都有竞争的条件(即:队列未满)这样就导致两个任务都会执行后续的代码,而其中一个任务抢到了cpu,优先执行了存数据的操作,使用了最后一个容量,解锁后,另一个任务还是执行同样的操作,这时候就会出现内存越界的现象。
【如何防止】
只要在加锁的部分再添加一个越界判断就可以:
int sendTB(Myqueue * queue) { if(NULL == queue) return -3; if(queue->index >= queue->length )//第一次判断 return -1; if(shared_mutex_trylock(queue)) { return -2; } if(queue->index >= queue->length )//第二次判断 return -1; /*************存数据的一系列操作************/ queue->data[queue->index] = 10; queue->index ++; shared_mutex_unlock(queue); return 0; }
【引发疑问】
第一次的判断可以删掉吗?
理论上来说,这样并不会造成线程不安全问题,但是这样会导致即使队列处于满的状态,但每一个进入到该函数的任务都会被阻塞掉,加锁解锁变得更加的频繁,影响性能,所以还是带着第一次判断更好
在这样的基础之上如何更加的优化性能?
我个人认为,锁的粒度可以更加的细化,不是对整个队列进行加锁,而是对queue->data[i]加锁,这样在单个元素被占用的情况下,也不会阻塞其他元素的存取过程,从而提高性能。但是这样会需要额外的维护一个队列来记录哪个元素处于空闲状态。