linux之线程互斥
多线程在访问共享资源时的问题
假如我们设置一个全局变量!
int tickets = 1000;//充当有1000张票
void* getTicket(void* args)
{
std::string username = static_cast<const char*>(args);
while(true)
{
if(tickets>0)
{
usleep(1245);//这个是用来模拟线程等待
std::cout << username << " 正在抢票" << tickets<<std::endl;
tickets--;
}
else {
break;
}
}
return nullptr;
}
==让多个线程同时的去执行这个抢票函数——会有有可能出现一个结果,票变为负数!==
==综上我们可以得出一个结论!我们定义的全局变量,在没有保护的时候!往往是线程不安全的!像上面多个线程在交替执行造成的数据安全问题——我们称之为发生了数据不一致问题!==
为了解决问题于是就有了一个解决方案——加锁
进程线程间的互斥相关背景概念
==多个执行流进行安全访问的共享资源——我们称之为临界资源!==
==我们把多个执行流中访问临界资源的代码——我们称之为临界区==
互斥锁
我们说过为了解决多线程访问共享资源的问题于是就有了一个解决方案——加锁
那么锁是什么呢?
我们可以看到是一个pthread_mutex_t
的数据类型——==这就是锁==
如何看待互斥锁?
我们访问临界资源之前!我们肯定就是要访问这把锁!——因为我们要保护共享资源!
那么每一个线程肯定也要看到这把锁!——==所以互斥锁本身就是一个共享资源!==
全局的变量是要被保护的!——锁是用来保护全局的资源的!锁本身也是一个全局的资源!
==那么锁的安全的谁来保护呢?——锁是共享资源!也天然是一种临界资源!==
==我们使用phtread_lock函数进行加锁,使用pthread_unlock进行解锁。所以就必须保证加锁解锁的过程是安全的!==——加锁和解锁的过程都是原子的!所以我们不用担心
==如果申请锁成功,就继续向后执行!如果申请锁暂时没有成功,那么执行流会如何呢?==
加锁后能切换线程吗?
==如上图未来我们使用锁的时候!一定要尽量的保证临界区的粒度要非常的小!==
比如我们使用锁的时候
void* getTicket(void* args)
{
//可以这样加锁
std::string username = static_cast<const char*>(args);
while(true)
{
pthread_mutex_lock(&lock);//直接进行加锁和解锁
if(tickets>0)
{
usleep(1245);
std::cout << username << " 正在抢票" << tickets<<std::endl;
tickets--;
pthread_mutex_unlock(&lock);
}
else {
pthread_mutex_unlock(&lock);
break;
}
usleep(1231);
}
return nullptr;
}
//也可以这样加锁!
void* getTicket(void* args)
{
pthread_mutex_lock(&lock);//直接进行加锁和解锁
std::string username = static_cast<const char*>(args);
while(true)
{
if(tickets>0)
{
usleep(1245);
std::cout << username << " 正在抢票" << tickets<<std::endl;
tickets--;
}
else {
break;
}
usleep(1231);
}
pthread_mutex_unlock(&lock);
return nullptr;
}
粒度就是指——锁中间保护的代码的个数的多少!
因为我们一加锁,就式串行的!不能并行执行!这样子效率就变低了!==所以我们要尽量让临界区变得很短!==将非临界区资源的代码这种可放可不放的,都放在临界区外面!
这样子让多执行流并发执行的时候,让关键的代码安全的访问!然后也能高效率的运行!否则速度就会变的很慢
==加锁这个行为必须做到要加就都加上!不能一个加一个不加!这叫做写代码有bug!==
加锁/解锁的流程
加锁的过程是原子的!解锁也是原子的(但是相对于解锁安全性要去没有那么高!因为一次解锁肯定是只有一个!)
==那么加锁是如何实现原子性的?==
为了实现互斥锁操作,大多数体系结构(例如:x86_32 x86_64或者arm)都提供了swap或exchange指令,==该指令的作用是把寄存器和内存单元的数据相交换==,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。——这是在软件层面实现原子性
==加锁的流程是如下图==
互斥锁的封装
我们想要简单的使用锁!可以对其进行一些简单的设计!——让锁能出作用域后自动的就解锁!
而不是让我们手动的去解锁!
//Mutex.hpp
#pragma once
#include<iostream>
#include<pthread.h>
namespace MYTOOL
{
class Mutex
{
public:
Mutex(pthread_mutex_t *lock_p)
: lock_p_(lock_p)
{
}
void lock()
{
if(lock_p_) pthread_mutex_lock(lock_p_);
}
void unlock()
{
if(lock_p_) pthread_mutex_unlock(lock_p_);
}
~Mutex()
{}
private:
pthread_mutex_t *lock_p_;
};
class LockGuard
{
public:
LockGuard(pthread_mutex_t *mutex)
: mutex_(mutex)
{
mutex_.lock();
}
~LockGuard()
{
mutex_.unlock();
}
private:
Mutex mutex_;
};
}
#include<iostream>
#include<unistd.h>
#include"Mutex.hpp"
void* getTicket(void* args)
{
std::string username = static_cast<const char*>(args);
while(true)
{
MYTOOL::LockGuard lockguard(&lock);
//当构建这个锁的时候会自动的调用构造函数去加锁!只要出来while循环的作用域!那么这个锁就会自动调用析构函数解锁!
///////////////////////////////////////////////////////////
if(tickets>0)
{
usleep(1245);
std::cout << username << " 正在抢票" << tickets<<std::endl;
tickets--;
}
else {
break;//我们也不怕提前break因为只要出了while的作用域,那么这个锁就会自动的解锁!
}
usleep(1231);
///////////////////////////////////////////////////////////////
//这些代码都是属于加锁的范围!
}
return nullptr;
}
//但是我们上面这样子其实让不需要加锁的代码也加了锁!——例如usleep
void* getTicket(void* args)
{
std::string username = static_cast<const char*>(args);
while(true)
{
{
MYTOOL::LockGuard lockguard(&lock);
//我们可以只需要加锁的临界资源区上下加一个花括号!创建一个代码块
//这样子出了这个代码块!那么锁就自动的解锁了!
if(tickets>0)
{
usleep(1245);
{
//我们可以只需要加锁的临界资源区上下加一个花括号!
//这样子出了这个代码块!那么锁就自动的解锁了!
std::cout << username << " 正在抢票" << tickets<<std::endl;
tickets--;
}
}
else
{
break;
}
}
usleep(1231);
}
return nullptr;
}
可重入和线程安全
概念
==线程安全==:多个线程并发同一段代码时,不会出现不同的结果——这就是线程安全。常见对全局变量或者静态变量进行操作, 并且没有锁保护的情况下,会出现该问题。——这就是线程不安全
==重入==:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重 入函数,否则,是不可重入函数。
常见线程安全的情况
每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
类或者接口对于线程来说都是原子操作
多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
可重入函数体内使用了静态的数据结构
常见可重入的情况
不使用全局变量或静态变量
不使用用malloc或者new开辟出的空间
不调用不可重入函数
不返回静态或全局数据,所有数据都有函数的调用者提供
使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
可重入函数是线程安全函数的一种
==线程安全不一定是可重入的,而可重入函数则一定是线程安全的。==
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
死锁
==死锁是指一组执行流,在持有自己锁资源的同时,还去申请对方的锁资源!因为锁是不可抢占的锁(即除非自己主动归还,否则无论对方怎么样都无法拿到),所以就有可能出现多执行流互相等待对方的资源!从而导致代码无法推进的情况!==
==如果破坏死锁==
只要我们破坏上面的四个必要条件中的一个!我们就能破坏死锁!
-
互斥,这个是锁的特性!我们没有办法破坏这个条件
-
请求与保持这个条件该怎么破坏?
==避免死锁具体方案==
-
破坏死锁的四个必要条件
-
加锁顺序一致——避免环路等待
-
避免锁未释放的场景 ——避免请求与保持,方式锁为释放的场景
-
资源一次性分配
==避免死锁算法==
- 死锁检测算法