07-系统编程(线程的同步与互斥)

线程的同步与互斥




一、线程互斥方式 — 互斥锁 /互斥量

1.互斥锁及特点

互斥锁是专门用于处理线程互斥的一种方式,它有两种状态:上锁状态/解锁状态。
特点:如果互斥锁处于上锁状态,那么再上锁就会造成阻塞,直到这把锁解开了之后,才能上锁。解锁状态依然继续解锁,不会阻塞

2.关于线程互斥锁函数接口

1)定义互斥锁变量 -> 数据类型: pthread_mutex_t
2)初始化互斥锁 -> pthread_mutex_init()

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
参数:
    mutex: 未初始化过互斥锁变量的地址
    mutexattr:普通属性,NULL
返回值:
    成功:0
    失败:非0错误码
静态初始化:
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
也就是说,以上这句话等价于:
pthread_mutex_t m;
pthread_mutex_init(&m,NULL);

3)上锁-> pthread_mutex_lock()

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
参数:
    mutex:互斥锁变量的地址
返回值:
    成功:0
    失败:非0错误码
4)解锁  -> pthread_mutex_unlock()
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:
    mutex:互斥锁变量的地址
返回值:
    成功:0
    失败:非0错误码

4)解锁 -> pthread_mutex_unlock()

#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
参数:
    mutex:互斥锁变量的地址
返回值:
    成功:0
    失败:非0错误码

5)销毁互斥锁 -> pthread_mutex_destroy()

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
参数:
    mutex:互斥锁变量的地址
返回值:
    成功:0
    失败:非0错误码

6)互斥锁使用场景:
当我们使用一些临界资源时,防止多个线程同时访问,我们可以这么做,在访问临界资源前,
让线程先上锁,然后再访问资源,访问完了之后就解锁,让别的线程去上锁。
说明:
临界资源:共享资源(多线程之间需要共同操作的资源)

代码解释:

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

int g_val = 10;

//1、先定义一个互斥锁变量
pthread_mutex_t mutex;


void* start_routine1(void*arg)
{
    //在访问共享资源的时候先上锁 
    pthread_mutex_lock(&mutex);

    g_val = 20;
    sleep(1);
    printf("start_routine1 g_val:%d\n",g_val);

    //共享资源 使用结束的时候要解锁
    pthread_mutex_unlock(&mutex);
}

void* start_routine2(void*arg)
{
    //在访问 共享资源的时候 先 上锁   --如果没有拿到锁,会阻塞 等待  有锁
    pthread_mutex_lock(&mutex);

    g_val = 200;
    sleep(2);
    printf("start_routine2 g_val:%d\n",g_val);

    //共享资源 使用结束的时候要解锁
    pthread_mutex_unlock(&mutex);
}

int main()
{
    //2、初始化互斥锁
    pthread_mutex_init(&mutex,NULL);

    //1、子线程1 
    pthread_t thread1;
    pthread_create(&thread1,NULL,start_routine1, NULL);

    //2、子线程2
    pthread_t thread2;
    pthread_create(&thread2,NULL,start_routine2, NULL);

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    //销毁锁 
    pthread_mutex_destroy(&mutex);

    return 0;
}

小练习: 使用互斥锁 +多线程实现整形矩阵(二维整形数组) 所有元素求和问题
定义一个全局变量total累计 所有元素的和

#define N   5
int  array[N][N] = {10,20,30,40,50,
                    11,22,33,44,55,
                    66,77,88,99,11,
                    12,31,12,34,45,
                    12,43,45,66,77};
数组的初始化可以直接 定义的时候初始化 或者 使用 随机数初始化
每一条线程负责计算一行的元素和 ,最后所有线程的元素和相加就是整形矩阵(二维整形数组) 所有元素和
#include <stdio.h>
#include <string.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>

#define N   5
int  array[N][N] = {10,20,30,40,50,
                    11,22,33,44,55,
                    66,77,88,99,11,
                    12,31,12,34,45,
                    12,43,45,66,77};
数组的初始化可以直接定义的时候初始化或者使用随机数初始化
每一条线程负责计算一行的元素和 ,最后所有线程的元素和相加就是 整形矩阵(二维整形数组)所有元素和
#endif

