Linux下互斥与死锁


一、同步、竞争、互斥

当多个线程同时访问其共享的资源时,需要相互协调,以防止出现数据不一致、不完整的问题,能达到这种状态叫线程同步。
而有些资源在同一时刻只有一个线程访问,对于这种资源的访问需要竞争。
当资源获取到后,能够防止资源被其他线程抢占(再次获取)的技术叫互斥。

二、互斥量(锁)

pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;
int pthread_mutex_init (pthread_mutex_t *__mutex, __const pthread_mutexattr_t *__mutexattr)
功能:初始化互斥量,使用第二互斥量来初始化第一个互斥量,如果第二个为空,则使用默认参数初始化互斥量,也可以使用宏来初始化。
int pthread_mutex_destroy (pthread_mutex_t *__mutex)
功能:销毁互斥量
注意:互斥量是一个结构体,里面有成员是指针,指向了堆内存数据,需要显式初始化函数以及销毁函数。
如果使用堆内存存储互斥量,需要在调用了销毁函数后,再进行free。

int pthread_mutex_lock (pthread_mutex_t *__mutex)
功能:锁定互斥量,当互斥量是锁定状态,此函数则阻塞(直到互斥量在其他线程中解锁,调用者线程加锁成功才返回)。
注意:互斥量一旦加锁,只有它自己能解。

int pthread_mutex_trylock (pthread_mutex_t *__mutex)
功能:尝试锁定互斥量,能锁就锁,不能锁就返回。

int pthread_mutex_timedlock (pthread_mutex_t *__restrict __mutex, __const struct timespec *__restrict __abstime)
功能:在指定时间内锁定一个互斥量(由于系统原因导致不可知锁的状态),而一旦获取锁的状态后立即做出抉择。

struct timespec
{
    __time_t tv_sec;
    long int tv_nsec;
}

int pthread_mutex_unlock (pthread_mutex_t *__mutex)
功能:解锁

测试:

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

pthread_mutex_t key = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mouse = PTHREAD_MUTEX_INITIALIZER;

void* ming(void* arg)
{
    pthread_mutex_lock(&key);
    usleep(100);
    pthread_mutex_lock(&mouse);
    printf("键盘和鼠标都拿到了,可以开心的打游戏了\n");
    sleep(5);
    pthread_mutex_unlock(&mouse);
    pthread_mutex_unlock(&key);
}

void* huang(void* arg)
{
    pthread_mutex_lock(&key);
    usleep(100);
    pthread_mutex_lock(&mouse);
    printf("键盘和鼠标都拿到了,可以开心的做ppt了\n");
    sleep(5);
    pthread_mutex_unlock(&mouse);
    pthread_mutex_unlock(&key);
}

int main()
{
    pthread_t pid1;
    pthread_create(&pid1,NULL,ming,NULL);

    pthread_t pid2;
    pthread_create(&pid2,NULL,huang,NULL);

    pthread_join(pid1,NULL);
    pthread_join(pid2,NULL);
}

三、死锁

多个线程进行等待对方的资源,在得到所有资源继续运行前,都不会释放自己的资源,这样造成的循环等待现象,称为死锁。
构成死锁的四大必要条件:
    1、资源互斥
    2、占有,还想占有(请求与保持)
    3、资源不可剥夺
    4、环路等待(循环等待)
防止死锁的方法:
    构成死锁的四个条件只要破坏其中一个就构不成死锁,死锁一旦形成就无法消除,因此最后的方法就是避免产生死锁。
    1、破坏互斥条件,让资源能够共享,但缺点是不通过,因为有些资源不能共享,如打印机。
    2、破坏请求并保持,采用预先分配的方法,在进行运行前一次申请好它所需要的所有资源,但缺点是浪费资源。
    3、破坏不可剥夺的条件,对已经占用资源的线程发送取消请求,但是实现比较复杂,而且还会破坏业务逻辑。
    4、破坏循环等待条件,为每个资源进行编号,采用顺序的资源分配方法,规定每个线程必须按照递增的顺序请求资源,缺点是编号必须相对稳定,增加新资源时会比较麻烦,而且有些特殊的业务逻辑不能完全按照指定的顺序分配资源。
避免产生死锁的算法(银行家算法):
    1、申请资源的额度不能超过银行现有资源的总和。
    2、分批向银行,但是货款额度不能超过一开始最大需求量总和。
    3、银行如果不能满足客户的需要,必须及时给出答复
    4、客户必须在规定的时间内还款。
如何检测死锁:
    1、画出资源分配图,并简化,模拟资源分析的过程。
    2、监控线程过程的栈内存使用情况。
    3、设计看门狗机制(TCP心跳包)

四、信号量

线程的信号量与进程的信号量的机制是一样的,但使用方法不同,用于控制、管理线程间的共享资源。
#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:初始化信号量(创建信号量)
sem:信号量ID,输出
pshared:一般为0(线程之间)进程中使用的。
非零表示进程间使用,但Linux不支持。
value:信号量的初始化

int sem_wait(sem_t *sem);
功能:信号量减1,不够减则阻塞(为0时)。

int sem_trywait(sem_t *sem);
功能:信号量减1,不够减则立即返回-1

int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
功能:信号量减1,不够减则阻塞,直到abs_timeout超时返回-1。

struct timespec
{
    __time_t tv_sec;
    long int tv_nsec;
}

int sem_post(sem_t *sem);
功能:信号量+1

