LinuxC编程——线程的同步与互斥

我们知道,一个进中的所有线程共享进程的资源,所以可以通过在进程中定义全局变量来完成进程中线程间的通信,但是,当在同一内存空间运行多个线程时,要注意一个基本的问题,就是不要让线程之间互相破坏。例如,我们要实现两个线程要更新两个变量的值,一个线程要把两个变量的值都设成0,另一个线程要把两个变量的值都设成1。
如果两个线程同时要做这件事情,结果可能是,一个变量的值是0;另一个变量的值是1。这是因为正好在第1个线程把第1个变量设为0后,时间片到,CPU切换第2个线程,第2个线程将把两个变量都设成1,然后CPU再切换线程,第1个线程恢复运行,把第2个变量设成0。结果就是,一个变量的值是0,另一个变量的值是1。

因此需要同步机制来进行制约。 在System V IPC机制中提供了信号量来实现进程或线程之间的通信。此外按照POSIX标准,POSIX提供了两种类型的同步机制,它们是互斥锁(Mutex)条件变量(condition Variable)

一、同步的概念

同步是指多个 任务(线程)按照约定的顺序相互配合完成一件事。

二、同步机制

2.1 信号量

2.1.1基础概念

  • 通过信号量实现同步操作;由信号量来决定线程是继续运行还是阻塞等待
  • 信号量代表某一类资源,其值表示系统中该资源的数量,信号量值>0,表示有资源可以用,可以申请到资源,继续执行程序;信号量值<=0,表示没有资源可以用,无法申请到资源,阻塞。
  • 信号量是一个受保护的变量,只能通过三种操作来访问:初始化sem_init、P操作(申请资源)sem_wait、V操作(释放资源)sem_post

2.1.2 函数接口

  1. 初始化信号量:sem_init
    int sem_init(sem_t *sem, int pshared, unsigned int value)

    • 功能:初始化信号量
    • 参数:
      • sem:初始化的信号量对象
      • pshared:信号量共享的范围(0:线程间使用 ,非0:进程间使用)
      • value:信号量初值
    • 返回值:成功 0;失败 -1
  2. 申请资源:sem_wait
    int sem_wait(sem_t *sem)

    • 功能:申请资源 P操作
    • 参数:sem:信号量对象
    • 返回值:成功;失败 -1

    此函数执行过程,当信号量的值大于0时,表示有资源可以用,则继续执行,同时对信号量减1;当信号量的值等于0时,表示没有资源可以使用,函数阻塞。

  3. 释放资源:sem_post
    int sem_post(sem_t *sem)

    • 功能:释放资源 V操作
    • 参数:sem:信号量对象
    • 返回值:成功 0;失败 -1

    注:释放一次信号量的值加1,函数不阻塞

2.1.3 例子

  1. 测试信号量<0时,进程(线程)阻塞
    在这里插入图片描述
  2. 通过线程实现数据的交互,主线程循环从终端输入,线程函数将数据循环输出,即输入一行数据打印一行数据,当输入quit结束程序。
/*
    练习:使用信号量实现同步,即通过线程实现数据的交互,主线程循环从终端输入,
        线程函数将数据循环输出,当输入quit结束程序。
    要点:
        信号量初值的设定:初始化信号量为0,是为了让打印线程开始申请不到资源
*/
#include <stdio.h>
#include <pthread.h>
#include <string.h>
#include <semaphore.h> 

char buf[32] = {0};
sem_t sem;

void *handler(void *arg) //线程函数循环输出
{
    while (1)
    {
        sem_wait(&sem); //P  申请资源
        if(strcmp(buf,"quit")==0)
        {
            pthread_exit(NULL);
        }
        printf("buf:%s\n",buf);
    }
    return NULL;
}
int main(int argc, char const *argv[])
{
    pthread_t tid; //创建线程
    if(pthread_create(&tid,NULL,handler,NULL) != 0)
    {
        perror("pthread_create err");
        return -1;
    }
    //初始化信号量为0,是为了让打印线程开始申请不到资源
    if(sem_init(&sem,0,0)<0)
    {
        perror("sem_init err\n");
        return -1;
    }
    while(1)
    {
        // scanf("%s",buf);
        fgets(buf,32,stdin);
        if(buf[strlen(buf)-1]=='\n')
            buf[strlen(buf)-1] = '\0';
        sem_post(&sem); // V  释放资源
        if(strcmp(buf,"quit")==0)
        {
            break;
        }
    }
    pthread_join(tid, NULL);
    sem_destroy(&sem);
    return 0;
}

