【Linux】线程同步与互斥

目录

线程相关问题

线程安全

常见的线程安全的情况

常见的线程不安全的情况

可重入函数与不可重入函数

常见不可重入的情况

常见可重入的情况

可重入与线程安全的关系

联系

区别

线程同步与互斥

互斥锁

使用

死锁

死锁的四个必要条件

如何避免死锁

条件变量

同步概念与竞态条件

使用

Posix 信号量

使用

生产者消费者模型

线程池

读者写者问题


线程相关问题

线程安全

线程安全是指多线程环境下,某个函数、代码块或数据结构在被多个线程并发访问时,能够正确地执行并且保持数据的一致性和完整性,不会出现数据污染、数据竞争或死锁等问题。

常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
  • 类或者接口对于线程来说都是原子操作。
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性。

常见的线程不安全的情况

  • 不保护共享变量的函数。
  • 函数状态随着被调用,状态发生变化的函数。
  • 返回指向静态变量指针的函数。
  • 调用线程不安全函数的函数。

可重入函数与不可重入函数

若一个函数在调用期间被不同的控制流程调用,在第一次调用未结束前再次调用该函数,这种行为是被允许的,运行结果正确且不会出现任何问题,则称该函数为可重入函数,否则就是不可重入函数。

上图表示链表的多次 insert 操作,在 insert node1后,若在更新 head 前进入信号处理函数并 insert node2,最后的结果就是丢失 node2 的信息,所以 insert 是不可重入函数。

常见不可重入的情况

  • 调用了 malloc/free 函数,因为 malloc 函数是用全局链表来管理堆的。
  • 调用了标准 I/O 库函数,标准 I/O 库的很多实现都以不可重入的方式使用全局数据结构。
  • 可重入函数体内使用了静态的数据结构。

常见可重入的情况

  • 没有使用全局变量或静态变量
  • 没有使用 malloc 或者 new 开辟出空间
  • 没有调用其他不可重入函数
  • 返回值不是静态或全局数据,所有数据都由函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据

可重入与线程安全的关系

联系

  • 函数如果可重入,那么就是线程安全的。
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

区别

  • 可重入函数是线程安全函数的一种。
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。

线程同步与互斥

编写代码,创建 4 个线程进行抢票,Thread.hpp 文件为【Linux】线程与线程控制-CSDN博客里模拟实现的线程库。

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"

using namespace zyh;

const int num = 4;
int ticket = 10000;

void print(std::string name)
{
    while (ticket > 0)
    {
        usleep(1001);
        td::cout << "I am " << name <<  ", ticket: " << --ticket << std::endl;
        // sleep(1);
    }
}

int main()
{
    std::vector<Thread<int>> threads;

    int cnt = 10;
    // 1. 创建线程
    for (int i = 0; i < num; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        threads.emplace_back(print, name, name);
    }

    // 2. 启动线程
    for (auto& thread : threads)
    {
        thread.start();
    }

    sleep(3);
    // 3. 等待线程
    for (auto& thread : threads)
    {
        thread.join();
        std::cout << "wait thread done, thread is: " << thread.getThreadName() << std::endl;
    }

    return 0;
}

运行结果:

可以看到,我们希望原本票数为 0 时,线程就应该无法进行抢票,但实际上,票数为负数时仍有线程在抢票。

原因:当 ticket 为 0,线程 1 执行完 while (ticket > 0),但还未打印剩余票数时,发生线程调度切换到线程 2,此时 ticket 仍为 0,线程 2 会打印剩余票数并使剩余票数减 1,执行完毕后 ticket < 0,切换回线程 1 后继续执行剩余的打印代码,这时打印出来的结果就是负数。

解决方案:出现上述问题,归根结底的原因是 ticket 是临界资源,多线程在对临界资源的修改与访问并不是原子化的,使用互斥锁就可以解决问题。

#include <iostream>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include "Thread.hpp"

using namespace zyh;

const int num = 4;
int ticket = 10000;
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

void print(std::string name)
{
    while (ticket > 0)
    {
        pthread_mutex_lock(&mtx);
        //usleep(1001);
        if (ticket > 0)
        {
            std::cout << "I am " << name <<  ", ticket: " << --ticket << std::endl;
            // sleep(1);
        }
        pthread_mutex_unlock(&mtx);
    }
}

int main()
{
    std::vector<Thread<std::string>> threads;

    int cnt = 10;
    // 1. 创建线程
    for (int i = 0; i < num; i++)
    {
        std::string name = "thread-" + std::to_string(i + 1);
        threads.emplace_back(print, name, name);
    }

    // 2. 启动线程
    for (auto& thread : threads)
    {
        thread.start();
    }

    sleep(3);
    // 3. 等待线程
    for (auto& thread : threads)
    {
        thread.join();
        std::cout << "wait thread done, thread is: " << thread.getThreadName() << std::endl;
    }

    return 0;
}

