Linux学习日记19:线程同步与互斥锁

一、前言

        前面我们了解了线程的基础知识,而在多线程编程中,线程同步是核心技术,用于解决多线程并发访问共享资源时的竞态条件,保证数据一致性和线程执行顺序的可控性;互斥锁就是线程同步的其中一种机制。

二、线程同步

2.1、线程同步的定义

        在一般情况下,创建一个线程是不能提高程序的执行效率的,所以要创建多个线程。但是多个线程同时运行的时候可能调用线程函数,在多个线程同时对同一个内存地址进行写入,由于CPU时间调度上的问题,写入数据会被多次的覆盖,所以就要使线程同步。

        线程共享进程的地址空间(全局变量、堆、文件描述符等),当多个线程同时读写「临界资源」(如全局变量、硬件设备、网络连接)时,会导致数据错乱。

        同步的目标:1、保护临界资源,同一时间只有一个线程访问;2、协调线程执行顺序(如生产者生产完数据后,消费者再消费)。

        补:临界区:访问临界资源的代码段(需被同步机制保护);竞态条件:多线程并发执行临界区代码,导致结果依赖于线程执行顺序的不可控问题;同步机制:通过内核 / 库提供的接口,限制临界区的并发访问、协调线程执行时机。

2.2、同步的目标

1、原子性:保证临界区代码 “要么全执行,要么全不执行”,不可中断;

2、可见性:一个线程修改的共享变量,其他线程能立即看到;

3、有序性:保证线程按预期的顺序执行(如生产者先生产,消费者后消费)。

2.3、典型示例

        首先创建一个pthread_tb.c文件,然后输入以下代码:

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

int number;//共享全局变量
void *myfun1(void *arg)
{
        for(int i=0;i<10000;i++)
        {
                int ret;
                ret = number;
                ret++;
                number = ret;
                printf("fun1 is %ld,number is %d\n",pthread_self(),number);
                usleep(10);//微秒级睡眠
        }

}

void *myfun2(void *arg)
{
        for(int i=0;i<10000;i++)
        {
                int ret;
                ret = number;
                ret++;
                number = ret;
                printf("fun2 is %ld,number is %d\n",pthread_self(),number);
                usleep(10);
        }
}

int main()
{
        pthread_t pthid1;
        pthread_t pthid2;
        pthread_create(&pthid1,NULL,myfun1,NULL);//线程创建
        pthread_create(&pthid2,NULL,myfun2,NULL);
        pthread_join(pthid1,NULL);//线程等待
        pthread_join(pthid2,NULL);
        return 0;
}

        编译并运行,结果如下:

        看到这里可能会有疑问,理论上运行完后number会运行到20000,那为什么最终只有19803呢?

        因为理论结果前提是 20000 次自增都能被正确累加,但你这段代码里的“自增”并不是一条不可分割的操作,而是读+加+写三步,两个线程一旦交叉执行,就会发生丢失更新,导致很多次“+1”被白白覆盖掉,假设某一刻number=19780,线程1执行到ret = number;读到ret = 19780(但还没写回),发生线程切换,线程也执行到:ret = number,也读到ret=19780,线程2,ret++;number=ret,写回number =19781(这次+1生效),再切回线程1,线程1:ret++; number=ret; 也写回 number=19781(把线程2的结果“覆盖成同一个值”);这两次自增,本该让 number 变成 19782,结果只变成 19781 —— 少加了 1。这种“两个线程读到同一个旧值,然后分别写回同一个新值”的情况在你循环 20000 次里会发生很多很多次,于是最终就会出现 19803 < 20000。以上例子就是典型的反面案例。

三、互斥锁

3.1、互斥锁的定义

        互斥锁是 Linux 多线程同步中最基础、最常用的核心机制,其核心目标是保证同一时间只有一个线程能进入 “临界区”(访问共享资源的代码段),从而解决 “竞态条件”,保证共享资源的原子性、可见性和有序性。

        互斥锁本质是一个“二值锁”,状态只有未锁定与已锁定。通过 “加锁 - 访问临界区 - 解锁” 的闭环,强制临界区代码原子执行(要么全执行,要么全不执行,不可被线程切换中断)。

3.2、Linux互斥锁的底层实现

