Linux_线程的同步与互斥

目录

1、互斥相关概念 

2、代码体现互斥重要性

3、互斥锁 

3.1 初始化锁 

3.2 申请、释放锁 

3.3 加锁的思想

3.4 实现加锁 

3.5 锁的原子性 

4、线程安全 

4.1 可重入函数 

4.2 死锁 

5、线程同步 

5.1 条件变量初始化 

5.2 条件变量等待队列

5.3 唤醒等待队列

5.4 实现线程同步

结语 


前言:

        在Linux下,线程是一个很重要的概念,他可以提高多执行流的并发度,而同步与互斥是对线程的一种约束行为,比如当多个线程都访问同一个资源时,若不对该资源加以保护则会导致意料之外的错误。具体的保护措施是让线程访问共享资源时具有互斥性,即当一个线程访问时别的线程无法访问,通常用互斥锁来实现。而同步是为了让多个线程具有一定的顺序来访问共享内存,保障每个线程访问资源的机会是一样的。

1、互斥相关概念 

        线程之所以需要互斥,是因为多线程在访问共享资源时,可能该资源只允许被修改一次,但是其他线程在修改的时候“刹不住车”,导致该资源被修改多次,原因就是多个线程同时访问了该资源,如下图所示:

        在概念层面上,通常把共享资源叫做临界资源。在代码层面,把访问共享资源的代码叫做临界区


        当线程有了互斥约束后,就不会出现上述a=0时继续访问a的情况,如下图:

2、代码体现互斥重要性

        在实际生活中,某些有限的物品是不能出现负数的情况的,比如抢票,票为0时是不能继续抢票的,但是当实现多线程抢票时,若没有互斥的约束,则很容易发生票为0时还在抢票,模拟抢票的代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>

using namespace std;

int tickets = 1000; // 用多线程模拟抢票

class threadData
{
public:
    threadData(int number)
    {
        threadname = "线程-" + to_string(number);
    }

public:
    string threadname;
};

void *getTicket(void *args)
{
    threadData *td = static_cast<threadData *>(args);
    const char *name = td->threadname.c_str();
    while (true)
    {
        if(tickets > 0)
        {
            usleep(1000);
            printf("%s, 抢到一张票: %d\n", name, tickets); 
            tickets--;
        }
        else
            break;
    }
    printf("%s 退出\n", name);
    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    vector<threadData *> thread_datas;
    for (int i = 1; i <= 4; i++)
    {
        pthread_t tid;
        threadData *td = new threadData(i);
        thread_datas.push_back(td);
        pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);
        tids.push_back(tid);
    }

    //等待线程
    for (auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }
    //释放空间资源
    for (auto td : thread_datas)
    {
        delete td;
    }
    return 0;
}

         运行结果:

        从结果可以看到,发生了负数票的情况,原因就是上面多线程代码没有任何互斥的约束。


        对上面代码进行分析找出其临界区,全局变量ticket是临界资源,因此代码中对ticket的访问就是临界区,如下图所示:

        为了解决上面的问题,只能使用互斥约束多线程,而互斥就必须用到互斥锁。 

3、互斥锁 

         实现互斥锁的步骤:

        1、创建一个锁变量。

        2、使用接口初始化该变量。

        3、在临界区处申请该锁。

        4、临界区代码执行完后释放锁。

        5、销毁锁。

        值得注意的是:只能用一把锁限制对临界区的访问,即线程要想访问临界区,则必须申请到该锁才能访问,没有申请到锁的线程就无法访问临界区。 


        申请锁的示意图如下:

3.1 初始化锁 

        初始化锁用到的接口介绍如下:

#include <pthread.h>
//销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

//初始化锁,方式1
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);
//restrict mutex表示要初始化的锁
//restrict attr表示初始化的属性

//初始化锁,方式2
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//定义在全局,则mutex锁就已经被初始化了

        pthead_mutex_t是库提供的数据类型,用于定义一个锁。方式2是一个全局变量初始化锁,若用方式2初始化一个锁则无需对该锁进行destroy。注意:若用方式2进行锁的初始化则该锁必须是全局的。

3.2 申请、释放锁 

        锁的初始化工作完成后,接下来就是申请锁,申请锁的接口介绍如下:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);//申请mutex锁

int pthread_mutex_trylock(pthread_mutex_t *mutex);//申请mutex锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);//释放mutex锁

        pthread_mutex_lock申请不到锁会阻塞在该函数处,而pthread_mutex_trylock申请不到锁不会阻塞,会继续执行下面代码。

3.3 加锁的思想

        申请锁就是加锁,加锁的本质是用时间换来线程安全,让线程访问临界资源时串开访问,对临界区进行加锁时尽量缩小临界区的代码量,因为临界区的代码越少,执行的速度越快,则进程被cpu挂起的概念就越低,被cpu挂起的概念低了则可以减少其他线程等的时间,因为当申请到锁的线程被挂起了,那么其他的线程就算被cpu调度了也不能执行临界区的代码(因为其他线程没有持有锁),只能干等。

