作者: 雪山肥鱼
时间:20210429 12:00
目的:常见的并发问题
# 并发问题归类
## 反原子操作
## 顺序问题
# 死锁
## 死锁产生的条件
## Prevention 预防
### 循环和等待的预防
### 持有和等待的预防
### 非抢占式的预防
### 互斥性的预防
### 通过调度预防
### 检查于重启
并发问题归类
死锁和非死锁问题。非死锁问题其实占据了多数。
反原子操作
破坏原子操作
//pthread_mutex_t proc_info_lock = PTHREAD_MUTEX_INITIALIZER
Thread 1:
//pthread_mutex_lock(&proc_info_lock)
if (thd->proc_info) {
...
fputs(thd->proc_info, ....);
...
}
Thread2::
pthread_mutex_lock(&proc_info_lock);
thd->proc_info = NULL;
pthread_mutex_unlock(&proc_info_lock);
Thread 检查完 thd->proc_info 后,被中断,然后执行Thread2 proc_info 被赋值了。再回到 Thread1 的fputs 函数 就出问题了。
解决方案 其实很简单,上下加锁就行了。
顺序问题 order violation
Thread1:
void init() {
mThread = PR_CreateThread(mMain, ...);
}
Thread2:
void mMain() {
mState = mThread->state;
}
在Thread2中 认为 Thread1的 mThread 已经准备好。但是如果此时Thread2 先于 Thread1 ,则会出现问题。
这时候为了保序,可以用条件变量。
pthread_mutex_t mtLock = PTHREAD_MUTEX_INITIALZIER;
ptherad_cond_t mtCond = PTHREAD_COND_INITIALZIER;
int miInit;
Thread 1
void init() {
mThread = PR_CreateThread(mMain, ...);
phthread_mutex_lock(&mtLock)
miInit =1;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtLock);
}
void mMain() {
pthread_mutex_lock(&mtLock);
while(miInit == 0)
pthread_cond_wait(&cond, mtLock);
mState = mThread->state;
pthread_mutex_unlock(&cond);
}
生产中遇到的多线程问题会比上面提到的复杂很多。
死锁
死锁常见情况是,上锁的顺序不同。解决当然也简单,上锁顺序相同即可。
但是为什么解决方案这么简单,但是依然会出现死锁问题呢?
- 代码基数
- 模块之间复杂的依赖性
- 虚拟内存向要从磁盘向内存加载一个block,那么就要访问文件系统
- 文件系统随后会向内存申请一页,将block写入内存,随后于虚拟内存进行交互
- 模块化注定会有封装,那么隐藏的多线程问题就会出现。这种bug并非恶意的。
死锁产生的条件
- 互斥性:线程对于资源申请互斥的control, 例如去grab 锁
- 持有和等待:当线程等待另一个资源的时候,她手上有锁,另一个资源等不到,那么她就不会放手,死锁案发生。也就是说手上有锁,又在等其他的资源。
- 非抢占式: locks 不会被强行剥夺
- 循环等待:线程之间存在一个环路,环路上每个线程都额外持有一个资源,而这个资源又是下一个线程要申请的。
4个只要有一个不存在,死锁就不会发生。
Prevention 预防
循环和等待
- total ordering 全序
对于简单的系统可以保证全局上锁有序 - partial ordering 部分有序
对于复杂的系统可以采用这种方式
最好的例子是 memory mapping code in linux
持有和等待
原子的去枪锁,不适合封装,而且效率比较低。注意释放的顺序
枪锁
非抢占式
top:
pthread_mutex_lock(&L1);
if(pthread_mutex_try_lock(&L1) != 0 )
{
pthread_mutex_unlock(&L1);
goto top;
}
top2:
pthread_mutex_lock(&L2);
if(pthread_mutex_try_lock(&L2) != 0)
{
pthread_mutex_unlock(&L2);
goto top;
}
这种情况下会造成活锁,livelock, 线程1 线程2 都活着,都会得到调度,但是也没有什么作为。避免活锁,可以在一个循环结束后,等带一段时间。实际是错开调度。
如果在上了L2 锁之后 allocate 了一些内存,则在 trylock 失败的时候需要free这些资源。
互斥性
从硬件上支持原子操作,比如比较和交换,硬件指令实现了下述功能
//相等才交换,不相等不交换
int CompareAndSwap(int * address, in expected, int new) {
if( *address == expected) {
*address = new;
return 1; // success;
}
return 0;
}
//原子的增加 - 无等待实现
void AtomicIncrement(int * value, int amount) {
do {
int old = value;
}while(CompareAndSwqp(value, old, old + amount) == 0);
}
说实话没太明白这一章的意义,用原子操作替换锁?可能是这个意思吧
另一个例子,看看就好
//traditional lock:
void insert(int value) {
node_t * n = malloc(sizeof(node_t));
assert(n!=NULL);
n->value = value;
pthread_mutex_lock(listlock);
n->next = head;
head = n;
ptherad_mutex-lock(listlock);
}
//atomlock
void insert(int value) {
node_t *n = malloc(sizeof(node_t));
assert(n!=NULL);
n->value = value;
do{
n->next = head;
}while(CompareAndSwap(&head, n->next, n) == 0);
}
通过调度避免死锁
比如 4根线程 在两个CPU 上运行。只有两把锁,4根线程并不都需要锁。所以可以错开调度。
先了解到这里吧,遇到再说
检查于重启
有些程序和系统中有死锁检查器的存在,允许一定偶尔的死锁。只要重启就好。
网友评论