Linux——多线程,互斥与同步

这篇具有很好参考价值的文章主要介绍了Linux——多线程,互斥与同步。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

一.linux互斥

1.进程线程间的互斥相关背景概念

2.互斥量mutex

3.加锁互斥锁mutex

4.锁的底层原理

 二.可重入VS线程安全

1.概念

2.常见的线程不安全的情况

3.常见的线程安全的情况 

4.常见不可重入的情况 

5..常见可重入的情况

6.可重入与线程安全联系

 三.死锁

1.死锁四个必要条件

2.避免死锁

3.避免死锁算法

四.Linux线程同步

1.条件变量

2.同步概念与竞态条件

 3.条件变量函数

4.代码样例


Linux——多线程,互斥与同步,linux,运维,服务器

一.linux互斥

1.进程线程间的互斥相关背景概念

  1. 临界资源:多线程执行流共享的资源就叫做临界资源。
  2. 临界区:每个线程内部,访问临界资源的代码,就叫做临界区。
  3. 互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
  4. 原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

2.互斥量mutex

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。

但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。

多个线程并发的操作共享变量,会带来一些问题:

测试代码:

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>

using namespace std;

#define NUM 4      // 线程数
int ticket = 1000; // 1000票

void *RobTicket(void *args)
{
    const char *name = static_cast<const char *>(args);
    while (1)
    {
        if (ticket > 0)
        {
            usleep(2000);
            printf("%s-ticket,%d\n", name, ticket);
            ticket--; // 四个线程同时对ticket--,直到ticket为0时结束
        }
        else
        {
            break;
        }
        usleep(100);
    }
}

int main()
{
    pthread_t tid[NUM];
    for (int i = 0; i < NUM; i++)
    {
        char *name = new char[50];
        sprintf(name, "Thread-%d", i + 1);
        pthread_create(tid + i, NULL, RobTicket, name);
    }

    for (int i = 0; i < NUM; i++)
    {
        pthread_join(tid[i], NULL);
    }

    return 0;
}

测试结果:

Linux——多线程,互斥与同步,linux,运维,服务器

 说明:

  1. 由于 if 语句判断条件为真以后,代码可以并发的切换到其他线程。
  2. usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
  3. -- ticket 操作本身就不是一个原子操作,转换成汇编有三条汇编指令。
  4. 当ticket的值已经等于1时,由于某一个usleep会有较长时间的等待,此时又会有几个线程进入,if语句内部,所以这几个线程又会对ticket多次-- 操作。也就会出现ticket出现小于0的情况。
  5. 上述中,ticket就是临界资源,整个if语句就是临界区。

 取出ticket--部分的汇编代码
objdump -d a.out > test.objdump
152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket>
153 400651: 83 e8 01 sub $0x1,%eax
154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>

3.加锁互斥锁mutex

解决办法:

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

 要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。

Linux——多线程,互斥与同步,linux,运维,服务器

创建锁:

pthread_mutex_t mutex;

初始化锁:

 初始化互斥量有两种方法:

方法1,静态分配:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法2,动态分配:

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

参数:

  • mutex:要初始化的互斥量
  • attr:NULL

销毁锁:

int pthread_mutex_destroy(pthread_mutex_t *mutex);

注意:

  1. 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁。
  2. 不要销毁一个已经加锁的互斥量。
  3. 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

加锁与解锁:

int pthread_mutex_lock(pthread_mutex_t *mutex);//加锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);//解锁

返回值:成功返回0,失败返回错误号。

调用 pthread_ lock 时,可能会遇到以下情况:

  1. 该锁没被其他线程持有,直接申请成功该锁并且返回。
  2. 该锁已经被其他线程所持有,或者存在其他线程同时申请锁,但没有竞争到锁,那么pthread_ lock调用线程会陷入阻塞(执行流被挂起),等待其他线程解锁。

 测试代码:

