线程系列:
一、线程的认识:线程的认识:误进解线程的概念和线程的基本控制
二、Linux--线程的分离、线程库的地址关系的理解、线程的简单封装
线程的互斥
线程互斥(Thread Mutual Exclusion)是多线程编程中的一个重要概念,它指的是在任意时刻只允许一个线程访问某一共享资源。这是为了避免多个线程同时访问和修改同一资源,从而导致数据不一致或其他不可预知的问题。
cpp
#include<iostream>
#include<vector>
#include"Thread.hpp"
#include<unistd.h>
#include<pthread.h>
using namespace ThreadMdule;
int g_tickets=1000;
class ThreadData
{
public:
ThreadData(int& tickets,const string& name,pthread_mutex_t& mutex)
:_tickets(tickets),_name(name),_total(0),_mutex(mutex)
{
}
~ThreadData(){}
public:
int &_tickets;//所有线程都会引用到这个全局变量
string _name;
int _total;
pthread_mutex_t& _mutex;
};
void route(ThreadData* td)
{
while(true)
{
//pthread_mutex_lock(&gmutex);//加锁
if(td->_tickets>0)
{
usleep(10000);
printf("get tickets:%d\n",td->_tickets);
td->_tickets--;
//pthread_mutex_unlock(&gmutex);//过了临界区解锁
td->_total++;
}
else
{
//pthread_mutex_unlock(&gmutex);
break;
}
}
}
const int num=4;
int main()
{
pthread_mutex_t mutex;
pthread_mutex_init(&mutex,nullptr);
vector<Thread<ThreadData*>> threads;
vector<ThreadData*> datas;
//创建新线程
for(int i=0;i<num;i++)
{
string name="thread"+to_string(i + 1);
ThreadData* td=new ThreadData(g_tickets,name,mutex);
threads.emplace_back(route,td,name);
datas.emplace_back(td);
}
//启动进程
for(auto& thread:threads)
{
thread.start();
}
//等待进程结束
for(auto& thread:threads)
{
thread.Join();
cout<<"wait thread done,thread is: "<<thread.name()<<endl;
}
//统计数据
for(auto data: datas)
{
cout<<data->_name<<" : "<<data->_total<<endl;
delete data;
}
pthread_mutex_destroy(&mutex);
return 0;
}
共享资源:每个线程可以同时访问的数据或其他资源。程序给出1000张票,让线程来模拟抢票,
总票数g_tickets会从内存中加载到CPU中进行逻辑运算,由于我们的线程没有进行保护,也就是说当票数只有一张的时候,所有线程运行时没有时间先后,是并行运行的;
所以剩1张票时4个线程都在抢,京就会出现负数的情况;我们看到每个线程最后的抢票总数,也是相对平均的,说明是并行运行的;
如何解决互斥问题?
先说一个概念:
临界资源(Critical Resource)是指在多线程或多进程环境中,一次只能被一个线程或进程安全访问的资源 。这种资源如果被多个执行流同时访问,可能会导致数据不一致、数据损坏或其他未定义的行为。
临界资源通常是全局变量、共享数据结构(如链表、树、图等)、文件、网络连接、硬件设备或其他需要被多个执行线程或进程共享的资源。为了避免竞争条件和数据不一致,对临界资源的访问必须受到严格的控制。
为了保护临界资源,程序员通常会使用同步机制来确保在任何时候只有一个执行线程可以访问这些资源;
而使用互斥锁,就是实现同步机制的其中一种常见方法:
互斥锁(Mutex) :一种简单的锁定机制,用于保护临界资源。当一个线程拥有互斥锁时,其他试图获取该锁的线程将被阻塞,直到锁被释放。
互斥锁有关函数
在Linux中,线程互斥锁(Mutex)通常是通过POSIX线程库(pthread)来实现的。以下是与线程互斥锁相关的主要函数:
pthread_mutex_init
: 初始化互斥锁。
cpp
int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
mutex
:指向要初始化的互斥锁对象的指针。attr
:指定互斥锁属性的对象,如果传递NULL,则使用默认的互斥锁属性。
pthread_mutex_destroy
: 销毁互斥锁。
cpp
int pthread_mutex_lock(pthread_mutex_t *mutex);
mutex
:指向要锁定的互斥锁对象的指针。
pthread_mutex_lock
: 锁定互斥锁.
cpp
int pthread_mutex_lock(pthread_mutex_t *mutex);
mutex
:指向要锁定的互斥锁对象的指针。
pthread_mutex_unlock
: 解锁互斥锁。
cpp
`int pthread_mutex_unlock(pthread_mutex_t *mutex);
mutex
:指向要解锁的互斥锁对象的指针。
pthread_mutex_trylock
: 尝试锁定互斥锁,不会阻塞。
cpp
int pthread_mutex_trylock(pthread_mutex_t *mutex);
mutex
:指向要尝试锁定的互斥锁对象的指针
定义全局锁
使用初始化和销毁来实现
线程互斥的底层实现
互斥锁是通过原子操作实现的。原子操作是不间断的单个指令,确保在执行过程中不被中断。
体系结构会提供一个swap或exchange的指令,该指令作用是将内存单元(mutex)和CPU上的寄存器进行数据交换,由于只有一条指令,保证了原子性。当某一个线程执行了这条指令,也就代表临界区被锁住了,那么只有当前这个线程能够过这个临界区,这就实现了对临界资源的保护。
只需要通过一条指令即可实现互斥锁:这是因为当数据在内存中时,所有线程都是可以访问的,属于共享的。如果转移到CPU内部寄存器时,那么这样就变成了私有了!!例如thread1先实现了交换指令,那么对于thread1线程来说寄存器1是私有的,对于临界区仍然可以执行;thread2也想实现临界区的内容,但由于共享资源被锁住,寄存器2又没有交换后的内容,所以thread2无法执行临界区的内容,只能等待thread1解锁后才能得到互斥锁;