线程同步

线程同步

1. 互斥锁mutex(临界区)


锁类型

初始化方式

加解锁特征

调度特征

普通锁

PTHREAD_MUTEX_TIMED_NP

PTHREAD_MUTEX_INITIALIZER

同一线程可重复加锁,解锁一次释放锁

先等待锁的进程先获得锁

嵌套锁

PTHREAD_MUTEX_RECURSIVE_NP

PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP

同一线程可重复加锁,解锁同样次数才可释放锁

先等待锁的进程先获得锁

纠错锁

PTHREAD_MUTEX_ERRORCHECK_NP

PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP

同一线程不能重复加锁,加上的锁只能由本线程解锁

先等待锁的进程先获得锁

自适应锁

PTHREAD_MUTEX_ADAPTIVE_NP

PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP

同一线程可重加锁,解锁一次生效

所有等待锁的线程自由竞争

 
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
int pthread_mutex_init(pthread_mutex_t *restrict mutex, pthread_mutexattr_t *restrict attr);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

如果mutex变量是静态分配的(全局变量或static变量),也可以用宏定义PTHREAD_MUTEX_INITIALIZER来初始化,相当与用pthread_mutex_init()初始化并且设置attr为NULL。

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

trylock失败,返回EBUSY。

#include <pthread.h>
#include <time.h>
int pthread_mutex_timedlock(pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);

超时指定愿意等待的绝对时间(与相对时间对比而言,指定在时间X之前可以阻塞等待,而不是说愿意阻塞Y秒)。这个超时时间是用timespec结构来表示,它用秒和纳秒来描述时间。

为了实现互斥操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另外一个处理器的交换指令只能等待总线周期。

    pthread_mutex_lock(&m);
    while(a <= 0)
        TODO
    pthread_mutex_unlock(&m);

while多执行一次,检测a是否大于0。多线程执行时,一线程响应后a=0;其余线程执行a<=0时阻塞。

上述code在一直执行如下操作:获取锁----条件不满足----释放锁----获取锁----条件不满足----释放锁......,因此引入条件变量。

2. 条件变量condition variable(同步)

线程间的同步还有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,就唤醒线程A继续执行。

在pthread库中通过条件变量( Condition Variable) 来阻塞等待一个条件,或者唤醒等待这个条件的线程。 Condition Variable用pthread_cond_t类型的变量表示。

