勿忘初心做自己 发表于 2025-1-1 08:07:04

【Linux-多线程】线程互斥(锁和它的接口等)

一、线程互斥

我们把多个线程可以或许看到的资源叫做共享资源,我们对共享资源举行保护,就是互斥
1.多线程访问问题

【示例】见一见多线程访问问题,下面是一个抢票的代码,共计票数10000张,4个线程去抢
之前我们展示过封装代码,这里我们直接使用
#include <iostream>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include "mythread.hpp"

using namespace TreadMoudle;

int tickets = 10000;

void route(const std::string &name)
{
    while (true)
    {
      if (tickets > 0)
      {
            // 抢票过程
            usleep(1000); // 1ms -> 抢票花费的时间
            printf("who: %s, get a ticket: %d\n", name.c_str(), tickets);
            tickets--;
      }
      else
      {
            break;
      }
    }
}

int main()
{
    Thread t1("thread-1", route);
    Thread t2("thread-2", route);
    Thread t3("thread-3", route);
    Thread t4("thread-4", route);

    t1.Start();
    t2.Start();
    t3.Start();
    t4.Start();

    t1.Join();
    t2.Join();
    t3.Join();
    t4.Join();
} 【测试结果】:四个线程均抢完票,而且等候乐成,回收乐成;但是我们却发现一个问题,票一共只有1w张,理应最后一个线程得到的票号是1,这里确出现了负数,这是什么原因?
https://i-blog.csdnimg.cn/direct/8838c28a35904123acb4c97d28a3cca0.png
【解释】:盘算机的运算类型有算数运算和逻辑运算,而且是CPU中寄存器举行运算,在CPU内,寄存器只有一套,但是寄存器内里的数据,可以有多套;这些数据属于线程私有,看起来放在了一套公共的寄存器中,但是属于线程私有,当他被切换的时间,他要带走本身的数据!返来的时间会恢复;


[*] 我们寻常所用的一条代码,在汇编层上大概会对应很多汇编语句,好比一个简单的 tickets-- ,就扳连到至少三条指令:1.重读数据,2.--数据,3.写回数据;
[*] 因此在进入抢票的过程中,看似就几行代码,到了汇编层就是很多代码,CPU是会举行线程切换,这样就会发生数据不一至的问题,怎样明白?我们看图
https://i-blog.csdnimg.cn/direct/d550c8b555274d77b89f2f2ad247cef7.png
 https://i-blog.csdnimg.cn/direct/7d51eea5aae5450e8a11800f643d35a7.png
怎样办理这种问题?加锁!!
2.认识锁和它的接口 

pthread_mutex_lock

pthread_mutex_lock 是一个在多线程编程中用于锁定互斥量(mutex)的函数。以下是关于 pthread_mutex_lock 的详细阐明:
函数原型:该函数的原型界说如下:
https://i-blog.csdnimg.cn/direct/05876814925a47c6813baf3d04bb445d.png
参数:pthread_mutex_t是互斥锁的类型,任何时刻,只允许一个线程举行资源访问
功能形貌:当调用 pthread_mutex_lock 时,它将尝试锁定 mutex 参数指向的互斥量。如果这个互斥量当前没有被锁定,它将被锁定,而且调用该函数的线程将成为互斥量的所有者,函数会立即返回。如果互斥量已经被其他线程锁定,那么调用该函数的线程将会壅闭,直到互斥量被解锁。
互斥量的状态:互斥量有两种状态:未锁定(此时不被任何线程拥有)和锁定(此时被一个线程拥有)。一个互斥量不能同时被两个差别的线程所拥有。如果一个线程尝试锁定一个已经被其他线程锁定的互斥量,它将会等候,直到那个线程解锁互斥量。
返回值:如果函数实行乐成,返回值为 0。如果发生错误,比方尝试重新锁定已经被同一个线程锁定的互斥量,函数将返回一个错误码。

pthread_mutex_t

pthread_mutex_t 是 POSIX 线程(通常称为 pthreads)API 中界说的一个数据类型,用于表示互斥量(mutex)。互斥量是一种同步机制,用于防止多个线程同时访问共享资源,从而制止竞态条件。
界说:
https://i-blog.csdnimg.cn/direct/31f274aa88084ba0a03436d72cbae8a1.png
 
