Linux:线程互斥 | 锁概念 | 锁原理 | 代码实现

文章目录

          • 1.多线程抢票问题
          • 2.互斥的概念
          • 3.互斥量(锁)使用与原理
            • 3.1.抢票系统加锁
            • 3.2.互斥量(锁)实现原理
            • 3.3.锁的封装

完整的代码放在gitee中,需要的请自取: 链接

1.多线程抢票问题

在单核CPU中,操作系统为了营造多个进程在同时运行的假象,它会为每个进程分配时间片,如果一个进程的时间片结束,那么就会进程切换,由于时间片很短,就造成并发的现象。

如果一个进程只有一个执行流,那么它就是单线程,当然一个进程可以由多个执行了,也就是多线程程序。多个线程之间,共享进程的大部分资源,但线程也有一套自己的寄存器和独立的栈结构。

为了提升并发度,当多个线程访问临界资源的时候,会因为时序问题(时钟中断),导致数据不一致问题!

接下来就使用Linux下的原生线程库来模拟多个线程竞争共享资源,导致数据不一致的多线程抢票问题。
抢票代码1.0版本:

#define NUM 3
int ticket = 1000;

void *getTicket(void *args)
{
    uint64_t number = (uint64_t)args;
    while (true)
    {
        //可能当多个线程判断为真之后,就切换,多个线程,切换回来继续执行,ticke此时等于1,有两个线程进来,对ticket--两次,所以会出现负数。
        if (ticket > 0) 
        {
            usleep(1000);  // 模拟业务
            cout << "the thread" << number << " get ticket " 
                 << ticket << endl;   
            ticket--;
        }
        else
            break;
        usleep(10);  //注意这里!
    }
}
int main()
{
    vector tids;
    
    for (uint64_t i = 0; i < NUM; i++)
    {
        pthread_t tid;       
        pthread_create(&tid, nullptr, getTicket, (void *)i);
        tids.push_back(tid);
    }

    for(auto tid : tids)
    {
        pthread_join(tid,nullptr);
    }
    return 0;
}

输出结果:

the thread0 get ticket 1000
.......
the thread0 get ticket 1
the thread1 get ticket 0
the thread2 get ticket -1

为什么会出现这样无法取得正确的结果

  • if 语句判断条件为真以后,线程切换
  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段
  • –ticket 操作本身就不是一个原子操作

一行汇编就是原子的,但是--操作对应的三行汇编,我们可以一些工具查看到。

这里使用 -g 选项保留了调试信息,objdump 可以显示源代码和汇编的对应关系,更容易找到对应源代码和汇编的对应关系。

[xiyan@hecs-34711 mutual]$ g++ -g -o mutual mutual.cc -std=c++11 -lpthread
[xiyan@hecs-34711 mutual]$ objdump -Sd mutual > mutual.objdump

查看对应的mutual.objdump可以找到,ticket–对应的汇编

            ticket--;
  400b05:	8b 05 91 15 20 00    mov    0x201591(%rip),%eax # 60209c <ticket>
  400b0b:	83 e8 01             sub    $0x1,%eax
  400b0e:	89 05 88 15 20 00    mov    %eax,0x201588(%rip) # 60209c <ticket>

如图示例ticket--操作:

  • load :将共享变量ticket从内存加载到寄存器中
  • update : 更新寄存器里面的值,执行-1操作
  • store :将新值,从寄存器写回共享变量ticket的内存地址
    Linux:线程互斥 | 锁概念 | 锁原理 | 代码实现_第1张图片
2.互斥的概念

在上面的例子中,多个线程访问的票是共享资源,把多线程执行流共享的资源就叫做临界资源;而访问临界资源的代码称之为临界区。在上面的抢票代码1.0版本当中,多个线程进入临界区,修改临界资源,输出的结果是存在不确定性,这对于一个抢票系统而言是不能忍受的。

所以要对临界资源进行保护,保证任何时刻,有且只有一个执行流进入临界区,访问临界资源,这就是互斥。

Linux:线程互斥 | 锁概念 | 锁原理 | 代码实现_第2张图片

注意:多线程或多进程在访问临界资源的时候,为了保护资源的安全性,都可以采用互斥的方法。

如何解决上面出现数据不确定性的问题

如果要解决这个问题:1.使用互斥的方法,当一个线程,进入临界区执行,其他的线程就不允许进入该临界区 2.临界区中要保证只有一个线程进入 3.另外,如果一个线程从临界区中离开,它就不能阻止其他线程进入临界区。

如果要满足这三点,本质上只需要加一把锁!用于管理资源竞争和保证数据一致性,Linux中提供的锁称为互斥量。下面使用互斥量来实现互斥代码。

3.互斥量(锁)使用与原理

接口说明

初始化互斥量

  • 方法一:静态分配
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER
  • 方法二:动态分配
int pthread_mutex_init(pthread_mutex_t *restrict mutex, 
					   const pthread_mutexattr_t *restrictattr);
参数:
    mutex:要初始化的互斥量
    restrictattr:NULL

销毁互斥量

int pthread_mutex_destroy(pthread_mutex_t *mutex)

注意:

  • 使用静态分配PTHREAD_MUTEX_INITIALIZER的互斥量不用手动释放

  • 不要销毁已经加锁的互斥量,另外,已经释放的互斥量要保证不会有线程在尝试加锁

互斥量加锁和解锁

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

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

在调用 pthread_ lock时:1.互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功;2.发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

3.1.抢票系统加锁

售票系统2.0:

//方法一:静态分配
//pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
//方法二:动态分配
pthread_mutex_t lock;

#define NUM 3
int ticket = 1000;