运行结果:
在这里插入图片描述

注意:
(1)sem_t sem定义的信号量在主线程和新线程都要使用,故要定义为全局变量。
(2)信号量初值的设定:初始化信号量为0,是为了让打印线程开始申请不到资源,使得主线程获取到数据后先执行。
(3)在终端获取数据可以用scanf或者fgets,但是要注意一点就是使用fgets时,若不加处理会出现输入quit不能终止程序执行,原因是fgets会将换行作为字符捕获,这时的buf内容为quit\n\0,在使用strcmp(buf,“quit”)进行比较时,quit\n\0和quit\0并不相等。处理方法可以是将buf中quit\n\0的倒数第二个字符’\n’给替换掉,使得strcmp(buf,“quit”)可以比较成功。

  1. 通过两个线程实现数组倒置,线程1用于循环倒置,线程2用于循环打印,要满足导致一次打印一次,用信号量实现同步
/*练习:.通过两个线程实现数组倒置,线程一用于循环倒置,线程二用于循环打印。
用信号量实现同步
int a[10] = {0,1,2,3,4,5,6,7,8,9};
*/
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
sem_t sem1;

void *reverse(void *arg)
{
    int temp;
    while(1)
    {
        for (int i = 0, j = 9; i < j; i++, j--)
        {
            temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
        sem_post(&sem1);
        sleep(1); //添加延时,保证数组倒置完成
    }
    pthread_exit(NULL);
    return NULL;
}
void *print(void *arg)
{
    while(1)
    {
        sem_wait(&sem1);
        for (int i = 0; i < 10; i++)
        {
            printf("%d ", a[i]);
        }
        printf("\n");
    }
    pthread_exit(NULL);
    return NULL;
}

int main(int argc, char const *argv[]) //主进程
{
    pthread_t tid1;
    pthread_t tid2;

    if (pthread_create(&tid1, NULL, reverse, NULL) != 0)
    {
        perror("pthread_create tid1 error");
        return -1;
    }
    if (pthread_create(&tid2, NULL, print, NULL) != 0)
    {
        perror("pthread_create tid2 error");
        return -1;
    }
    if(sem_init(&sem1,0,0)<0)
    {
        perror("sem1_init err\n");
        return -1;
    }
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

注🚨:这个程序中在reverse线程函数中添加了一个1秒的延时,是为了在完成一次数组倒置信号量加一(P操作),等待print线程申请到资源并接着打印数组。若不加延时,则可能会出现reverse线程上来就执行了多次数组倒置,信号量>=2了,此时reverse线程在这次过程中还没倒置完数组,时间片就到了,CPU调度到了print线程,就会打印出没有倒置完的数组,如下的运行结果👇
在这里插入图片描述

还可以不加延时,那要再加一个信号量,用两个信号量来实现这个题的同步,如下代码👇

/*练习:.通过两个线程实现数组倒置,线程一用于循环倒置,线程二用于循环打印。
用信号量实现同步
*/
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
sem_t sem1;
/*****新加的********/
sem_t sem2;
/******************/

void *reverse(void *arg)
{
    int temp;
    while(1)
    {
        /*****新加的********/        
        sem_wait(&sem2);
        /******************/        
        for (int i = 0, j = 9; i < j; i++, j--)
        {
            temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
        sem_post(&sem1);
        // sleep(1);
    }
    pthread_exit(NULL);
    return NULL;
}
void *print(void *arg)
{
    while(1)
    {
        sem_wait(&sem1);
        for (int i = 0; i < 10; i++)
        {
            printf("%d ", a[i]);
        }
        printf("\n");
        /*****新加的********/ 
        sem_post(&sem2);
        /******************/ 
    }
    pthread_exit(NULL);
    return NULL;
}

int main(int argc, char const *argv[]) //主进程
{
    pthread_t tid1;
    pthread_t tid2;

    if (pthread_create(&tid1, NULL, reverse, NULL) != 0)
    {
        perror("pthread_create tid1 error");
        return -1;
    }
    if (pthread_create(&tid2, NULL, print, NULL) != 0)
    {
        perror("pthread_create tid2 error");
        return -1;
    }
    if(sem_init(&sem1,0,0)<0)
    {
        perror("sem1_init err\n");
        return -1;
    }
    /*****新加的********/
    if(sem_init(&sem1,0,1)<0)
    {
        perror("sem2_init err\n");
        return -1;
    }
    /*******************/
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    return 0;
}

在这里插入图片描述

2.2 互斥锁

2.2.1 几个概念

  • 临界资源:一次仅允许一个进程所使用的资源
  • 临界区:指的是一个访问共享资源的程序片段
  • 互斥:多个线程在访问临界资源时,同一时间只能一个线程访问
  • 互斥锁:通过互斥锁可以实现互斥机制,主要用来保护临界资源,每个临界资源都由一个互斥锁来保护,线程必须先获得互斥锁才能访问临界资源,访问完资源后释放该锁。如果无法获得锁,线程会阻塞直到获得锁为止。

2.2.2 函数接口

  1. 初始化互斥锁:pthread_mutex_init
    int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr)

    • 功能:初始化互斥锁
    • 参数
      • mutex:互斥锁
      • attr: 互斥锁属性 // NULL表示缺省属性
    • 返回值:成功 0;失败 -1
  2. 申请互斥锁:pthread_mutex_lock
    int pthread_mutex_lock(pthread_mutex_t *mutex)

    • 功能:申请互斥锁
    • 参数:mutex:互斥锁
    • 返回值:成功 0;失败 -1

    注:和pthread_mutex_trylock区别:pthread_mutex_lock是阻塞的;pthread_mutex_trylock不阻塞,如果申请不到锁会立刻返回

  3. 释放互斥锁:pthread_mutex_unlock
    int pthread_mutex_unlock(pthread_mutex_t *mutex)

    • 功能:释放互斥锁
    • 参数:mutex:互斥锁
    • 返回值:成功 0;失败 -1
  4. 销毁互斥锁:pthread_mutex_destroy
    int pthread_mutex_destroy(pthread_mutex_t *mutex)

    • 功能:销毁互斥锁
    • 参数:mutex:互斥锁

2.2.3 练习

通过两个线程实现数组倒置,线程1用于循环倒置,线程2用于循环打印,要满足导致一次打印一次。用互斥锁实现

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

int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
pthread_mutex_t lock;

void *reverse(void *arg)
{
    int temp;
    while (1)
    {
        pthread_mutex_lock(&lock);
        for (int i = 0; i < 10/2; i++)
        {
            temp = a[i];
            a[i] = a[9-i];
            a[9-i] = temp;
        }
        pthread_mutex_unlock(&lock);
    }
}
void *print(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&lock);
        for (int i = 0; i < 10; i++)
        {
            printf("%d ", a[i]);
        }
        printf("\n");
        pthread_mutex_unlock(&lock);
        sleep(1);
    }
}