将上述的代码对临界区加锁,使得临界区一次只能进入一个线程,每次只能有一个线程对临界资源访问,即每次都只能有一个线程对ticket--。

#include <iostream>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <pthread.h>

using namespace std;

#define NUM 4          // 线程数
int ticket = 1000;     // 1000票
pthread_mutex_t mutex; // 锁

void *RobTicket(void *args)
{
    const char *name = static_cast<const char *>(args);
    while (1)
    {
        pthread_mutex_lock(&mutex); // 加锁
        if (ticket > 0)
        {
            usleep(2000);
            printf("%s-ticket,%d\n", name, ticket);
            ticket--;                     // 四个线程同时对ticket--,直到ticket为0
            pthread_mutex_unlock(&mutex); // 解锁
        }
        else
        {
            pthread_mutex_unlock(&mutex); // 解锁
            break;
        }
        usleep(100);
    }
}

int main()
{
    pthread_mutex_init(&mutex, NULL); // 初始化锁
    pthread_t tid[NUM];
    for (int i = 0; i < NUM; i++)
    {
        char *name = new char[50];
        sprintf(name, "Thread-%d", i + 1);
        pthread_create(tid + i, NULL, RobTicket, name);
    }

    // 线程等待
    for (int i = 0; i < NUM; i++)
    {
        pthread_join(tid[i], NULL);
    }

    return 0;
}

测试结果:

Linux——多线程,互斥与同步,linux,运维,服务器

 说明:

  1. 不会再出现ticket为负数的情况了。
  2. 我们可以明显的发现,代码的打印速度变慢了,因为临界区的代码,包括像显示器打印的语句都被串行化了。

 4.锁的底层原理

经过上面的例子,大家已经意识到单纯的 i++ 或者 ++i 都不是原子的,有可能会有数据一致性问题。

为了实现互斥锁操作,大多数体系结构都提供了swapexchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。

Linux——多线程,互斥与同步,linux,运维,服务器

 说明:

由于初始化pthread_mutex_init () 初始化会将mutex在内存初始化为1,第一个线程申请锁的时候,会将自己线程内部的一个寄存器%al初始化为0,并且将寄存器%al的值与mutex内存的数据交换,那么现在该申请锁的线程的%al寄存器中就存储的是1,mutex的内存中存储的就是0,再有经过检测,如果%al的值是大于0的return之后,继续往后运行。后续的线程再申请锁的时候,exchange之后,他们的%al寄存器只能存储的是0,所以会被挂起等待。

解锁仅仅需要将mutex的内存数据重新赋值为1,并且唤醒挂起等待的进程。

5.锁封装

class Mutex//自己不维护锁,由外部传入
{

public:
    Mutex(pthread_mutex_t *mutex)
        : _mutex(mutex)
    {
    }

    void lock()
    {
        pthread_mutex_lock(_mutex);
    }
    void unlock()
    {
        pthread_mutex_unlock(_mutex);
    }
    ~Mutex()
    {
    }

private:
    pthread_mutex_t *_mutex;
};

class MutexGuard
{
public:
    MutexGuard(pthread_mutex_t *mutex)
        : _mutex(mutex)
    {
        _mutex.lock();
    }
    ~MutexGuard()
    {
        _mutex.unlock();
    }

private:
    Mutex _mutex;
};

 二.可重入VS线程安全

1.概念

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。

2.常见的线程不安全的情况

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

3.常见的线程安全的情况 

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

4.常见不可重入的情况 

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

5..常见可重入的情况

  1. 不使用全局变量或静态变量。
  2. 不使用用malloc或者new开辟出的空间。
  3. 不调用不可重入函数。
  4. 不返回静态或全局数据,所有数据都有函数的调用者提供。
  5. 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。

6.可重入与线程安全联系

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

 三.死锁

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

Linux——多线程,互斥与同步,linux,运维,服务器

1.死锁四个必要条件

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