3.4 实现加锁 

        对上述代码实现加锁,代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>

using namespace std;

int tickets = 1000; // 用多线程模拟抢票

class threadData
{
public:
    threadData(int number , pthread_mutex_t *mutex)
    {
        threadname = "线程-" + to_string(number);
        lock = mutex;
    }

public:
    string threadname;
    pthread_mutex_t *lock;
};

void *getTicket(void *args)
{
    threadData *td = static_cast<threadData *>(args);
    const char *name = td->threadname.c_str();
    while (true)
    {
        pthread_mutex_lock(td->lock); //申请锁
        if(tickets > 0)
        {
            usleep(1000);
            printf("%s, 抢到一张票: %d\n", name, tickets);
            tickets--;
            pthread_mutex_unlock(td->lock);//释放锁
        }
        else{
            pthread_mutex_unlock(td->lock);//释放锁

            break;
        }
        //usleep(12); //先把此处的usleep屏蔽,观察抢票现象
    }
    printf("%s 退出\n", name);
    return nullptr;
}

int main()
{
    pthread_mutex_t lock;//定义一个锁
    pthread_mutex_init(&lock, nullptr);//对该锁进行初始化

    vector<pthread_t> tids;
    vector<threadData *> thread_datas;
    for (int i = 1; i <= 3; i++)
    {
        pthread_t tid;
        threadData *td = new threadData(i ,&lock);
        thread_datas.push_back(td);
        //要将锁也传给线程
        pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);
        tids.push_back(tid);
    }

    //等待线程
    for (auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }
    //释放空间资源
    for (auto td : thread_datas)
    {
        delete td;
    }

    pthread_mutex_destroy(&lock);
    return 0;
}

         运行结果:

        从结果看,虽然没有出现负票的情况,但是发现只有一个线程在抢票,原因很简单,肯定是只有该进程申请到锁了,其他线程没申请到,那么为什么只有该线程能申请到,而其他线程申请不到呢?是因为这个线程刚释放完锁后他就立马再进行申请锁的动作了,他之所以可以比其他线程更快申请到锁的原因是“他离锁最近”,具体示意图如下:


        所以在一个线程释放锁后,可以手动对该线程进行sleep,让其他线程有机会去申请到锁,因此把上述代码中释放锁后面的usleep放开,就可以让其他线程申请到锁了,运行结果如下:

3.5 锁的原子性 

        从上文可以得知,当多线程访问共享资源时,若没有互斥约束,则会发生错误,所以对线程进行加锁的操作,但是锁本身也是共享资源,因为多线程都能看到锁并且申请他,那么申请锁的时候不好导致同样的问题吗?

        答案是不会,多线程访问共享资源之所以会发生意料之外的错误,是因为多线程对共享资源做修改操作的时候,这些修改操作在底层被转换成汇编语句,虽然上层看到的修改操作只有一句代码,但是在底层转换成两三句汇编指令,而cpu一次只能运算一句汇编指令,这就导致同一个操作没有真正被cpu执行完就被切换走了,等到下次继续执行该操作时,从内存中读取的数据可能已经被别的线程修改了,这就导致了意料之外的错误。而申请锁的动作只有一句汇编指令,他的状态只有两种:1、要么没申请到锁,2、要么申请到锁。不存在执行一半被切走的可能,通常把这种状态叫做原子性,因此锁是具有原子性的。

4、线程安全 

        线程安全指的是在多线程的并行下,访问某些资源时,不会导致该资源的数据损坏或出现意料之外的错误,线程与线程之间不会互相干扰对方的操作,多线程能够安全的执行下去,把这叫做线程安全。

4.1 可重入函数 

        可重入函数值得是当同一个函数被多个线程调用时,调用的结果不会产生任何的问题,比如不会导致数据损坏或者资源泄漏,则该函数被称为可重入函数,否则,是不可重入函数。

4.2 死锁 

         死锁指的是当线程申请锁时造成了循环申请,也就是说线程1要申请线程2的锁,而线程2要申请线程1的锁,造成死循环称之为死锁,具体示意图如下:

        造成死锁的四个必要条件:

1、互斥条件:一把锁只能被一个线程申请。
2、请求与保持条件:多线程之间互相申请对方的锁,但是对方就是不释放该锁。
3、不剥夺条件 :不释放对方的锁,即使要申请的锁在对方手里也不主动释放。
4、循环等待条件 : 多线程循环等待彼此的资源。

        只要不满足上面4个条件是任何一个,则就造成不了死锁。 

5、线程同步 

        线程同步的目的是让每个线程申请锁的能力是有顺序性的,即每个线程都可以公平的申请到锁,通常是定义一个条件变量,然后将线程放入等待队列中(申请的前提是该线程必须持有锁),申请到锁的线程就能够进入等待队列中等待了,进入等待队列时线程会自动释放锁,目的是让下一个线程申请锁然后也入队,因此条件变量必须搭配锁才能使用。

        将线程放入等待队列的示意图如下:


        唤醒等待队列里的线程去申请锁:

        等待队列申请锁的逻辑:首先需要唤醒该等待队列,然后队列里的第一个线程可以重新去申请锁,访问临界资源结束后,释放锁的线程会回到队列的末尾,如此逻辑就能够实现线程同步了