int main(int argc, char const *argv[])
{
    pthread_t tid1, tid2;
    if (pthread_create(&tid1, NULL, reverse, NULL) != 0)
    {
        perror("pthread_create tid1 error");
        return -1;
    }
    if (pthread_create(&tid2, NULL, print, NULL) != 0)
    {
        perror("pthread_create tid2 error");
        return -1;
    }

    if (pthread_mutex_init(&lock, NULL))
    {
        perror("mutex err");
    }

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    pthread_mutex_destroy(&lock);

    return 0;
}

在这里插入图片描述

注📢:这个程序可以实现循环倒置与打印,因为通过互斥锁保护了临界资源,实现了互斥,不会出现还未倒置完就会打印出来的情况,但是不能实现倒置一次打印一次(下面的条件变量可以实现)。因为两个线程并不是交替执行,而是谁抢到时间片谁执行。比如有可能倒置线程先抢到时间片先执行,然后打印线程抢到时间片执行两次,等等其他无次序交替执行。

2.3 条件变量

2.3.1 步骤

  1. pthread_cond_init:初始化
  2. pthread_cond_wait:阻塞等待条件产生,若没有条件产生会阻塞并且解锁,当有条件产生,再次上锁。所以在使用时先上锁再调用pthread_cond_wait
  3. pthread_cond_signal:产生条件,不阻塞
  4. pthread_cond_destory:销毁条件变量