注意,pthread_mutex_t 是一个不透明的数据类型,其内部布局对用户是潜伏的。用户不应该尝试直接访问或修改这个布局体的内容。
初始化: 在使用 pthread_mutex_t 之前,必须对其举行初始化。互斥量可以通过以下几种方式举行初始化:
静态初始化(锁是全局的或者静态的):可以在声明时直接使用宏 PTHREAD_MUTEX_INITIALIZER 举行初始化。
https://i-blog.csdnimg.cn/direct/cd290558496b4451a2d2c5ffa4f33159.png
动态初始化:使用 pthread_mutex_init 函数举行初始化。
https://i-blog.csdnimg.cn/direct/701e38789946488899b15c2afd3d6ba8.png 
此中 attr 是一个指向 pthread_mutexattr_t 布局的指针,该布局用于设置互斥量的属性。如果 attr 为 NULL,则互斥量将使用默认属性。
烧毁: 当不再须要互斥量时,应该使用 pthread_mutex_destroy 函数来释放它所占用的资源。 
https://i-blog.csdnimg.cn/direct/566e9f2195f9459f953017aff95414bf.png
在烧毁一个互斥量之前,必须确保没有线程正在等候或持有该互斥量。
锁定与解锁:


[*] 使用 pthread_mutex_lock 尝试锁定互斥量。如果互斥量已被锁定,调用线程将壅闭直到互斥量被解锁。
[*] 使用 pthread_mutex_trylock 尝试锁定互斥量,但不会壅闭;如果互斥量已被锁定,则立即返回一个错误码。
[*] 使用 pthread_mutex_unlock 解锁互斥量。
属性: 互斥量可以有差别的属性,如类型(平常、递归、错误查抄等),这些属性可以通过 pthread_mutexattr_t 布局来设置。
错误处理: 所有与互斥量相干的函数在出错时都会返回错误码,可以通过 strerror 函数或 perror 函数来获取错误信息。
pthread_mutex_lock / _unlock

https://i-blog.csdnimg.cn/direct/66fba38126624766991e80070a479499.png
 
pthread_mutex_lock 是 POSIX 线程(pthreads)库中的一个函数,用于锁定一个互斥量(mutex)。当一个线程调用 pthread_mutex_lock 尝试锁定一个互斥量时,以下环境大概会发生:
❍ 如果互斥量当前是未锁定的状态,调用线程会乐成锁定该互斥量,并继续实行。
❍ 如果互斥量已经被另一个线程锁定,调用线程将会壅闭,直到该互斥量被解锁。

pthread_mutex_unlock 函数,这是一个 POSIX 线程(pthreads)库中的函数,用于解锁一个互斥量(mutex)。当一个线程完成了对临界区的访问后,它应该解锁互斥量,以便其他线程可以锁定并访问该临界区。
pthread_mutex_trylock

pthread_mutex_trylock 是 POSIX 线程(pthreads)库中的一个函数,用于尝试锁定一个互斥量(mutex),但它与 pthread_mutex_lock 的重要区别在于,如果互斥量已经被锁定,pthread_mutex_trylock 不会壅闭调用线程,而是立即返回一个错误码。
https://i-blog.csdnimg.cn/direct/79c3a2cad1d34368aa3f45edbf4cdac1.png
返回值:


[*] 乐成时(互斥量被乐成锁定),返回 0。
[*] 如果互斥量已经被锁定,返回 EBUSY。
[*] 出现其他错误时,返回其他错误编号。
使用场景:


[*] 非壅闭互斥量锁定:当线程不希望因等候互斥量而壅闭时,可以使用 pthread_mutex_trylock。
[*] 制止死锁:通过尝试锁定互斥量,线程可以决定是否继续实行或接纳其他操作,从而制止死锁。
[*] 优先级继续:在某些实时系统中,为了制止优先级反转,可以使用 pthread_mutex_trylock 来尝试锁定互斥量。
注意事项


[*] 错误处理:调用 pthread_mutex_trylock 时,应查抄返回值,并根据返回值做出相应的处理。
[*] 资源释放:如果 pthread_mutex_trylock 返回 EBUSY,线程应该释放已经持有的资源,制止资源泄露。
[*] 重试策略:通常在使用 pthread_mutex_trylock 时,如果返回 EBUSY,线程大概会在一段时间后重试锁定。
学会锁的根本使用后我们就可以修改我们本身实现的多线程,而且重新举行抢票
mythread.hpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>

namespace ThreadMoudle
{
    class ThreadData
    {
    public:
      ThreadData(const std::string &name, pthread_mutex_t *lock):_name(name), _lock(lock)
      {}
    public:
      std::string _name;
      pthread_mutex_t *_lock;
    };

    // 线程要执行的方法,后面我们随时调整
    typedef void (*func_t)(ThreadData *td); // 函数指针类型

