Linux线程互斥之线程加锁的使用详解

 更新时间:2025年04月29日 09:54:44   作者:s_little_monster_  
这篇文章主要介绍了Linux线程互斥之线程加锁的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

一、锁的定义

线程加锁是在多线程编程环境中,为了确保在同一时刻只有一个线程能够访问特定的共享资源或执行特定的代码段,而采取的一种同步手段,通过在需要保护的资源或代码段前获取锁,在访问完成后释放锁,来实现对共享资源的互斥访问

二、库函数

1、初始化互斥锁

#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
  • 返回值:成功返回0,失败返回非零错误码
  • mutex:表示要初始化的互斥锁,pthread_mutex_t是POSIX线程库中定义的互斥锁类型
  • attr:包含互斥锁的属性,设置为NULL表示使用默认属性

2、销毁互斥锁

#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
  • 返回值:成功返回0,失败返回非零错误码
  • mutex:表示要销毁的互斥锁

3、加锁

#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
  • 返回值:成功返回0,失败返回非零错误码
  • mutex:表示要加锁的互斥锁

4、解锁

#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 返回值:成功返回0,失败返回非零错误码
  • mutex:表示要解锁的互斥锁

5、示例

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

using namespace std;

//定义一个全局锁就可以不需要初始化和销毁锁的函数了
//pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

#define NUM 4
//共500张票
int tickets = 500;

class ThreadInfo
{
public:
    ThreadInfo(const string &threadname, pthread_mutex_t *lock)
    :threadname_(threadname)
    ,lock_(lock)
    {}

public:
    string threadname_;
    pthread_mutex_t *lock_;
};

void *GrabTickets(void *args)
{
    ThreadInfo *ti = static_cast<ThreadInfo*>(args);
    string name(ti->threadname_);
    while(true)
    {
        pthread_mutex_lock(ti->lock_); // 加锁
        if(tickets > 0)
        {
            usleep(10000);
            printf("%s get a ticket: %d\n", name.c_str(), tickets);
            tickets--;
            pthread_mutex_unlock(ti->lock_); // 解锁
        }
        else 
        {
            pthread_mutex_unlock(ti->lock_); // 解锁
            break;
        }
        //这里上面的代码
        usleep(13); // 用休眠来模拟抢到票的后续动作
    }
    printf("%s quit...\n", name.c_str());
}

int main()
{
    pthread_mutex_t lock; // 定义互斥锁
    pthread_mutex_init(&lock, nullptr); // 初始化互斥锁
    vector<pthread_t> tids;
    vector<ThreadInfo*> tis;
    for(int i = 1; i <= NUM; i++)
    {
        pthread_t tid;
        ThreadInfo *ti = new ThreadInfo("Thread-"+to_string(i), &lock);
        pthread_create(&tid, nullptr, GrabTickets, ti);
        tids.push_back(tid);
        tis.push_back(ti);
    }

    // 等待所有线程
    for(auto tid : tids)
    {
        pthread_join(tid, nullptr);
    }
    // 释放资源
    for(auto ti : tis)
    {
        delete ti;
    }
	// 销毁互斥锁
    pthread_mutex_destroy(&lock); 
    
    return 0;
}

这样就不会出现好多线程抢到一张票或者抢到不存在的票的问题了

三、深入理解锁

1、解读锁的机制

(一)先入为主原则

我们将上方代码中表示抢到票后续动作的休眠代码注释掉再次执行程序我们会发现,都是线程1抢的票,多次执行代码之后发现这是概率性问题,但是在抢票的时候,有一段时间的票都是一个线程抢到的,我们预想的应该是几乎平均分配的样子

这说明了几个问题:

  • 第一,线程对于锁的竞争能力不同,一定有一个首先抢到锁的线程
  • 第二,一般来说,刚解锁再去抢锁的更容易一些,类似于上面的结果,一直是线程1在抢票

