美文网首页
线程(六):常见的并发问题

线程(六):常见的并发问题

作者: 404Not_Found | 来源:发表于2021-04-29 08:15 被阅读0次

作者: 雪山肥鱼
时间: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);
}

生产中遇到的多线程问题会比上面提到的复杂很多。

死锁

死锁常见情况是,上锁的顺序不同。解决当然也简单,上锁顺序相同即可。
但是为什么解决方案这么简单,但是依然会出现死锁问题呢?

  1. 代码基数
  2. 模块之间复杂的依赖性
  • 虚拟内存向要从磁盘向内存加载一个block,那么就要访问文件系统
  • 文件系统随后会向内存申请一页,将block写入内存,随后于虚拟内存进行交互
  1. 模块化注定会有封装,那么隐藏的多线程问题就会出现。这种bug并非恶意的。

死锁产生的条件

  1. 互斥性:线程对于资源申请互斥的control, 例如去grab 锁
  2. 持有和等待:当线程等待另一个资源的时候,她手上有锁,另一个资源等不到,那么她就不会放手,死锁案发生。也就是说手上有锁,又在等其他的资源。
  3. 非抢占式: locks 不会被强行剥夺
  4. 循环等待:线程之间存在一个环路,环路上每个线程都额外持有一个资源,而这个资源又是下一个线程要申请的。

4个只要有一个不存在,死锁就不会发生。

Prevention 预防

循环和等待

  1. total ordering 全序
    对于简单的系统可以保证全局上锁有序
  2. 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根线程并不都需要锁。所以可以错开调度。
先了解到这里吧,遇到再说

检查于重启

有些程序和系统中有死锁检查器的存在,允许一定偶尔的死锁。只要重启就好。

相关文章

网友评论

      本文标题:线程(六):常见的并发问题

      本文链接:https://www.haomeiwen.com/subject/kmzirltx.html