5.1 条件变量初始化 

        条件变量的初始化逻辑和锁的初始化逻辑相似,都是有两种初始化方式,具体接口如下:

#include <pthread.h>
//销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);

//条件变量初始化方式1
int pthread_cond_init(pthread_cond_t *restrict cond,
              const pthread_condattr_t *restrict attr);
//restrict cond表示要初始化的条件变量的地址
//attr表示条件变量初始化的属性设置

//条件变量初始化方式2
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//在全局定义完成初始化

5.2 条件变量等待队列

        在条件变量完成初始化后,需要将线程放入条件变量的等待队列中, 这个过程只需要调用函数pthread_cond_wait即可完成,但是要注意调用该函数时当前线程必须是持有锁的,所以使用条件变量必须依赖锁,pthread_cond_wait函数介绍如下:

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *restrict cond,
              pthread_mutex_t *restrict mutex);
//cond表示将该线程放入哪个条件变量的队列
//mutex表示等待队列被唤醒后可申请的锁

        当线程调用此函数时,会释放已经申请的锁然后在等待队列中排队,所以线程的执行流会阻塞在该函数处。

5.3 唤醒等待队列

        当调用,唤醒函数介绍如下:

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);
//cond表示要唤醒的条件变量等待队列,该函数被调用一次则整个队列都被唤醒

int pthread_cond_signal(pthread_cond_t *cond);
//cond表示要唤醒的条件变量等待队列,该函数被调用一次则只唤醒队头的线程

5.4 实现线程同步

         上文中抢票代码的逻辑是线程释放锁后对该线程进行sleep,这么做让其他线程有了申请锁的机会,其实这也是同步的一种方法,只不过sleep的时间不好控制,而现在我们无需对线程进行sleep也可以实现同步,即使用条件变量进行同步,让系统去维护同步机制,可以更好的控制同步。

        实现线程同步的代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>

using namespace std;

int tickets = 1000; // 用多线程模拟抢票
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//全局初始化

class threadData
{
public:
    threadData(int number , pthread_mutex_t *mutex)
    {
        threadname = "线程-" + to_string(number);
        lock = mutex;
    }

public:
    string threadname;
    pthread_mutex_t *lock;
};

void *getTicket(void *args)
{
    threadData *td = static_cast<threadData *>(args);
    const char *name = td->threadname.c_str();
    while (true)
    {
        pthread_mutex_lock(td->lock); //申请锁
        pthread_cond_wait(&cond,td->lock);//将线程放入等待队列
        if(tickets > 0)
        {
            //usleep(1000);
            printf("%s, 抢到一张票: %d\n", name, tickets);
            tickets--;
            pthread_mutex_unlock(td->lock);//释放锁
        }
        else{
            pthread_mutex_unlock(td->lock);//释放锁

            break;
        }
    }
    printf("%s 退出\n", name);
    return nullptr;
}

int main()
{
    pthread_mutex_t lock;//定义一个锁
    pthread_mutex_init(&lock, nullptr);//对该锁进行初始化

    vector<pthread_t> tids;
    vector<threadData *> thread_datas;
    for (int i = 1; i <= 3; i++)//创建3个线程
    {
        pthread_t tid;
        threadData *td = new threadData(i ,&lock);
        thread_datas.push_back(td);
        //要将锁也传给线程
        pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);
        tids.push_back(tid);
    }
    sleep(2);//目的是让线程全部都放入队列中,然后再进行唤醒
    //唤醒队列
    while(true)
    {
        pthread_cond_signal(&cond);
    }
    //等待线程
    for (auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }
    //释放空间资源
    for (auto td : thread_datas)
    {
        delete td;
    }

    pthread_mutex_destroy(&lock);
    return 0;
}

        运行结果:

        从结果可以看到,没有出现负票的情况,并且所有线程都在抢票。这里注意pthread_mutex_lock和pthread_cond_wait两个函数对锁的申请和释放逻辑,调用pthread_mutex_lock时线程会申请锁,然后调用pthread_cond_wait时,线程会释放锁,并且阻塞在该函数处等待被唤醒,被唤醒后该线程又重新申请锁,申请成功后执行临界区代码。

结语 

        以上就是关于线程的同步与互斥讲解,若使用多线程进行并发式的执行程序,那么同步和互斥是必不可少的保护措施,他保障了多线程并发执行时线程的安全,防止出现意料之外的错误,因此对临界资源进行同步和互斥是多线程执行时非常重要的一步。

        最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!

<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
发出的红包

打赏作者

安权_code

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

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

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

打赏作者

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

抵扣说明:

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

余额充值