2.3.2 函数

  1. 初始化条件变量:pthread_cond_init
    int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);

    • 功能:初始化条件变量
    • 参数:
      • cond:是一个指向结构pthread_cond_t 的指针
      • restrict attr:是一个指向结构pthread_condattr_t的指针,一般设为NULL
    • 返回值:成功:0 ;失败:非0
  2. 等待信号的产生:pthread_cond_wait
    int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);

    • 功能:等待信号的产生
    • 参数
      • restrict cond:要等待的条件
      • restrict mutex:对应的锁
    • 返回值:成功:0,失败:不为0

    注📢🚨⭐🎯:当没有条件产生时函数会阻塞,同时会将锁解开;如果等待到条件产生,函数会结束阻塞同时进行上锁

  3. 产生条件:pthread_cond_signal
    int pthread_cond_signal(pthread_cond_t *cond);

    • 功能:给条件变量发送信号
    • 参数:cond:条件变量值
    • 返回值:成功:0,失败:非0

    注:必须等待pthread_cond_wait函数先执行,再产生条件

  4. 销毁条件:pthread_cond_destroy
    int pthread_cond_destroy(pthread_cond_t *cond);

    • 功能:将条件变量销毁
    • 参数:cond:条件变量值
    • 返回值:成功:0, 失败:非0

2.3.3 练习

通过两个线程实现数组倒置,线程1用于循环倒置,线程2用于循环打印,实现终端间隔1秒交替循环输出,要先输出倒置的数组。用 互斥锁+条件变量 实现此同步

/*练习:.通过两个线程实现数组倒置,线程一用于循环倒置,线程二用于循环打印。
用互斥锁 + 条件变量实现同步
int a[10] = {0,1,2,3,4,5,6,7,8,9};
*/
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