2.避免死锁

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

 3.避免死锁算法

  1. 死锁检测算法(了解)
  2. 银行家算法(了解) 

四.Linux线程同步

1.条件变量

  1. 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  2. 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。

2.同步概念与竞态条件

  1. 同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。
  2. 竞态条件:因为时序问题,而导致程序异常,我们称之为竞态条件。 

 3.条件变量函数

定义条件变量:

pthread_cond_t cond;

初始化条件变量:

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

参数:

  • cond:要初始化的条件变量。
  • attr:NULL。

销毁条件变量:

int pthread_cond_destroy(pthread_cond_t *cond);

等待条件满足:

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

参数:

  • cond:要在这个条件变量上等待。
  • mutex:互斥量,后面详细解释。

注意:

条件变量一般需要搭配互斥锁来使用,当线程满足等待条件时,需要线程先释放锁,再去等待。否则带着锁再条件变量等待,会导致其他线程申请锁失败。

唤醒等待:

int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);
  • pthread_cond_broadcast:一次唤醒所有在cond上等待的线程。
  • pthread_cond_signal:唤醒一个在cond上等待的线程。

4.代码样例

#include <iostream>
#include <queue>
#include <cstdlib>
#include <cstdio>
#include <ctime>
#include <unistd.h>
#include <pthread.h>

using namespace std;

queue<int> V;
pthread_mutex_t mutex = PTHREAD_ADAPTIVE_MUTEX_INITIALIZER_NP;
pthread_cond_t cond;
void *push_date(void *args)
{
    const char *name = static_cast<const char *>(args);
    while (1)
    {
        int date;
        cin >> date; // 输入数据
        // 对临界资源的访问需要加锁
        pthread_mutex_lock(&mutex);
        V.push(date);
        // 当队列中有数据以后,需要唤醒get_date线程
        pthread_cond_signal(&cond);
        cout << name << ":push 一个数据 :" << date << endl;
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }
}
void *get_date(void *args)
{
    const char *name = static_cast<const char *>(args);
    while (1)
    {
        pthread_mutex_lock(&mutex);
        //如果队列为空
        if (V.empty())
        {
            pthread_cond_wait(&cond, &mutex);
        }
        int date = V.front();
        V.pop();

        cout << name << ":我得到一个数据:" << date << endl;
        pthread_mutex_unlock(&mutex);
    }
}

int main()
{
    srand(time(nullptr));
    //
    pthread_t tid_push, tid_get;
    pthread_create(&tid_get, nullptr, get_date, (void *)"Thread_get");
    pthread_create(&tid_get, nullptr, push_date, (void *)"Thread_push");

    pthread_join(tid_get, NULL);
    pthread_join(tid_push, NULL);

    return 0;
}

 测试结果:

Linux——多线程,互斥与同步,linux,运维,服务器

说明:文章来源地址https://www.toymoban.com/news/detail-742736.html

  1. 只有我们输入之后,get_date线程才会去队列中获取数据。
  2. 说明,get_gate线程再队列为空的时候是不会去拿数据的,使得我们的push线程先push数据的逻辑始终在get数据之前。这就是线程同步。

