文章目录
- 线程锁的本质
- 局部锁的使用
- 锁的封装及演示
- 线程饥饿问题
- 线程加锁本质
- 可重入和线程安全
- 死锁问题
根据前面内容的概述, 上述我们已经知道了在linux下关于线程封装和线程互斥,锁的相关的概念, 下面就来介绍一下关于线程锁的一些其他概念.
线程锁的本质
当这个锁是全局的或者是静态属性时,可以使用PTHREAD_MUTEX_INITIALIZER (initializer 初始化器(初始化列表那样的东西))
,这个宏来进行初始化.
局部锁的使用
局部的锁就要使用pthread_mutex_init()
创建, pthread_mutex_destroy()
来销毁
回调函数处:
锁的封装及演示
这边引入锁的封装, 将线程名称与锁进行封装的一种保护机制(lock guard):.
意义在于: 创建后再程序结束时会自动释放锁,方便使用
LockGuard.hpp定义
#pragma once
#include <iostream>
//不定义锁,默认认为外部会给我们传入锁对象
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是Mutex的对象,该对象调用对应的方法
{
_mutex.Lock();//调用Mutex类的加锁方法
}
~LockGuard ()
{
_mutex.Unlock();
}
private:
Mutex _mutex;
};
Thread.hpp 对pthread线程的封装实现
#pragma once
#include <iostream>
#include <functional>
#include <pthread.h>
#include <string>
using namespace std;
template<class T>
using func_t = function<void(T)>;//std::function 是C++标准库中的一个模板类,可以包装任何可调用目标(callable target),比如函数、lambda表达式、函数对象(functor)等。
template<class T>
class Thread
{
public:
Thread(const string &name, func_t<T> func, T data)
: _name(name), _func(func), _data(data), _tid(0), _isrunning(false)
{}
static void *ThreadRoutine(void *args)//子线程入口,接受参数为当前对象的指针
{
Thread *t = static_cast<Thread*>(args);//转义为所需要的指针类型,当前的t和this一样,但是不能与库内的this进行重名
t->_func(t->_data); //当前对象调用参数_func(他是一个function类创建的对象,这个类可以包装任何内容,这边包装函数,_func是这个函数模板创建的对象),接受来自Thread创建时的第三个参数
//到这边是完成对整个类的包装,模板概念已经结束,具体操作回到main内查看,对应的函数执行结束后,执行exit(0)
exit(0);
}
bool Start()
{
int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);//创建线程,加载输出OS给的tid,默认方式创建(不设置分离状态,栈大小等),子线程入口,传入参数给子线程
if(n == 0)
{
_isrunning = true;
return true;
}
else
{
return false;
}
}
bool Join()
{
if(!_isrunning)
{
return true;
}
int n = pthread_join(_tid, nullptr);
if(n == 0)
{
_isrunning = false;
return true;
}
else
{
return false;
}
}
~Thread(){}
bool IsRunning()
{
return _isrunning;
}
private:
pthread_t _tid;
string _name;
func_t<T> _func;
T _data;
bool _isrunning;
};
main.cc代码演示
#include "Thread.hpp"
#include <unistd.h>
#include "LockGuard.hpp"
class ThreadData
{
public:
ThreadData(string name, pthread_mutex_t *pmutex)
: _name(name), pmutex(pmutex)
{
}
~ThreadData()
{
}
public:
string _name;
pthread_mutex_t *pmutex;
};
int numsize = 10000;
string GetThreadName()
{
static int num = 1;
return static_cast<string>("Thread-" + to_string(num++));
}
void Print(ThreadData *td)//执行Print方法,参数是来自线程创建函数的第四个参数,这一功能也由线程创建函数实现,功不可没,十分可秒啊
{
//全局内定义的参数进行--操作,验证线程互斥问题
while (true)
{
{//将临界区进行花括号包裹,代码更加明显
LockGuard lockguard(td->pmutex);//利用锁保护功能模块进行加锁(启动锁)
// LockGuard lockguard(&mutex);
// pthread_mutex_lock(mutex);
if (numsize > 0)
{
usleep(1000);
std::cout << td->_name << ", the numsize is: " << numsize << std::endl;
--numsize;
// pthread_mutex_unlock(mutex);
}
else
{
// pthread_mutex_unlock(mutex);
break;
}
}//加锁, 解锁功能结束,一个线程访问临界区的操作也结束,意味着后续线程可以访问这个临界区
//在运行结果时会发现,有时候会出现一个线程把所有numsize都分完了,这是因为线程执行多久是由于时间片决定,当在多线程情况下把所有任务(同一份资源)都做完的情况叫做多线程饥饿问题
}
}
int main()
{
pthread_mutex_t mutex; // 创建锁初始化
pthread_mutex_init(&mutex, nullptr);
string name1 = GetThreadName();//获取线程名称
ThreadData *td1 = new ThreadData(name1, &mutex); // 将锁和线程的名字的信息写入ThreadData,便于管理
Thread<ThreadData *> t1(name1, Print, td1);//为线程创建进行加载对应信息
string name2 = GetThreadName();
ThreadData *td2 = new ThreadData(name2, &mutex);
Thread<ThreadData *> t2(name2, Print, td2);
string name3 = GetThreadName();
ThreadData *td3 = new ThreadData(name3, &mutex);
Thread<ThreadData *> t3(name3, Print, td3);
string name4 = GetThreadName();
ThreadData *td4 = new ThreadData(name4, &mutex);
Thread<ThreadData *> t4(name4, Print, td4);
string name5 = GetThreadName();
ThreadData *td5 = new ThreadData(name5, &mutex);
Thread<ThreadData *> t5(name5, Print, td5);
t1.Start();//线程启动
t2.Start();
t3.Start();
t4.Start();
t5.Start();
t1.Join();//线程等待
t2.Join();
t3.Join();
t4.Join();
t5.Join();
pthread_mutex_destroy(&mutex); // 消除锁
return 0;
}
基于上篇文章定义对main内的一些修改:
线程饥饿问题
再多线程创建后
在运行结果时会发现,有时候会出现一个线程把所有numsize都分完了,这是因为线程执行多久是由于时间片决定,当在多线程情况下把所有任务(同一份资源)都做完的情况叫做多线程饥饿问题.
要解决饥饿问题要让线程在执行时,预备一定的顺序性–这就是线程同步(下章见晓)
线程加锁本质
原子性问题在软硬件层面的体现
软件方面
线程能被调度是因为OS以一种非常快的方式来受理时钟中断,这时就会执行调度进程
硬件方面
把中断关掉,这时只执行进程,OS不会继续执行,这时不会进行调度
大部分的体系结构(像X86,AMD芯片中)会提供swap和exchange汇编级的指令,作用是把寄存器的内容和内存单元的内容进行数据交换
1.exchange eax mem_addr //将eax 和 mem_addr的内容进行交换
直接进行交换,这一个操作是原子性的
2.什么是一把锁?在代码中是创建一个变量,首先把他想象成一个变量struct {int num = 1;}
利用伪代码进行理解:
关于加锁的原则: 谁加锁,谁解锁.
可重入和线程安全
可重入VS线程安全:
可重入还是不可重入描述的是函数的问题,跟线程无关,他描述的是函数的特点,无褒贬之分,函数大部分都是不可重入
线程安全,:
多个线程并发同一段代码时,不会出现不同的结果,常见对全局变量或静态变量进行操作,并且没有锁保护
的情况下,会出现该问题,它描述的是线程的特征
eg:线程访问不可重入函数是线程不安全的情况之一
线程安全的操作:
对于一个全局的变量,在开始改变完他的值之后在退出这个函数之前将值恢复成开始的值,这样来变相的达到线程安全的操作,这只是其中一个例子
可重入与线程安全是二义性
函数可重入意味着当线程进入这个函数是线程安全的
反之,当这个函数不可重入,那么就是线程不安全的
死锁问题
问题解释: 处于一组进程中的各个线程不会释放资源,但因为相互申请被其他进程所占据不会释放资源而处于一种永久等待的状态(多个执行流在一段时间内因为相互牵制不会向后推进)
死锁产生的四个必要条件:
互斥条件:一个资源只能被一个执行流使用(产生死锁的根本原因)
请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放(把自己的锁拿的紧紧地,还伸手向别人要锁)
不剥夺条件:一个执行流已获得的资源,在未使用之前,不能被强行剥夺(锁2不能解锁1)
循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源关系(互相申请对方的的锁的问题,形成了申请的循环)
当上述四个条件都成立才会产生死锁
如何避免死锁呢?
避免死锁:不用锁
但是为了保护共享资源, 提出来的使用锁
核心原理:
1.破坏4个必要条件中的一个或者多个
2.建议, 按照同样的次序进行申请锁的操作(加锁循序尽量保持一致)
尽量把锁的资源,按照申请的资源一次给申请线程了,这样不易出现错误(目前用不到)
3.避免锁未被释放的场景发生
4.资源一次性分配
注:
一个线程也能实现死锁:
比如:不下心把解锁写成了加锁,这个时候就会出错
这个时候是自己阻塞自己