#define N   5
int  array[N][N] = {10,20,30,40,50,
                    11,22,33,44,55,
                    66,77,88,99,11,
                    12,31,12,34,45,
                    12,43,45,66,77};

//定义一个互斥锁变量
pthread_mutex_t mutex;
int total = 0;

void *start_routine(void*arg)
{
    //获取当前子线程 要计算第几行的数据
    int row = *(int*)arg;
    int sum=0;
     
    for(int i=0; i<N; i++){
        sum += array[row][i];
    }
    printf("[%lu]子线程 计算第%d行 sum:%d total:%d  start\n",pthread_self(),row,sum,total);

    pthread_mutex_lock(&mutex);//加锁力度要小
    total +=sum;
    pthread_mutex_unlock(&mutex); //解锁

    printf("[%lu]子线程 计算第%d行 sum:%d total:%d  end\n",pthread_self(),row,sum,total);
}

int main()
{
    //初始化互斥锁
    pthread_mutex_init(&mutex,NULL);

    pthread_t thread[N];
    //有多少行 就创建多少条线程
    for(int i=0; i<N; i++)
    {
        pthread_create(&thread[i],NULL,start_routine, &i);
        //usleep(1); //1us 
        sleeep(1);
    }

    for(int i=0; i<N; i++)
    {
        pthread_join(thread[i], NULL);
    }
   
    pthread_mutex_destroy(&mutex);

    return 0;
    }

二、问题的引入

代码解释:

#include<stdio.h>
#include<pthread.h>
#include<semaphore.h>

int g_val = 0;
//1)定义互斥锁变量。 -----》数据类型  pthread_mutex_t 
pthread_mutex_t mutex;

//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine1(void *arg)
{
    pthread_mutex_lock(&mutex);//上锁
    //写操作,修改内存空间的值 
    g_val = 100;
    int i;

    for(i=0; i<5; i++)
    {
        sleep(1);
        g_val += g_val*i;
        printf("%d  routine1 100  g_val:%d\n",i,g_val);
    }

    pthread_mutex_unlock(&mutex);//解锁
}

//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine2(void *arg)
{
    pthread_mutex_lock(&mutex);//上锁
    //写操作,修改内存空间的值 
    g_val = 200;
    int i;

    for(i=0; i<5; i++)
    {
        sleep(1);
        g_val += g_val*i;
        printf("routine2 200  g_val:%d\n",g_val);
    }

    pthread_mutex_unlock(&mutex);//解锁
}

//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine3(void *arg)
{
    pthread_mutex_lock(&mutex);//上锁
    int i;
    //读操作,此时仅仅只是将这个值打印出来
    for(i=0; i<5; i++)
    {
        sleep(1);
        printf("routine3  g_val:%d\n",g_val);
    }

    pthread_mutex_unlock(&mutex);//解锁
}
//线程的例程函数,也就是创建线程之后,去执行这个函数
void* routine4(void *arg)
{
    pthread_mutex_lock(&mutex);//上锁
    int i;
    //读操作,此时仅仅只是将这个值打印出来
    for(i=0; i<5; i++)
    {
        sleep(1);
        printf("routine4  g_val:%d\n",g_val);
    }

    pthread_mutex_unlock(&mutex);//解锁
}

int main()
{
    //2)初始化 互斥锁
     pthread_mutex_init(&mutex,NULL);
      
    // 创建一个新的线程1
    pthread_t thread1;
    pthread_create(&thread1,NULL,routine1,NULL);  
    // 创建一个新的线程2
    pthread_t thread2;
    pthread_create(&thread2,NULL,routine2,NULL);
    // 创建一个新的线程3
    pthread_t thread3;
    pthread_create(&thread3,NULL,routine3,NULL);  
    // 创建一个新的线程4
    pthread_t thread4;
    pthread_create(&thread4,NULL,routine4,NULL);

    //接合子线程 --阻塞等待子线程退出 回收资源
    pthread_join(thread1,NULL);
    pthread_join(thread2,NULL);
    pthread_join(thread3,NULL);
    pthread_join(thread4,NULL);
    //5)销毁互斥锁
    pthread_mutex_destroy(&mutex);
    return 0;
}