(二)锁和线程

  • 对于上面第二个问题来说,我们有处理方法,这种方法就是同步,同步可以让所有的线程按照一定的顺序获取锁
  • 对于其他线程来讲,一个线程要么获取到了锁,要么释放了锁,当前进程访问临界区的过程对于其他线程是原子的

在加锁期间,即解锁之前,是可以发生线程切换的,线程切换的时候是拿着锁走的,被锁起来的内容其他线程也是访问不到临界区的的,在该线程再次切换回来的时候,恢复线程上下文继续访问临界区代码

(三)锁的特点

加锁的本质就是用时间来换取安全,我们知道在加锁后,临界区的代码只能由一个线程执行,如果是并发执行,至少时间要缩短5倍,但是锁给我们消除了安全隐患,即可能出现的++--的隐患

加锁的表现就是线程对于临界区代码串行执行,一条线从上到下

我们加锁的原则就是尽量保证临界区的代码要少一些,可以使单线程执行的代码量更小,多线程综合处理的代码量更大,提高效率

锁的本身是共享资源,所以加锁和解锁本身就被设计成为了原子性操作(加锁和解锁通过硬件提供的原子指令,结合操作系统内核态的底层同步原语支持以及库层面的合理封装,来确保操作的原子性),这样可以确保在多线程环境下对共享资源加锁和解锁操作的完整性与一致性,避免因多线程并发干扰导致锁状态异常,进而保障线程安全和数据的正确性

2、锁的原理

下面来看一下加锁解锁对应的汇编指令,我们说,一条汇编指令就是原子性的

首先al寄存器中的数字为0时,代表锁已被拿走,为非零(一般为1)时,代表锁当前空闲,可以上锁

加锁机制

  • movb $0, %al:将值 0 移动到 AL 寄存器
  • xchgb %al, mutex:这是一个原子交换指令,将 AL 寄存器中的值(即 0)与 mutex 变量的值交换
  • if (al寄存器的内容 > 0):检查 AL 寄存器中的内容(此时它保存的是原来 mutex 的值),如果值大于 0,说明互斥锁之前没有被锁定,锁定成功,返回 0
  • else:如果 AL 中的值是 0,说明互斥锁已经被锁定,程序会等待
  • goto lock:程序跳转回 lock 标签,重新尝试获取锁

解锁机制

  • movb $1, mutex:将值 1 移动到 mutex
  • xchgb %al, mutex:通过交换 AL 中的值和 mutex,实现解锁
  • return 0:解锁后,函数返回

四、锁的封装

1、LockGuard.hpp

#pragma once
#include <pthread.h>
//简单的封装了一下函数,用的时候方便一些
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock)
    :lock_(lock)
    {}

    void Lock()
    {
        pthread_mutex_lock(lock_);
    }

    void Unlock()
    {
        pthread_mutex_unlock(lock_);
    }
private:
    pthread_mutex_t *lock_;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *lock)
    :mutex_(lock)
    {
        mutex_.Lock(); // 对象创建的时候加锁
    }

    ~LockGuard()
    {
        mutex_.Unlock(); // 对象销毁的时候解锁
    }
private:
    Mutex mutex_;
};
#include <iostream>
#include <pthread.h>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include "LockGuard.hpp"

using namespace std;

#define NUM 4

int tickets = 500; 
//全局变量定义锁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

class ThreadInfo
{
public:
    ThreadInfo(const string &threadname)
        : threadname_(threadname)

public:
    string threadname_;
};

void *GrabTickets(void *args)
{
    ThreadInfo *ti = static_cast<ThreadInfo *>(args);
    string name(ti->threadname_);
    while (true)
    {
        {
            LockGuard lockguard(&lock); // RAII 风格的锁
            if (tickets > 0)
            {
                usleep(10000);
                printf("%s get a ticket: %d\n", name.c_str(), tickets);
                tickets--;
            }
            else
            {
                break;
            }
        }
        usleep(13); // 用休眠来模拟抢到票的后续动作
    }

    printf("%s quit...\n", name.c_str());
}