int a[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
pthread_mutex_t lock;
pthread_cond_t cond;

void *reverse(void *arg)
{
    int temp;
    while (1)
    {
        sleep(1); //保证print先抢到锁,让其阻塞等待信号
        pthread_mutex_lock(&lock);
        for (int i = 0, j = 9; i < j; i++, j--)
        {
            temp = a[i];
            a[i] = a[j];
            a[j] = temp;
        }
        pthread_cond_signal(&cond);  //产生条件
        pthread_mutex_unlock(&lock); //解锁
    }
    pthread_exit(NULL);
    return NULL;
}
void *print(void *arg)
{
    while (1)
    {
        pthread_mutex_lock(&lock);
        // 阻塞等待条件产生,若没条件,则解锁,条件到来解除阻塞上锁
        pthread_cond_wait(&cond, &lock);
        for (int i = 0; i < 10; i++)
        {
            printf("%d ", a[i]);
        }
        printf("\n");
        pthread_mutex_unlock(&lock);
    }
    pthread_exit(NULL);
    return NULL;
}
int main(int argc, char const *argv[]) //主进程
{
    pthread_t tid1;
    pthread_t tid2;
    if (pthread_mutex_init(&lock, NULL)) //初始化互斥锁
    {
        perror("mutex_init err");
        return -1;
    }
    if (pthread_cond_init(&cond, NULL)) //初始化条件变量
    {
        perror("cond_init err");
        return -1;
    }
    // 创建线程
    if (pthread_create(&tid1, NULL, reverse, NULL) != 0)
    {
        perror("pthread_create tid1 error");
        return -1;
    }
    if (pthread_create(&tid2, NULL, print, NULL) != 0)
    {
        perror("pthread_create tid2 error");
        return -1;
    }
    // 阻塞回收线程
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

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

在这里插入图片描述
注📢:reverse线程函数里的sleep(1)放在加锁的上面,是为了保证print线程先抢到锁,让其在pthread_cond_wait处阻塞等待条件信号(pthread_cond_signal)产生,然后再reverse线程得到执行,实现数组的倒置,保证第一次的输出结果是倒置后的数据。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
<h3>回答1:</h3><br/>Linux线程同步互斥是指在多个线程同时访问共享资源时,为了避免数据竞争和不一致性而采取的一些措施。同步是指多个线程按照一定的顺序执行,以保证数据的正确性和一致性;互斥是指在同一时间只允许一个线程访问共享资源,以避免多个线程同时修改数据而导致的错误。在Linux系统中,常用的同步互斥机制包括信号量、互斥锁、条件变量等。这些机制可以通过系统调用或者库函数来实现,从而保证多线程程序的正确性和稳定性。 <h3>回答2:</h3><br/>在多线程编程中,线程同步互斥是非常重要的一个话题。在Linux系统下,线程同步互斥通常使用pthread库提供的相关函数来实现。本文将重点介绍pthread库中几个常用的线程同步互斥的相关函数。 线程同步: 1. pthread_cond_init/pthread_cond_destroy 这两个函数主要用于条件变量的初始化和销毁。条件变量是线程同步的一种方式,用于在线程之间传递信息。 2. pthread_cond_signal/pthread_cond_broadcast 这两个函数用于唤醒一个或多个等待的线程。 3. pthread_cond_wait 这个函数用于阻塞当前线程,直到条件变量被唤醒。 线程互斥: 1. pthread_mutex_init/pthread_mutex_destroy 这两个函数主要用于互斥量的初始化和销毁。互斥量是一种保护共享资源的一种机制。 2. pthread_mutex_lock/pthread_mutex_trylock/pthread_mutex_unlock 这三个函数分别用于加锁、尝试加锁和解锁互斥量。 以上这些函数是pthread库中最常用的线程同步互斥相关函数。在实际编程中,经常使用这些函数来保证线程之间的互斥同步。另外,还有很多其他的线程同步互斥的方式,如信号量、读写锁等等。需要根据具体的应用场景和需求来选择。 <h3>回答3:</h3><br/>线程同步互斥是多线程程序开发中重要的概念。在多个线程同时访问共享资源时,为了保证数据的正确性和一致性,需要使用线程同步互斥技术来防止数据竞争(Data Race)。 线程同步是指协调多个线程的执行顺序,让它们以正确的顺序访问共享资源。线程同步的方法有很多种,比如信号量、互斥锁、条件变量等。其中,互斥锁(Mutex)是最常用的一种同步方法。 互斥锁是用来保护共享资源,实现线程互斥的一种机制。在临界区(Critical Section)内,只能有一个线程访问共享资源。当一个线程进入临界区时,首先要尝试获得互斥锁。如果锁已经被其他线程占用,则当前线程会被阻塞,直到其他线程释放锁。当当前线程执行完临界区的代码后,需要释放互斥锁,让其他线程获得资源的使用权。 互斥锁的实现方式有很多种,比如基于原子操作、自旋锁、互斥量等。在Linux系统中,最常用的是互斥量(pthread_mutex_t)。互斥锁可以确保临界区内的代码在同一时刻只会被一个线程执行,从而避免了数据竞争所带来的副作用。 除了互斥锁,Linux系统还提供了其他同步机制,比如条件变量(pthread_cond_t)和信号量(sem_t)。这些机制的主要作用是在多个线程之间传递信号和状态,从而实现线程同步。 总之,在多线程编程中,线程同步互斥是必不可少的。只有通过合适的同步措施,才能保证多个线程能够正确地协作,从而充分利用多核CPU的计算能力,提高程序的性能和响应速度。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Sunqk5665

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值