Linux ------ 线程互斥
- [1. 临界资源与临界区](#1. 临界资源与临界区)
- [2. 互斥的定义](#2. 互斥的定义)
- [3. 原子性](#3. 原子性)
- [4. 互斥量(Mutex)](#4. 互斥量(Mutex))
- [5. 互斥的实现示例](#5. 互斥的实现示例)
- [6. 互斥量实现原理探究](#6. 互斥量实现原理探究)
1. 临界资源与临界区
- 临界资源: 指的是多个线程或进程共享的资源 ,例如全局变量、文件、数据库等。由于这些资源的共享,可能会导致数据不一致或程序崩溃。
- 临界区: 是指访问临界资源的代码段。 为了保护临界资源,必须控制对临界区的访问,确保在任何时刻只有一个线程或进程可以进入临界区。
2. 互斥的定义
互斥是一种同步机制,旨在确保同一时刻只有一个执行流(线程或者进程)可以进入临界区。
互斥通常通过互斥量(mutex)来实现。 互斥量是一种锁,线程或进程在访问临界资源之前需要获取这个锁,完成后释放它。
3. 原子性
原子性通俗讲就是指某个操作要么完全执行,要么就完全不执行,不会被其他的进程或线程打断。
原子性操作对于保证数据的一致性和安全性至关重要。比如:在抢票软件的抢票过程中,如果没有原子性则可能出现售出的票数大于总票数的情况。
4. 互斥量(Mutex)
- 互斥量的使用:
- 初始化: 只是用互斥量之前,必须对其进行初始化。可以使用
pthread_mutex_init
函数来进行初始化。 - 加锁: 线程或进程在进入临界区之前,需要调用
pthread_mutex_lock
来获取互斥量的锁。如果锁已经被其他的线程占用了,当前线程将被阻塞,知道锁被释放。 - 解锁: 完成对临界资源的访问之后,必须调用
pthread_mutex_unlock
来释放互斥量的锁。以允许其他线程访问临界区。 - 销毁: 在不再需要互斥量的时候,应该调用
pthread_mutex_destroy
来销毁互斥量,释放有关的资源。
- 初始化: 只是用互斥量之前,必须对其进行初始化。可以使用
我们需要确保每次只有一个执行流进入临界区来访问临界资源,所以就得进行加锁处理,获得锁进而访问临界资源。
5. 互斥的实现示例
下面简单使用自己封装的pthread库来进行演示:
cpp
//Thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
namespace ThreadMoudle
{
typedef void (*func_t)(const std::string &name); // 函数指针
class Thread
{
public:
void Excute()
{
std::cout << _name << " is running ..." << std::endl;
_isrunning = true;
_func(_name);
_isrunning = false;
}
public:
Thread(std::string name, func_t func)
: _name(name), _func(func)
{
std::cout << "create " << name << " done ..." << std::endl;
}
static void *ThreadRountine(void *args)
{
Thread *self = static_cast<Thread *>(args);
self->Excute();
return nullptr;
}
bool Start()
{
int n = ::pthread_create(&_tid, nullptr, ThreadRountine, 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;
}
std::string Name()
{
return _name;
}
~Thread()
{
}
private:
std::string _name;
pthread_t _tid;
bool _isrunning;
func_t _func; // 回调函数
};
}
简单写一个抢票代码,总共有10000张票,创建出4个线程来同时抢票:
cpp
#include <iostream>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include "Thread.hpp"
using namespace ThreadMoudle;
static int ticket = 10000;
void route(const std::string &name)
{
while(true)
{
if(ticket > 0)
{
usleep(1000);
printf("%s get a ticket: %d \n ",name.c_str(),ticket);
ticket--;
}
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();
return 0;
}
我们可以看到出现了抢到负数的情况。这是为什么呢?
- 这是因为在多线程环境中,操作系统会在多个线程之间进行切换和调度,每个线程都有其自己的程序计数器和寄存器,用于记录当前执行的位置和状态。操作系统会通过某种调度算法来决定哪个线程获得CPU时间片。
- 然而当多个线程访问同一个共享资源的时候,他们共同会读取和修改该资源。
假设有两个线程A和B,他们都要访问ticket
变量。线程A和B的执行过程如下:
- 线程A获取
ticket
的值,发现是1,同时线程B也获取ticket
的值,发现同意是2。- 线程A将
ticket
的值减 1 ,结果变为0。- 但是在线程B也将进行减操作的时候,此时ticket已经变为0了,再
--
就变成了负数。
在
- -
的实现中,并不是直接- -
的,而是分为三步,1. 重读数据, 2.- -
数据, 3.写回数据。所以才会出现
ticket
为负数的情况。
进行加锁处理:
cpp
//Thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <pthread.h>
namespace ThreadMoudle
{
class ThreadDate
{
public:
ThreadDate(const std::string & name,pthread_mutex_t *lock)
:_name(name),_lock(lock)
{
}
public:
std::string _name;
pthread_mutex_t * _lock;
};
typedef void (*func_t)(ThreadDate* td); // 函数指针
class Thread
{
public:
void Excute()
{
std::cout << _name << " is running ..." << std::endl;
_isrunning = true;
_func(_td);
_isrunning = false;
}
public:
Thread(std::string name, func_t func,ThreadDate* td)
: _name(name), _func(func),_td(td)
{
std::cout << "create " << name << " done ..." << std::endl;
}
static void *ThreadRountine(void *args)
{
Thread *self = static_cast<Thread *>(args);
self->Excute();
return nullptr;
}
bool Start()
{
int n = ::pthread_create(&_tid, nullptr, ThreadRountine, 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; // 回调函数
ThreadDate* _td;
};
}
cpp
//LockGuard.hpp
#pragma once
#include <pthread.h>
class LockGuard
{
public:
LockGuard(pthread_mutex_t * mutex)
:_mutex(mutex)
{
pthread_mutex_lock(_mutex);
}
~LockGuard()
{
pthread_mutex_unlock(_mutex);
}
private:
pthread_mutex_t * _mutex;
};
cpp
//main.cc
#include <iostream>
#include <vector>
#include <cstdio>
#include <unistd.h>
#include "Thread.hpp"
#include "LockGuard.hpp"
using namespace ThreadMoudle;
static int ticket = 10000;
// pthread_mutex_t gmutex = PTHREAD_MUTEX_INITIALIZER;
// void route(ThreadDate *td)
// {
// std::cout << td->_name << " : " << "mutex address: " << td->_lock << std::endl;
// sleep(1);
// while (true)
// {
// ptherad_mutex_lock(td->_lock);
// if (ticket > 0)
// {
// usleep(1000);
// printf("%s get a ticket: %d \n ", name.c_str(), ticket);
// ticket--;
// }
// else
// {
// break;
// }
// }
// }
void route(ThreadDate *td)
{
while(true)
{
LockGuard lockguard(td->_lock);
if(ticket > 0)
{
usleep(1000);
printf("who %s, get a tickdt: %d\n",td->_name.c_str(),ticket);
ticket--;
}
else
{
break;
}
}
}
static int threadnum = 4;
int main()
{
// Thread t1("thread-1", route);
// Thread t2("thread-2", route);
// Thread t3("thread-3", route);
// Thread t4("thread-4", route);
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);
ThreadDate* td = new ThreadDate(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;
}
以上使用了POSIX线程(pthreads)模拟了C++的多线程模块,定义了一个
Thread
类来封装线程的管理,以及使用一个用于自动互斥锁和解锁的LockGuard
类,以确保线程的安全。main函数中初始化一个互斥锁并且创建多个线程,这些线程执行一个共享的函数
route
,该函数递减共享的票务计数器,同时确保使用LockGuard
的互斥。每个线程都会打印其名称和票的编号,直到票数被抢光。
6. 互斥量实现原理探究
- 经过上面的例子,大家已经意识到单纯的
i++
或者++i
都不是原子的,有可能会有数据一致性问题 - 为了实现互斥锁操作,大多数体系结构都提供了
swap
或exchange
指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。