结果:
为了提高效率,有没有什么方法可以让两条线程在进行读操作的时候,可以同时进行呢?而且在进行读操作的时候不可以进行写操作。写操作之间是互斥的;写操作与读操作之间是互斥的;读操作之间是同步的;
答案:互斥锁无法做到,可以使用读写锁

三、读写锁

1.互斥锁的缺陷

互斥锁无论是读取共享资源,还是修改共享资源,都要上锁,而是在上锁期间,不能被别的线程上锁

2.读写锁的优势

访问资源(一起读一本书) -> 同时上读锁 -> 读锁就是一把共享锁
修改资源 -> 不能同时上写锁 -> 写锁就是一把互斥锁
这把既有读锁,又有写锁的锁,就称之为读写锁

3.读写锁函数接口

1)定义一个读写锁变量 (数据类型: pthread_rwlock_t)
pthread_rwlock_t rwlock;
2)初始化读写锁—>pthread_rwlock_init

#include <pthread.h>
int pthread_rwlock_init(pthread_rwlock_t * rwlock,const pthread_rwlockattr_t * attr);
参数:
    rwlock:读写锁变量的地址
    attr:属性,一般为NULL
返回值:
    成功:0
    失败:非0错误码

静态初始化:
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;

3)读锁上锁—>pthread_rwlock_rdlock

#include <pthread.h>
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
参数:
    rwlock:读写锁变量的地址

4)写锁上锁 —>pthread_rwlock_wrlock

int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
参数:
    rwlock:读写锁变量的地址

5)读写锁解锁 —>pthread_rwlock_unlock

#include <pthread.h>
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
参数:
    rwlock:读写锁变量的地址

6)销毁读写锁---->pthread_rwlock_destroy

#include <pthread.h>
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
参数:
    rwlock:读写锁变量的地址

代码解释:

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

//定义一个读写锁变量
pthread_rwlock_t rwlock;

int g_val = 10;

void *start_routine1(void*arg)
{
    pthread_rwlock_wrlock(&rwlock);//加写锁

    g_val = 20;
    sleep(3);
    printf("start_routine1: %d\n",g_val);

    pthread_rwlock_unlock(&rwlock); //解锁
}

void *start_routine2(void*arg)
{
    pthread_rwlock_wrlock(&rwlock);//加写锁

    g_val = 200;
    sleep(3);
    printf("start_routine2: %d\n",g_val);

    pthread_rwlock_unlock(&rwlock); //解锁
}
void *start_routine3(void*arg)
{
    pthread_rwlock_rdlock(&rwlock);//加读锁

    //获取数据 访问(读取) 共享资源但是没有进行修改(写入)
    int cnt=5;
    while(cnt--){
        sleep(1);
        printf("start_routine3: %d\n",g_val);
    }

    pthread_rwlock_unlock(&rwlock); //解锁
}

void *start_routine4(void*arg)
{
    pthread_rwlock_rdlock(&rwlock);//加读锁

    //获取数据 访问(读取) 共享资源 但是没有进行修改(写入)
    int cnt=5;
    while(cnt--){
        sleep(1);
        printf("start_routine4: %d\n",g_val);
    }

    pthread_rwlock_unlock(&rwlock); //解锁
}

int main()
{
    //初始化读写锁
    pthread_rwlock_init(&rwlock,NULL);

    pthread_t thread1;
    pthread_create(&thread1,NULL,start_routine1, NULL);
    pthread_t thread2;
    pthread_create(&thread2,NULL,start_routine2, NULL);
    pthread_t thread3;
    pthread_create(&thread3,NULL,start_routine3, NULL);
    pthread_t thread4;
    pthread_create(&thread4,NULL,start_routine4, NULL);


    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    pthread_join(thread3, NULL);
    pthread_join(thread4, NULL);
   
    //销毁读写锁
    pthread_rwlock_destroy(&rwlock);

    return 0;
}

现象:
两条读线程同时进行,两条写线程不能同时进行。
读取的值,拿到的是最后一次临界资源的值,但是可以保证在读取的过程中临界资源的值是不会被修改。
总结重点;
写操作之间是互斥的;写操作与读操作之间是互斥的;读操作之间是同步的;