1、用户态尝试加锁:线程加锁时,先通过原子操作尝试获取锁(修改锁的状态);

2、成功则直接执行:若锁未被持有,加锁成功,直接进入临界区;

3、失败则内核态挂起:若锁已被持有,线程进入内核态的 “等待队列” 挂起(放弃 CPU),避免无意义的自旋;

4、解锁时唤醒线程:持有锁的线程解锁时,若等待队列有线程,内核会唤醒其中一个线程重新尝试加锁。

补:原子操作是指一个操作在执行过程中不可被中断、不可被其他线程打断,从而保证对共享数据的修改要么完整发生、要么完全不发生。

3.3、互斥锁的相关函数

1、静态初始化互斥锁

        函数原型如下:

#include <pthread.h>
// 静态初始化全局互斥锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

        注:静态初始化的互斥锁无需手动销毁;同一互斥锁不能重复初始化(已初始化的锁再次调用 pthread_mutex_init 会导致未定义行为)。

2、动态初始化互斥锁

        函数原型如下:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
                       const pthread_mutexattr_t *restrict attr);

        参数:mutex:指向要初始化的互斥锁对象(不能为 NULL);

                   attr:互斥锁属性(NULL 表示使用默认属性);

        返回值:成功:返回0;失败:非0错误码。

3、阻塞加锁

        函数原型如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);

        参数:mutex:指向已初始化的互斥锁对象。

        返回值:成功:0(获取到锁);失败:非0错误码。 

        功能:若锁未被持有,当前线程立即获取锁,锁状态变为已锁定;若锁已被持有,当前线程放弃 CPU 使用权,进入内核态等待队列,直到锁被释放。

4、非阻塞加锁

        函数原型如下:

int pthread_mutex_trylock(pthread_mutex_t *mutex);

        参数:mutex:指向已初始化的互斥锁对象。

        返回值:成功:0 (获取到锁);失败:EBUSY(锁已被持有),EINVAL(锁未初始化)等。

        功能:若锁未被持有,立即获取并返回 0;若锁已被持有,不阻塞,直接返回 EBUSY 错误,线程可执行其他逻辑。

        适用场景:不想让线程阻塞等待锁的场景。

5、解锁

        函数原型如下:

int pthread_mutex_unlock(pthread_mutex_t *mutex);

        参数:mutex:指向已初始化的互斥锁对象。

        返回值:成功:0;失败:非 0 错误码。

        功能:释放当前线程持有的锁

        注:只有持有锁的线程能解锁,解锁未加锁的锁会触发错误。

6、销毁互斥锁

        函数原型如下:     

int pthread_mutexattr_destroy(pthread_mutexattr_t *attr);

        功能:销毁属性对象,释放其占用的资源(必须与pthread_mutex_init 配对使用)。 

3.4、典型示例

        接上一个线程同步的反面案例,通过互斥锁的形式来实现线程的同步,具体代码如下所示:

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

int number;
pthread_mutex_t mutex;
void *myfun1(void *arg)
{
        for(int i=0;i<10000;i++)
        {
                //lock
                pthread_mutex_lock(&mutex);
                int ret;
                ret = number;
                ret++;
                number = ret;
                printf("fun1 is %ld,number is %d\n",pthread_self(),number);
                //ulock
                pthread_mutex_unlock(&mutex);
                usleep(10);
        }

}
void *myfun2(void *arg)
{
        for(int i=0;i<10000;i++)
        {
                //lock
                pthread_mutex_lock(&mutex);
                int ret;
                ret = number;
                ret++;
                number = ret;
                printf("fun2 is %ld,number is %d\n",pthread_self(),number);
                //ulock
                pthread_mutex_unlock(&mutex);
                usleep(10);
        }
}
int main()
{
        //init mutex
        pthread_mutex_init(&mutex,NULL);

        pthread_t pthid1;
        pthread_t pthid2;
        pthread_create(&pthid1,NULL,myfun1,NULL);
        pthread_create(&pthid2,NULL,myfun2,NULL);
        pthread_join(pthid1,NULL);
        pthread_join(pthid2,NULL);
        //kill mutex
        pthread_mutex_destroy(&mutex);
        return 0;
}

        编译并运行,结果如下:

        可以看到通过互斥锁来实现了线程的同步,避免了线程之间的竞争关系。                     

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值