int sem_destroy(sem_t *sem);
功能:销毁信号量

int sem_getvalue(sem_t *sem, int *sval);
功能:获取信号量的值

五、生产者与消费者模型

测试代码:

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/time.h>

#define HOUSE_MAX 40
// 栈结构
char house[HOUSE_MAX] = {};
// 栈顶下标
int top = 0;

// 互斥量(确保只有一个线程访问栈顶)
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
// 满仓条件变量(满仓时生产线程睡入)
pthread_cond_t full = PTHREAD_COND_INITIALIZER;
// 空仓条件变量(空仓时消费线程睡入)
pthread_cond_t null = PTHREAD_COND_INITIALIZER;

// 显示仓库
void show_house(char* who,char* op,char ch)
{
    printf("%s:",who);
    for(int i=0; i<=top; i++)
    {
        printf("%c",house[i]);
    }
    printf("%s%c\n",op,ch);
}

// 生产者线程
void* product(void* arg)
{
    char* who = (char*)arg;
    for(;;)
    {
        char ch = 'A' + rand()%26;

        pthread_mutex_lock(&mutex);
        // 醒来后要再次检查是否满仓
        while(HOUSE_MAX <= top)
        {
            printf("%s:满仓\n",who);
            pthread_cond_wait(&full,&mutex);
        }

        show_house(who,"<-",ch);
        // 入仓数据
        house[top++] = ch;
        // 模拟现实情况
        usleep(100000);
        // 解锁
        pthread_mutex_unlock(&mutex);
        // 已经确保仓库不空,通知消费
        pthread_cond_signal(&null);
    }
    return NULL;
}

// 消费者线程
void* consume(void* arg)
{
    char* who = (char*)arg;
    for(;;)
    {
        // 加锁
        pthread_mutex_lock(&mutex);
        // 检查仓库是否空的
        while(0 == top)
        {
            printf("%s:空仓\n",who);
            pthread_cond_wait(&null,&mutex);
        }
        // 消费数据
        char ch = house[--top];
        show_house(who,"->",ch);
        usleep(100000);
        pthread_mutex_unlock(&mutex);
        pthread_cond_signal(&full);
    }
}

int main()
{
    srand(time(NULL));
    pthread_t pid[6] = {};
    pthread_create(&pid[0],NULL,product,"生产1");
    pthread_create(&pid[1],NULL,product,"生产2");
    pthread_create(&pid[2],NULL,product,"生产3");
    pthread_create(&pid[3],NULL,consume,"消费1");
    pthread_create(&pid[4],NULL,consume,"消费2");
    pthread_create(&pid[5],NULL,consume,"消费3");

    for(int i=0; i<6; i++)
    {
        pthread_join(pid[i],NULL);
    }
}

六、条件变量

条件变量可以让线程在满足特定的条件下暂停(睡眠),需要与互斥量配合使用。
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int pthread_cond_init (pthread_cond_t *__restrict __cond, __const pthread_condattr_t *__restrict __cond_attr);
功能:初始化条件变量
cond:待初始化的条件变量
cond_attr:条件变量的属性

int pthread_cond_destroy (pthread_cond_t *__cond);
功能:销毁条件变量

int pthread_cond_wait (pthread_cond_t *__restrict __cond, pthread_mutex_t *__restrict __mutex);
功能:让调用者线程进入睡眠,并解锁一个互斥量
cond:线程睡入的条件变量
mutex:线程睡眠前的要解锁的互斥量(是不是锁定状态没有关系)

int pthread_cond_signal (pthread_cond_t *__cond);
功能:唤醒条件变量中的线程(一个还是多个?)
注意:线程醒的前提条件是互斥量必须是解锁状态的,线程醒前会再次加锁,如果不能加锁就不会醒来。

int pthread_cond_timedwait (pthread_cond_t *__restrict __cond, pthread_mutex_t *__restrict __mutex, __const struct timespec *__restrict __abstime);
功能:让调用者线程进入睡眠,并解锁一个互斥量
注意:不是超时

测试:
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

// 定义并初始化条件变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

void* run(void* arg)
{
    printf("我将进入睡眠...\n");
    pthread_cond_wait(&cond,&mutex);
    printf("我醒了...\n");
}

int main()
{
    pthread_t pid;
    pthread_create(&pid,NULL,run,NULL);
    sleep(3);

    pthread_cond_signal(&cond);
    pthread_join(pid,NULL);
}

七、哲学家就餐问题

百度百科
参考资料:Linux下的user/include中的pthread.h文件

Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source Love丶伊卡洛斯 !
评论
 Previous
宣讲会个人体会 宣讲会个人体会
杭电CVTE宣讲会第一次去参加宣讲会,主要分为3大块,公司介绍自己,你提问公司,公司提问你。宣讲会有福利,这次的宣讲会提供了绿卡(免笔试,直接面试,算是个加分吧。通过线上推广集赞和线下现场抢答来获取)。换句话说就是宣讲会表现好是有很大好处的
2019-09-14
Next 
Linux下线程的相关知识 Linux下线程的相关知识
一、线程基本概念1、线程就是进程中的执行路线,即进程内部的控制序列,或者说是进程的子任务(进程就是正在运行的程序,它是一个资源单位) 2、线程就是轻量级的,没有自己独立的内存资源,使用的是进程的代码段、数据段、bss段、堆(注意:没有栈)、
2019-09-09
  TOC