三、条件变量(条件变量必须与互斥锁一起使用)

1.条件变量

线程因为某一个条件/情况不成立下,进入一个变量中等待,这个存放线程的变量就是条件变量。(条件变量必须与互斥锁一起使用)

2.关于条件变量的函数接口

1)先定义一个条件变量。 -> 数据类型: pthread_cond_t
pthread_cond_t cond; //condition
2)初始化条件变量

#include <pthread.h>
int pthread_cond_init(pthread_cond_t *cond,const pthread_condattr_t * attr);

pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //静态初始化
参数:
    cond:条件变量的地址
    cond_attr 普通属性,NULL。
返回值:
    成功:0
    失败:非0错误码

3)进入条件变量中等待(两个功能:1.阻塞等待 2.自动解锁)

int pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex);
参数:
    cond:条件变量的地址
    mutex:互斥锁的地址   -> 进入条件变量中,会自动解锁。
返回值:
    成功:0
    失败:非0错误码

4)唤醒条件变量中等待的线程 -> 线程离开条件变量时,会自动上锁

#include <pthread.h>
int pthread_cond_broadcast(pthread_cond_t *cond);//广播: 唤醒所有在条件变量中等待的线程
int pthread_cond_signal(pthread_cond_t *cond);//单播: 随机唤醒一个在条件变量中等待的线程
参数:
    cond:条件变量的地址
返回值:
    成功:0
    失败:非0错误码

5)销毁条件变量---->pthread_cond_destroy

#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
参数:
    cond:条件变量的地址

代码解释:

#include <stdio.h>
#include <sys/types.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <signal.h>
#include <sys/sem.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <semaphore.h>

/*
练习:有4个小孩,每个小孩的任务就是领取生活费1000,回学校之前,父母先在银行卡中存2000块钱,
2个线程拿到1000块钱之后退出,另外2个线程进去条件变量中等待,父亲再打钱1000,唤醒所有的小孩
起来拿钱, 过一会,再打1000块钱,再唤醒最后一个小孩起来拿钱赶紧走人上学。
*/

int g_money = 2000;
//定义一个互斥锁变量
pthread_mutex_t mutex;
//定义一个条件变量并且静态初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

void *start_routine(void*arg)
{
    printf("[%lu]子线程 start\n",pthread_self());

    pthread_mutex_lock(&mutex);//加锁力度要小

    //条件不满足的时候进入 条件变量中等待
    while(g_money<1000){
        printf("没钱了,进去条件变量中等待父母打钱 并且通知.....\n");
        //自动解锁 ,并且阻塞等待  
        pthread_cond_wait(&cond,&mutex);
        printf("父母打钱过来了,已经通知我了,此时余额:%d\n",g_money);
    } 

    //走到这里,说明有钱
    g_money -=1000;
    printf("[%lu]子线程 拿到钱了,此时银行卡余额:%d\n",pthread_self(),g_money);
        
    pthread_mutex_unlock(&mutex); //解锁

    printf("[%lu]子线程 end\n",pthread_self());

    //拿钱走人
    pthread_exit(NULL);
}

int main()
{
    //初始化互斥锁
    pthread_mutex_init(&mutex,NULL);

    pthread_t thread1; //我
    pthread_create(&thread1,NULL,start_routine, NULL);
    pthread_t thread2; //哥
    pthread_create(&thread2,NULL,start_routine, NULL);
    pthread_t thread3; //姐
    pthread_create(&thread3,NULL,start_routine, NULL);
    pthread_t thread4; //弟
    pthread_create(&thread4,NULL,start_routine, NULL);


    int cnt=5;
    while(cnt--){
        sleep(1);
        printf("主线程(父母) 即将准备打钱....%d\n",cnt);
    }
    //主线程(父母)  打钱
    pthread_mutex_lock(&mutex);//加锁
    g_money +=1000;
    pthread_mutex_unlock(&mutex); //解锁
    pthread_cond_broadcast(&cond);//广播: 唤醒所有在条件变量中等待的线程

    cnt=5;
    while(cnt--){
        sleep(1);
        printf("主线程(父母) 即将准备打钱....%d\n",cnt);
    }
    pthread_mutex_lock(&mutex);//加锁
    g_money +=1000;
    pthread_mutex_unlock(&mutex); //解锁
    pthread_cond_signal(&cond);//单播: 随机唤醒一个在条件变量中等待的线程

    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);
    pthread_join(thread3, NULL);
    pthread_join(thread4, NULL);


    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cond);
    return 0;
}