int main()
{
    vector<pthread_t> tids;
    vector<ThreadInfo *> tis;
    for (int i = 1; i <= NUM; i++)
    {
        pthread_t tid;
        ThreadInfo *ti = new ThreadInfo("Thread-" + to_string(i));
        pthread_create(&tid, nullptr, GrabTickets, ti);
        tids.push_back(tid);
        tis.push_back(ti);
    }

    // 等待所有线程
    for (auto tid : tids)
    {
        pthread_join(tid, nullptr);
    }

    // 释放资源
    for (auto ti : tis)
    {
        delete ti;
    }

    pthread_mutex_destroy(&lock);
    return 0;
}

这里封装的锁是RAII风格的锁,RAII风格是一种在 C++ 等编程语言中利用对象的构造和析构函数来自动管理资源的技术,确保资源在对象创建时获取,在对象生命周期结束时自动释放,以防止资源泄漏并简化资源管理

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Debian 9系统下修改默认网卡为eth0的方法

    Debian 9系统下修改默认网卡为eth0的方法

    这篇文章主要给大家介绍了在Debian 9系统下修改默认网卡为eth0的方法,文中介绍的非常详细,对大家具有一定的参考学习价值,需要的朋友们下面来一起看看吧。
    2017-06-06
  • 解决Centos7下crontab+shell脚本定期自动删除文件问题

    解决Centos7下crontab+shell脚本定期自动删除文件问题

    小编最近遇到这样的需求,就是rsync每次同步的数据量很多,但是需要保留的数据库bak文件,保留7天就够了,所以需要自动清理文件夹内的bak文件。这篇文章主要介绍了解决Centos7下crontab+shell脚本定期自动删除文件问题,需要的朋友可以参考下
    2018-11-11
  • Linux下如何挂载磁盘的方法示例

    Linux下如何挂载磁盘的方法示例

    这篇文章主要介绍了Linux下如何挂载磁盘的方法示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-09-09
  • Linux 文件压缩和打包简介

    Linux 文件压缩和打包简介

    Linux 上常用的压缩/解压工具,介绍了zip、rar、tar的使用。今天我们来详细探讨下
    2018-10-10
  • Linux 文件内容相关命令使用汇总

    Linux 文件内容相关命令使用汇总

    Linux操作系统有很多强大的文件内容相关命令,这些命令可以让您查看、分析和编辑文件。其中,最基本和常用的命令包括cat、more、less和head/tail等。除了这些基本命令之外,grep和find命令也是文件搜索和过滤方面的有力工具。
    2023-04-04
  • Linux保姆级配置vscode连接远端主机以及免密配置过程

    Linux保姆级配置vscode连接远端主机以及免密配置过程

    这篇文章主要介绍了Linux保姆级配置vscode连接远端主机以及免密配置过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-03-03
  • linux把php升级到5.6的实操方法

    linux把php升级到5.6的实操方法

    在本篇文章里小编给大家整理的是关于linux下如何把php升级到5.6的相关知识点,需要的朋友们学习下。
    2019-11-11
  • 等保测评:Centos超时退出问题

    等保测评:Centos超时退出问题

    等保测评主机测评中需要查询主机的超时退出配置,具体在Centos中的话,主要有两种方式可以实现超时退出的功能。文中给大家详细介绍,感兴趣的朋友跟随小编一起看看吧
    2019-07-07
  • Linux系统之基础扫盲教程大全

    Linux系统之基础扫盲教程大全

    本篇文章主要介绍了Linux系统之基础扫盲大全,介绍了网络,系统,cpu,内存,硬盘,进程等等常用的基础信息查看与基础功能设置,有兴趣的可以了解一下。
    2017-04-04
  • 每天一个linux命令(30): chown命令详解

    每天一个linux命令(30): chown命令详解

    本篇文章主要介绍了linux chown命令。chown将指定文件的拥有者改为指定的用户或组,感兴趣的朋友可以了解一下。
    2016-11-11

最新评论