目录
2.1 pthread_mutex_init——初始化互斥量
2.2 pthread_mutext_destroy——销毁一个互斥量
2.4 pthread_mutex_trylock——非阻塞的申请锁
⛳️推荐
前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站【Linux修行路】动静态库详解点击跳转到网站
一、多线程模拟抢票
#include <iostream>
#include <pthread.h>
#include <vector>
#include <cstdio>
#include <unistd.h>
using namespace std;
#define NUM 4
int tickets = 500; // 定义1000张票
class ThreaInfo
{
public:
ThreaInfo(const string &threadname)
:threadname_(threadname)
{}
public:
string threadname_;
};
void *GrabTickets(void *args)
{
ThreaInfo *ti = static_cast<ThreaInfo*>(args);
string name(ti->threadname_);
while(true)
{
if(tickets > 0)
{
usleep(1000);
printf("%s get a ticket: %d\n", name.c_str(), tickets);
tickets--;
}
else break;
}
printf("%s quit...\n", name.c_str());
}
int main()
{
vector<pthread_t> tids;
vector<ThreaInfo*> tis;
for(int i = 1; i <= NUM; i++)
{
pthread_t tid;
ThreaInfo *ti = new ThreaInfo("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;
}
return 0;
}
代码中大于0才抢票,但是最终有线程抢到的票号为负数,还有不同的线程抢到了同一张票,这显然是有问题的。
出现该现象的原因:tickets
属于可以被所有线程共享的共享数据。这种情况叫做,共享数据在无保护的情况下,被多线程并发访问,导致的数据不一致问题。对一个全局变量进行多线程并发访问(--
、++
)操作是不安全的。
数据不一致的原因是: --
操作不是原子的,一般来说,--
会被解释成三条汇编,并且一个线程是可以随时被调度的,在执行这三条汇编中的任意一条时,都可能被调度。
逻辑运算也要由 CPU 来执行,当 tickets
为 1 的时候,可能同时有多个线程来判断,线程 A 先来,将 tickets
在内存中的 1 加载到 CPU 的寄存器中,正准备进行逻辑运算的时候,线程 A 被切换出去了,寄存器中的 1 属于该线程的上下文数据,也会被切换出去,此时线程 B 被切换进来,还是先将内存中 tickets
的 1 加载到 CPU 的寄存器中,它运气比较好,一直执行完整个比较,发现还有一张票,会进行抢票操作,将票数减一,此时剩余票数为0,将 0 写回到内存。然后线程 B 被切换出去了,线程 A 被切换进来,A 线程将它的上下文数据恢复到 CPU 的寄存器中,也就是把 1 恢复到CPU 的寄存器中,进行比较,A 线程会认为还有票,接下来进行抢票,打印票数的时候,会重新去内存中读取 tickets
的值,此时内存中 tickets
的值已经被 B 线程修改成 0 了,所以 A 线程买到的就是 0 号票,此时已经有问题了,不可能存在 0 号票,接下来 A 线程对票数进行减减操作,还是先去内存中读取 tickets
的值,也就是 0, 然后对其进行减减操作,此时剩余票数就变成了 -1,然后将-1 写入到内存。这就是多线程对共享数据并发访问产生问题的具体过程。
二、加锁——互斥量
解决上面问题的方法就是加锁,Linux
上提供的这把锁叫做互斥量。**加锁的本质是:串行去执行临界区的代码,是用时间来换安全。**所以加锁的一个原则是:尽量的要保证临界区代码,越少越好。
2.1 pthread_mutex_init——初始化互斥量
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t
-
mutex
:要初始化的互斥量。其中pthread_mutex_t
是一个自定义数据类型,用来表示一个互斥量。 -
attr
:锁的属性,一般设置为nullptr
2.2 pthread_mutext_destroy——销毁一个互斥量
#include <pthread.h>
int pthread_mutex_destroy(pthread_mutex_t *mutex);
2.3 pthread_mutex_lock——加锁
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex);
调用该函数可能出现的两种情况:
-
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
-
发起函数调用时,其它线程已经锁定互斥量,或者存在其它线程同时申请互斥量,但没有竞争到互斥量,那么该函数调用会陷入阻塞(执行六被挂起),等待互斥量解锁。
-
不同线程对锁的竞争能力可能会不同,一个线程刚把锁释放,紧接着就立即去申请锁,那么该进程申请到锁的几率是比其它进程要大的。因为其它进程正处于被挂起的状态,要等待锁被释放,在锁被释放的时候,操作系统要先唤醒这些被挂起的进程,然后去申请锁,这个过程和前面那个一直在运行的进程相比一定是更慢的。在纯互斥环境中,如果锁分配不够合理,容易导致其他线程的饥饿问题(一个线程长时间申请不到互斥量)。因此我们需要想办法,让刚释放锁的线程不能再立即申请到锁。必须排在队伍的最后面。
可能同时存在多个线程在等待一把锁资源,当这个锁被释放的时候,操作系统如果把所有等待的线程全部唤醒,这也是不合理的,因为最终只会有一个线程拿到锁资源。
同步:让所有的线程获取锁,按照一定的顺序,按照一定的顺序性获取资源就叫同步。
所有线程在执行临界区代码访问临界资源之前,都需要先申请锁,所以锁本身就是一种共享资源,这就决定了申请锁和释放锁一定要被设计成原子性操作。
一个线程在执行临界区的代码时,是可以被切换的,在被切出去的时候,是持有锁被切出去的。所以在该线程释放锁资源之前,其它线程是无法进入临界区访问临界资源的。所以,当前线程访问临界区的过程,对于其它线程是原子的。
2.4 pthread_mutex_trylock——非阻塞的申请锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
申请成功返回0,失败错误码被返回。
2.4 pthread_mutex_unlock——解锁
#include <pthread.h>
int pthread_mutex_unlock(pthread_mutex_t *mutex);
2.5 定义一个全局或者静态的锁变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
对于全局和静态的锁,我们在使用的时候就不需要对其进行初始化和销毁。
2.6 加锁后的抢票
#include <iostream>
#include <pthread.h>
#include <vector>
#include <cstdio>
#include <unistd.h>
using namespace std;
#define NUM 4
int tickets = 500; // 定义1000张票
class ThreaInfo
{
public:
ThreaInfo(const string &threadname, pthread_mutex_t *lock)
:threadname_(threadname)
,lock_(lock)
{}
public:
string threadname_;
pthread_mutex_t *lock_;
};
void *GrabTickets(void *args)
{
ThreaInfo *ti = static_cast<ThreaInfo*>(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); // 用休眠来模拟抢到票的后续动作
// pthread_mutex_unlock(ti->lock_); // 不能在这里解锁,因为 tickets == 0 的时候就直接跳出循环了,导致锁没有被释放,其它线程就会阻塞住
}
printf("%s quit...\n", name.c_str());
}
int main()
{
pthread_mutex_t lock; // 定义一个互斥量
pthread_mutex_init(&lock, nullptr); // 初始化互斥量
vector<pthread_t> tids;
vector<ThreaInfo*> tis;
for(int i = 1; i <= NUM; i++)
{
pthread_t tid;
ThreaInfo *ti = new ThreaInfo("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;
}
三、锁的原理
原子性:一件事情要么做了,要么没做,不存在中间状态,在计算机底层,一条汇编就是原子的。
为了实现互斥锁操作,大多数体系结构(CPU 架构)都提供了 swap
或 exchange
指令, 该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。
交换的本质是把内存中的数据 (我们定义的锁),交换到 CPU 的寄存器中,也就是把数据交换到线程硬件的上下文中,一个线程的硬件上下文属于一个线程的私有数据。所以锁本质上,就是把一个共享资源,让一个线程通过一条汇编指令,交换到自己的硬件上下文中。假设锁为1,每个线程都用0去交换,1只有一份,同一时刻就只有一个线程能够交换到1,一个线程交换到了1,就说明该线程申请锁成功。
解锁没有使用 exchange
而是使用 move
,目的是,为了在一个线程申请到锁之后,可以由其它的线程去解锁。使用 exchange
就必须要求申请锁成功的线程去解锁,这样可能会导致死锁问题。
四、锁的封装
// 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; // 定义1000张票
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
class ThreaInfo
{
public:
ThreaInfo(const string &threadname /**, pthread_mutex_t *lock*/)
: threadname_(threadname)
/*,lock_(lock)*/
{
}
public:
string threadname_;
// pthread_mutex_t *lock_;
};
void *GrabTickets(void *args)
{
ThreaInfo *ti = static_cast<ThreaInfo *>(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;
}
// pthread_mutex_unlock(ti->lock_); // 不能在这里解锁,因为 tickets == 0 的时候就直接跳出循环了,导致锁没有被释放,其它线程就会阻塞住
}
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<ThreaInfo *> tis;
for (int i = 1; i <= NUM; i++)
{
pthread_t tid;
ThreaInfo *ti = new ThreaInfo("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;
}
RAII风格:RAII(Resource Acquisition Is Initialization)是由c++之父Bjarne Stroustrup提出的,中文翻译为资源获取即初始化,他说:使用局部对象来管理资源的技术称为资源获取即初始化;这里的资源主要是指操作系统中有限的东西如内存、网络套接字等等,局部对象是指存储在栈的对象,它的生命周期是由操作系统来管理的,无需人工介入。
标签:include,lock,Linux,互斥,mutex,pthread,线程,多线程 From: https://blog.csdn.net/m0_68662723/article/details/141872651