互斥锁

如文章【Linux】进程间通信 —— 管道与 System V 版本通信方式-CSDN博客 信号量部分所述,信号量本质上是一个描述临界资源数量的计数器,对公共局部临界资源的预定机制,用来保护临界资源。如果信号量初始值是 1 呢?代表将临界资源看作整体,来实现互斥,二元信号量就是一把锁。

伪代码如下:

使用

 ubuntu 下可能会出现 man 手册查不到 pthread 相关库函数的问题

原因:因为man手册中默认没有安装关于 posix 标准的文档。

解决办法:bash 输入以下内容

sudo apt-get install manpages-posix-dev

创建与销毁

原型

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

        int pthread_mutex_destroy(pthread_mutex_t *mutex);

若 mutex 为静态或全局变量,则可以用宏来初始化,后续不用 destroy
        pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

加锁与解锁

原型

        int pthread_mutex_lock(pthread_mutex_t *mutex);
        int pthread_mutex_trylock(pthread_mutex_t *mutex);
        int pthread_mutex_unlock(pthread_mutex_t *mutex);

死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

一个最简单的死锁代码:

#include <iostream>
#include <pthread.h>

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

int main()
{
    pthread_mutex_lock(&mtx);
    std::cout << "Lock once..." << std::endl;
    pthread_mutex_lock(&mtx);
    std::cout << "Deadlock generated." << std::endl;
    pthread_mutex_unlock(&mtx);

    return 0;
}

另一份死锁代码:

#include <iostream>
#include <pthread.h>

pthread_mutex_t mtx1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mtx2 = PTHREAD_MUTEX_INITIALIZER;

void* handler1(void* arg)
{
    pthread_mutex_lock(&mtx1);
    std::cout << "Lock mtx1." << std::endl;
    pthread_mutex_lock(&mtx2);
    std::cout << "Lock mtx1 and mtx2." << std::endl;
    pthread_mutex_unlock(&mtx2);
    std::cout << "Unlock mtx2." << std::endl;
    pthread_mutex_unlock(&mtx1);
    std::cout << "Unlock mtx1." << std::endl;

    return nullptr;
}

void* handler2(void* arg)
{
    pthread_mutex_lock(&mtx2);
    std::cout << "Lock mtx2." << std::endl;
    pthread_mutex_lock(&mtx1);
    std::cout << "Lock mtx1 and mtx2." << std::endl;
    pthread_mutex_unlock(&mtx1);
    std::cout << "Unlock mtx1." << std::endl;
    pthread_mutex_unlock(&mtx2);
    std::cout << "Unlock mtx2." << std::endl;
    
    return nullptr;
}

int main()
{
    pthread_t tid1, tid2;
    pthread_create(&tid1, nullptr, handler1, nullptr);
    pthread_create(&tid2, nullptr, handler2, nullptr);

    pthread_join(tid1, nullptr);
    pthread_join(tid2, nullptr);

    return 0;
}

死锁的四个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用。
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺。
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系。

如何避免死锁

  • 破坏死锁的四个必要条件之一
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。

例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

同步概念与竞态条件

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。

竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。

使用

创建与销毁

原型        

        int pthread_cond_init(pthread_cond_t *restrict cond,
                const pthread_condattr_t *restrict attr);

        int pthread_cond_destroy(pthread_cond_t *cond);

若 cond 为静态或全局变量,则可以用宏来初始化,后续不用 destroy

        pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

唤醒与等待

原型

        int pthread_cond_signal(pthread_cond_t *cond);

        int pthread_cond_broadcast(pthread_cond_t *cond);

        int pthread_cond_wait(pthread_cond_t *restrict cond,
                pthread_mutex_t *restrict mutex);

Posix 信号量

Posix 信号量和 System V 信号量作用相同,都是用于同步操作,达到无冲突的访问共享资源目的。 但 Posix 可以用于线程间同步。

相关概念可查看【Linux】进程间通信 —— 管道与 System V 版本通信方式-CSDN博客 信号量部分。

使用

创建与销毁

原型  

        int sem_init(sem_t *sem, int pshared, unsigned int value);

        int sem_destroy(sem_t *sem);

参数:
        pshared: 0 表示线程间共享,非 0 表示进程间共享
        value:信号量初始值

P 操作

原型

        int sem_wait(sem_t *sem);

        int sem_trywait(sem_t *sem);

V 操作

原型

        int sem_post(sem_t *sem);

生产者消费者模型

这里会放一个超链接,待更新。

线程池

这里会放一个超链接,待更新。

读者写者问题

这里会放一个超链接,待更新。

  • 14
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 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
发出的红包

打赏作者

毕瞿三谲丶

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

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

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

打赏作者

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

抵扣说明:

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

余额充值