#include<pthread.h>
int pthread_cond_destory(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒所有线程
int pthread_cond_signal(pthread_cond_t *cond); //唤醒一个线程

一个条件变量总是和一个mutex搭配使用。

pthread_cond_wait()在一个condtion variable上阻塞等待,这个函数做以下三步:

  1. 释放mutex;
  2. 阻塞等待; 1,2合成原子操作。
  3. 当被唤醒时,重新获得mutex并返回(原子性的)。
    pthread_mutex_lock(&m);
    while(a <= 0)
        TODO
    pthread_mutex_unlock(&m);

while多执行一次,检测a是否大于0。多线程执行时,一线程响应后a=0;其余线程执行a<=0时阻塞。

上述code在一直执行如下操作:获取锁----条件不满足----释放锁----获取锁----条件不满足----释放锁......,可引入条件变量。

    pthread_mutex_lock(&m);
    while(a <= 0)
        pthread_cond_wait(&c, &m);
    pthread_mutex_unlock(&m);

 参考:互斥锁和条件变量 深入理解pthread_cond_wait、pthread_cond_signal

pthread_cond_timedwait()函数用于在同时等待条件变量时提供超时功能,不过该函数的超时时间是一个绝对时间。默认使用系统时间,这意味这,若修改系统时间,那么超时就不准确,有可能提前返回,也可能要几年才返回。这在某些需求下会导致bug:可通过设置条件变量的时钟源(CLOCK_MONOTONIC)解决。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>

int main()
{
    pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
    pthread_condattr_t attr;
    pthread_cond_t cond;
    pthread_condattr_init(&attr);
    pthread_condattr_setclock(&attr, CLOCK_MONOTONIC);
    pthread_cond_init(&cond, &attr);
    struct timespec tv;
    pthread_mutex_lock(&m);
    do{
        clock_gettime(CLOCK_MONOTONIC, &tv);
        tv.tv_sec += 1;
        pthread_cond_timedwait(&cond,&m,&tv);
        printf("heart beat\n");
    }while(1);
    pthread_mutex_unlock(&m);
    return 0;
}

3. 信号量semaphore

由于互斥锁的粒度比较大,如果我们希望在多个线程间对某一对象的部分数据进行共享,使用互斥锁是没有办法实现的,只能将整个数据对象锁住,这样虽然达到了多线程操作共享数据时保证数据正确性的目的,却无形中导致线程的并发性下降,线程从并行执行,变成了串行执行,与直接使用单进程无异。

若要对部分数据实现互斥共享,此种情况下应使用信号量。

信号量可应用于进程间,也可应用于线程间,推荐使用posix 信号量。

参考:IPC之信号量 Posix信号量 Dale工作学习笔记 POSIX信号量

4. 读写锁

只要有一个线程可以改写数据,就必须考虑线程见同步问题,引入了读者写者锁(Reader-Writer Lock)。Linux:使用读写锁使线程同步

5. 死锁

在多线程编程中,我们为了防止多线程竞争共享资源而导致数据错乱,都会在操作共享资源之前加上互斥锁,只有成功获得到锁的线程,才能操作共享资源,获取不到锁的线程就只能等待,直到锁被释放。

死锁产生的必要条件:

互斥条件:即当资源被一个线程使用(占有)时,别的线程不能使用

不可抢占:资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。

请求和保持条件:即当资源请求者在请求其他的资源的同时保持对原有资源的占有。

循环等待条件:即存在一个等待队列:T1占有T2的资源,T2占有T3的资源,T3占有T1的资源。这样就形成了一个等待环路。

常见造成死锁场景:

1)加锁之后忘记解锁;

2)重复加锁,造成死锁;

3)程序中有多个共享资源,因此有很多锁,随意加锁,导致相互被阻塞

解决死锁的方法

死锁的处理逻辑也非常好想,因为上述四个条件都是死锁出现的必要条件,缺一不可,那么我们在设计程序时,只要破坏其中一项条件就可以不让死锁发生。

(1)一次性申请所有资源,此时就不会再占有一个资源时再去申请其他资源,即解决了死锁条件中的持有并等待条件这一条件。

不过这样的解决方法会出现两个缺点:①需要宏观设计,即要想到所有需要的资源,造成编程困难;②许多资源要很久后才会用到,造成资源浪费。

(2)对资源按照类型进行排序,资源申请必须按序进行,即解决了死锁条件中的环路等待条件这一条件。

这样也会导致资源浪费的情况。

(3)死锁检测。通过“银行家算法”,对于每一次的请求判断是否会出现死锁,从而产生一个能够不产生死锁的安全序列。这样会导致,程序的时间复杂度T=O(m*n^2)较高。

若通过改进的“银行家算法”,即不是每一次的请求都要判断,而是当出现了死锁之后,进行回滚,回滚至死锁出现前的情况。这样又会导致,比如现在进程是将文件写入磁盘,如果文件已经写入了,难道还要将文件再回滚到没写入之前的情况嘛。

(4)死锁忽略。因为死锁这一情况本身的出现概念是极低的,而且对于一般的个人PC机来说,一次重启即可以轻松解决,因此在Windows和Linux系统中,对于死锁的处理都是采用死锁忽略这一方法。

代码改进

1)避免多次锁定,多检查。

2)对共享资源访问完毕之后,一定要解锁,或者在加锁的使用 trylock。

3)如果程序中有多把锁,可以控制对锁的访问顺序 (顺序访问共享资源,但在有些情况下是做不到的),另外也可以在对其他互斥锁做加锁操作之前,先释放当前线程拥有的互斥锁。

4)项目程序中可以引入一些专门用于死锁检测的模块。

参考:

1.Linux下多线程编程详解简介

2.「Linux」多线程详解,一篇文章彻底搞懂多线程中各个难点

posted @ 2015-12-23 22:38  yuxi_o  阅读(242)  评论(0编辑  收藏  举报