void *getTicket(void *args)
{
    uint64_t number = (uint64_t)(args);
    while (true)
    {
        pthread_mutex_lock(&lock);
        if (ticket > 0)
        {	
            // 如果线程切换走根本不怕,锁携带的线程的上下文,其他线程也进不了临界区
            usleep(100);
            cout << "the thread" << number << " get ticket "
                 << ticket << endl;   
            ticket--;
            pthread_mutex_unlock(&lock);
        }
        else
        {
            pthread_mutex_unlock(&lock);
            break;
        }
        //刚抢完一张票还会立即抢吗,这里usleep(10)模拟抢票的业务逻辑,我们没有这会怎么样?
        //线程饥饿问题!
        usleep(10);  
    }
}

int main()
{
    pthread_mutex_init(&lock,nullptr);
    vector<pthread_t> tids;

    for (uint64_t i = 0; i < NUM; i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, getTicket, (void*)i);
        tids.push_back(tid); 
    }
    for(auto tid : tids)
    {
        pthread_join(tid,nullptr);
    }
    pthread_mutex_destroy(&lock);

    return 0;
}

上面的代码加锁,保证了临界区的互斥问题,虽然加锁会导致一个程度的效率降低,但是这是用时间换安全的一种做法!注意,锁也会被多个线程同时访问的,锁也是临界资源!所以锁被设计出来本身就是原子的

上面的代码中,一个线程刚抢完票,usleep(10),如果没有会怎么样

如果没有usleep模拟的业务,,在纯互斥的场景下如果锁资源分配不均匀,可能会导致线程饥饿的问题,因为线程抢夺锁资源的能力是不一样的。就比如,你手边有一块蛋糕, 你舍友离你十米开外然后跟你抢夺这块蛋糕他是抢不过你的!

没有usleep(10)我的部分输出结果:

the thread1 get ticket 4
the thread1 get ticket 3
the thread1 get ticket 2
the thread1 get ticket 1
3.2.互斥量(锁)实现原理

i++和i–都不是原子的操作,那么用锁来保护临界资源,锁也是共享资源,多个线程同时访问锁也是在竞争锁,那么pthread_mutex_lock(&lock)的操作也必须是原子的,怎么做到

我们先来看pthread_mutex_lock(&lock)加锁的伪代码

lock:
		movb $0, %al
		xchgb %al, mutex
		if(al寄存器的内容 > 0) return 0;
		else 挂起等待;
		goto lock

一行汇编就是原子的,要么执行,要么不执行!在大多数体系中为了实现互斥锁操作,都提供了swap或exchange指令(注:指令集:CPU能直接识别的基本指令),该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。
Linux:线程互斥 | 锁概念 | 锁原理 | 代码实现_第3张图片

  1. mutex的本质是一个变量,可以设为1
  2. movb $0, %al,将al寄存器中的值制为0
  3. xchgb %al, mutex,将内存中mutex的值经行交互,判断 > 0 说明到锁了
  4. 会有问题lock对应这么对条汇编指令,一个线程会不会执行到其中的某一步,就切换,是有可能的,但是线程切换,会将对应寄存器的上的值保存到自己私有的上下文中,如果一个线程执行了xchgb %al, mutex汇编交互语句,切换时,就会带着锁切换,其他线程一样申请不到,这样巧妙地设计保证了pthread_mutex_lock(&lock)加锁的原子性
  5. 通过xchgb的交换,本质把内存中的共享资源(mutex)交换的线程私有的上下文中,也就是说哪个线程先执行xchgb %al, mutex汇编语句就能先申请到锁!

我们再来看pthread_mutex_unlock解锁的伪代码

unlock:
		movb $1,mutex;
        唤醒等待mutex的线程;
        return 0;
  1. 解锁不用保证原子性movb $1,mutex;直接将内存的mutex置成1,这样不用申请锁的线程归还,还可以使用其他的线程对加锁的线程解锁
3.3.锁的封装

我们使用RAII风格对原生的锁进行一定的方式。注:RAII(Resource Acquisition Is Initialization)是一种 C++ 中的编程风格,它的核心思想是通过在对象的构造函数中获取资源,然后在对象的析构函数中释放资源。

class Mutex
{
public:
    Mutex(pthread_mutex_t* lock)
        :lock_(lock)
    {}
    void lock()
    {
        pthread_mutex_lock(lock_);
    }
    void unlock()
    {
        pthread_mutex_unlock(lock_);
    }
    ~Mutex(){}
private:
    pthread_mutex_t* lock_;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *lock)
        :mutex_(lock)
    {
        mutex_.lock();
    }
    ~LockGuard()
    {
        mutex_.unlock();
    }
private:
    Mutex mutex_;
};

售票系统3.0:

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

#define NUM 3
int ticket = 1000;

void *getTicket(void *args)
{
    uint64_t number = (uint64_t)(args);
    while (true)
    {
        // 出了这个花括号,临时对象就析构
        {
            // 临时的LockGuard对象, RAII风格的锁!
            LockGuard lockGuard(&lock);
            if (ticket > 0)
            {
                usleep(100);
                cout << "the thread" << number << " get ticket " 
                     << ticket << endl;
                ticket--;
            }
            else
                break;     
        }
        usleep(10);
    }
}

int main()
{
    vector<pthread_t> tids;

    for (uint64_t i = 0; i < NUM; i++)
    {
        pthread_t tid;
        pthread_create(&tid, nullptr, getTicket, (void *)i);
        tids.push_back(tid);
    }
    for (auto tid : tids)
    {
        pthread_join(tid, nullptr);
    }

    return 0;
}

你可能感兴趣的:(操作系统:Linux,linux,java,运维)