    class Thread
    {
    public:
      void Excute()
      {
            std::cout << _name << " is running" << std::endl;
            _isrunning = true;
            _func(_td);
            _isrunning = false;
      }
    public:
      Thread(const std::string &name, func_t func, ThreadData *td):_name(name), _func(func), _td(td)
      {
            std::cout << "create " << name << " done" << std::endl;
      }
      static void *ThreadRoutine(void *args) // 新线程都会执行该方法!
      {
            Thread *self = static_cast<Thread*>(args); // 获得了当前对象
            self->Excute();
            return nullptr;
      }
      bool Start()
      {
            int n = ::pthread_create(&_tid, nullptr, ThreadRoutine, this);
            if(n != 0) return false;
            return true;
      }
      std::string Status()
      {
            if(_isrunning) return "running";
            else return "sleep";
      }
      void Stop()
      {
            if(_isrunning)
            {
                ::pthread_cancel(_tid);
                _isrunning = false;
                std::cout << _name << " Stop" << std::endl;
            }
      }
      void Join()
      {
            ::pthread_join(_tid, nullptr);
            std::cout << _name << " Joined" << std::endl;
            delete _td;
      }
      std::string Name()
      {
            return _name;
      }
      ~Thread()
      {
      }

    private:
      std::string _name;
      pthread_t _tid;
      bool _isrunning;
      func_t _func; // 线程要执行的回调函数
      ThreadData *_td;
    };
} // namespace ThreadModle  main.cc
#include <iostream>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include "mythread.hpp"

using namespace ThreadMoudle;
int tickets = 10000; // 共享资源,造成了数据不一致的问题

pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
static int threadnum = 4;

void route(ThreadData *td)
{
    // std::cout <<td->_name << ": " << "mutex address: " << td->_lock << std::endl;
    // sleep(1);
    while (true)
    {
      pthread_mutex_lock(td->_lock);
      if (tickets > 0)
      {
            // 抢票过程
            usleep(1000); // 1ms -> 抢票花费的时间
            printf("who: %s, get a ticket: %d\n", td->_name.c_str(), tickets);
            tickets--;
            pthread_mutex_unlock(td->_lock);
      }
      else
      {
            pthread_mutex_unlock(td->_lock);
            break;
      }
    }
}

int main()
{
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex, nullptr);

    std::vector<Thread> threads;
    for (int i = 0; i < threadnum; i++)
    {
      std::string name = "thread-" + std::to_string(i + 1);
      ThreadData *td = new ThreadData(name, &mutex);
      threads.emplace_back(name, route, td);
    }

    for (auto &thread : threads)
    {
      thread.Start();
    }

    for (auto &thread : threads)
    {
      thread.Join();
    }

    pthread_mutex_destroy(&mutex);
    return 0;
} 【结果】:此时重新实行步调就不会出现数据不同等问题了
https://i-blog.csdnimg.cn/direct/f0b6c0b8fff14b80818ab5b1b9e94979.png
所谓的对临界资源举行保护,本质是对临界区代码举行保护
我们对所有资源举行访问,本质都是通过代码举行访问的!保护资源,本质就是把访问资源的代码保护起来
3.办理汗青问题


[*] 所以,加锁的范围,粒度一定要尽量小
[*] 任何线程,要举行抢票,都得先申请锁,原则上不应该有例外
[*] 所有线程申请锁,前提是所有的线程都得看到这把锁,锁本身也是共享资源----加锁的过程,必须是原子的
[*] 原子性:要么不做,要做就做完,没有中间状态,就是原子性
[*] 如果线程申请锁失败了,我的线程要被壅闭
[*] 如果线程申请锁乐成了,继续向后运行
[*] 如果线程申请锁乐成了,实行临界区的代码了,实行临界区代码期间是可以发生切换的(好比时间片到了),但是纵然切换了,其他线程无法进入!因为我虽然被切换了,但是我没有释放锁,我可以放心的实行完毕,没有人能打扰我
结论:所以对于其他线程,要么我没有申请锁,要么我释放了锁,对其他线程才故意义
4.原理角度明白这个锁

 
5.从实现角度明白锁

✸ 经过上面的例子,各人已经意识到单纯的i++或者++i 都不是原子的,有大概会有数据同等性问题
✸ 为了实现互斥锁的操作,大多数体系布局都提供了 swap 或 exchange指令,该指令的作用是把寄存器和内存单元的数据相互换,由于只有一条指令,包管了原子性,纵然是多处理器平台访问内存的,总线周期也有先后,一个处理器上的互换指令实行时另一个处理器的互换指令只能等候总线周期。
https://i-blog.csdnimg.cn/direct/c7cb1bfd14eb403f9f798089bdc2e201.png
https://i-blog.csdnimg.cn/direct/dd6909493a99439cb5103c25c8542366.png 
◉ CPU的寄存器只有一套,被所有的线程共享。但是寄存器内里的数据,属于实行流的上下文,属于实行流私有的数据
◉ CPU在实行代码的时间,一定要有对应的实行载体 -- 线程 && 历程
◉ 数据在内存中,被所有线程是共享的
结论:把数据从内存移动到CPU寄存器中,本质是把数据从共享,变成线程私有
 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【Linux-多线程】线程互斥(锁和它的接口等)