到了这里,关于Linux——多线程,互斥与同步的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux——多线程,互斥与同步

    目录 一.linux互斥 1.进程线程间的互斥相关背景概念 2.互斥量mutex 3.加锁互斥锁mutex 4.锁的底层原理  二.可重入VS线程安全 1.概念 2.常见的线程不安全的情况 3.常见的线程安全的情况  4.常见不可重入的情况  5..常见可重入的情况 6.可重入与线程安全联系  三.死锁 1.死锁四个必

    2024年02月05日
    浏览(32)
  • Linux-线程的同步与互斥

    🚀 临界资源:多线程指行流共享的资源叫做临界资源。 🚀 临界区:每个线程内部访问临界资源的代码片段叫做临界区。 🚀 互斥:任何时刻,互斥保证只有一个指行流进入临界区,访问临界资源,通常是对临界区起保护作用。 🚀 原子性:不被任何调度所打断的操作,该

    2024年02月09日
    浏览(46)
  • Linux——线程的同步与互斥

    目录 模拟抢火车票的过程 代码示例 thread.cc Thread.hpp 运行结果 分析原因 tickets减到-2的本质  解决抢票出错的方案 临界资源的概念 原子性的概念 加锁 定义 初始化 销毁 代码形式如下 代码示例1: 代码示例2: 总结 如何看待锁 申请失败将会阻塞  pthread_mutex_tyrlock 互斥锁实现

    2024年02月06日
    浏览(38)
  • 『Linux』第九讲:Linux多线程详解(三)_ 线程互斥 | 线程同步

    「前言」文章是关于Linux多线程方面的知识,上一篇是 Linux多线程详解(二),今天这篇是 Linux多线程详解(三),内容大致是线程互斥与线程同步,讲解下面开始! 「归属专栏」Linux系统编程 「主页链接」个人主页 「笔者」枫叶先生(fy) 「枫叶先生有点文青病」「每篇一句

    2024年02月02日
    浏览(70)
  • 【关于Linux中----线程互斥与同步】

    先来用代码模拟一个抢票的场景,四个线程不停地抢票,一共有1000张票,抢完为止,代码如下: 执行结果如下: 可以看到,最后出现了票数为负数的情况,很显然这是错误的,是不应该出现的。 为什么会出现这种情况? 首先要明确,上述的几个线程是不能同时执行抢票的

    2023年04月08日
    浏览(38)
  • 【Linux】多线程2——线程互斥与同步/多线程应用

    💭上文主要介绍了多线程之间的独立资源,本文将详细介绍多线程之间的 共享资源 存在的问题和解决方法。 intro 多线程共享进程地址空间,包括创建的全局变量、堆、动态库等。下面是基于全局变量实现的一个多线程抢票的demo。 发现错误:线程抢到负数编号的票,为什么

    2024年02月10日
    浏览(42)
  • 【Linux】多线程 --- 线程同步与互斥+生产消费模型

    人生总是那么痛苦吗?还是只有小时候是这样? —总是如此 1. 假设现在有一份共享资源tickets,如果我们想让多个线程都对这个资源进行操作,也就是tickets- -的操作,但下面两份代码分别出现了不同的结果,上面代码并没有出现问题,而下面代码却出现了票为负数的情况,这

    2024年02月06日
    浏览(42)
  • Linux pthread线程操作 和 线程同步与互斥操作

    在Linux系统中玩线程,使用pthread,这篇博客记录如何 创建线程 和 使用线程 和线程的 同步 与 互斥 。 还有一份nginx线程池的代码供大家阅读学习! 目录 一、简介 什么是线程 线程的优点、缺点 线程的应用场合 二、线程的使用 1.  创建线程 - pthread_create 2.  线程的终止 - pt

    2024年02月02日
    浏览(31)
  • 【Linux】多线程02 --- 线程的同步互斥问题及生产消费模型

    🍎 作者: 阿润菜菜 📖 专栏: Linux系统编程 线程同步互斥问题是指多线程程序中,如何保证共享资源的正确访问和线程间的协作。 因为线程互斥是实现线程同步的基础和前提,我们先讲解线程互斥问题。 在多线程中,假设我们有一个黄牛抢票的代码,其中有一份共享资源

    2024年02月08日
    浏览(39)
  • 运维 | 查看 Linux 服务器 IP 地址

    大多数在操作 Linux 系统时,我们经常需要知道服务器的 IP 比便于后续的一系列操作,这时候有快速查看主机 IP 的命令行操作,能够有效的帮助我们 本章节主要记录一些常用查看服务器 IP 的命令,希望对大家有所帮助。 查看 Linux 服务器的 IP 地址的命令大体上有以下几种。

    2024年04月27日
    浏览(76)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包