四、线程的死锁

1.死锁概念

死锁指的是由于某种逻辑问题,导致等待一把永远无法获得的锁的困境。比如最简单的是同一线程,连续对同一锁资源进行加锁,就进入了死锁

例子:
pthread_mutex_t m;

int main()
{
pthread_mutex_init(&m, NULL);

// 正常加锁
pthread_mutex_lock(&m);

// 未释放锁前重复加锁,进入死锁状态
pthread_mutex_lock(&m);

// 下面的代码永远无法执行
...
...

}

}
以上死锁的例子,可以通过仔细检查代码得以避免,但在现实场景中,有些产生死锁的情况是无法避免的,比如如下情形:
一条线程持有一把锁,期间不能屏蔽取消指令然后又恰巧被取消指令强制终止,此时死锁的产生变得不可避免

void *routine(void *arg)
{
thread_pool *pool = (thread_pool *)arg;
struct task *p;

while(1)
{
    // 操作临界资源之前,加锁
pthread_mutex_lock(&pool->lock);

    // 条件不允许时,进入条件量等待
while(pool->waiting_tasks == 0 && !pool->shutdown)
    pthread_cond_wait(&pool->cond, &pool->lock);

    // 条件允许时,操作临界资源
p = pool->task_list->next;
pool->task_list->next = p->next;
pool->waiting_tasks--;

    // !!! 注意 !!!
    // 线程若恰好在此处被意外终止,将导致死锁

    // 解锁
pthread_mutex_unlock(&pool->lock);

    // 其他操作
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
(p->do_task)(p->arg);
pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);

free(p);
}

pthread_exit(NULL);

}

2.死锁的解决办法(添加线程取消例程函数)

上述代码中,若线程在中间被取消,则导致死锁。对于这种情况,一个可行的解决办法是:
1.提前准备一个解锁处理函数,并将其压入线程专用的函数栈中备用。
2.准备操作临界资源,加锁
3.操作临界资源

  • 重点:
  • 若线程在此期间意外终止,则会自动调用处理函数解锁(线程取消例程函数)
    4.解锁
    5.在函数栈中弹出处理函数。
    说明:
    上述做法实际上相当于现实生活中的立遗嘱,因为人去世之后是无法再做任何事情的,因此为了防止死亡在关键阶段意外到来,可以在提前立遗嘱,万一不幸遇到该情况就有了预案(处理函数),但如果并未发生此种情形,那么就将遗嘱作废(弹出处理函数且不执行)即可
// 意外处理函数:
// 自动解锁
void handler(void *arg)
{
    pthread_mutex_unlock((pthread_mutex_t *)arg);
}
void *routine(void *arg)
{
    thread_pool *pool = (thread_pool *)arg;
    struct task *p;

    while(1)
    {
        //================================================//
        pthread_cleanup_push(handler, (void *)&pool->lock); // 提前准备好意外处理函数
        pthread_mutex_lock(&pool->lock);
        //================================================//

        // 1, no task, and is NOT shutting down, then wait
        while(pool->waiting_tasks == 0 && !pool->shutdown)
            pthread_cond_wait(&pool->cond, &pool->lock);

        // 2, no task, and is shutting down, then exit
        if(pool->waiting_tasks == 0 && pool->shutdown == true)
        {
            pthread_mutex_unlock(&pool->lock);
            pthread_exit(NULL); // CANNOT use 'break';
        }

        // 3, have some task, then consume it
        p = pool->task_list->next;
        pool->task_list->next = p->next;
        pool->waiting_tasks--;

        //================================================//
        pthread_mutex_unlock(&pool->lock); 
        pthread_cleanup_pop(0); // 弹出处理函数且不执行
        //================================================//

        pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
        (p->do_task)(p->arg);
        pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);

        free(p);
    }

    pthread_exit(NULL);
}

总结

下篇线程池…

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值