10.多线程
预备知识
- 预备知识一
- 预备知识二
- 预备知识三
-
如何理解进程和线程的关系,举一个生活中的例子
- 家庭:进程
- 家庭成员:线程
每个家庭成员都会为这个家庭做贡献,只不过大家都在做不同的事情(比如:我们在上学,父母在上班,爷爷奶奶打理屋子等等)
-
如何证明以上我们所说的进程和线程的关系?
- a.直接编写代码 ==> 见见猪跑
- b.见下一个概念,论证上面的观点
pthread_create()
功能:pthread_create - create a new thread
// 头文件
#include <pthread.h>
// 函数
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
// 参数
pthread_t *thread : pthread_t 其本质就是一个整数
const pthread_attr_t *attr : 线程属性,我们目前不管,直接设置为nullptr
void *(*start_routine) (void *) : 其本质是一个函数指针,我们需要将线程所要调用的方法(也就是一个函数)的地址传递给这个函数指针。
void *arg :是void *(*start_routine) (void *)的参数,也就是所要调用的方法所需要传递的参数
makefile
// -L指定库的路径,因为动态库在系统的默认搜索路径下,因此不需要去指定库所在路径
// -l 后面加静态库/动态库的名称(名称要去掉前缀lib,和后缀.a),指定库的名称
mythread:mythread.cc
g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
rm -f mythread
mythread.cc
#include <iostream>
#include <cstdio>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 新线程
void *thread_routine(void *args)
{
const char *name = (const char *)args;
while (true)
{
cout << "我是新线程, 我正在运行! name: " << name << endl;
sleep(1);
}
}
int main()
{
// pthread_t其实就是一个无符号的长整型
// typedef unsigned long int pthread_t;
// tid 是线程id
pthread_t tid;
int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread one");
assert(0 == n);
(void)n;
// 主线程
while (true)
{
char tidbuffer[64];
snprintf(tidbuffer, sizeof(tidbuffer), "0x%x", tid);
cout << "我是主线程, 我正在运行!, 我创建出来的线程的tid: " << tidbuffer << endl;
sleep(1);
}
return 0;
}
-
运行结果为:
-
查看线程(轻量级进程)
- 在Linux中,
ps -aL
是一个用于显示进程信息的命令。具体来说,ps
是用于报告当前运行进程的命令,-a
选项表示显示所有用户的进程,而-L
选项表示显示每个线程的详细信息。所以,ps -aL
将显示系统中所有用户的所有线程的进程信息。
- 在Linux中,
-
线程一旦被创建,几乎所有的资源都是被所有线程所共享的
// mythread.cc
#include <iostream>
#include <cstdio>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 是被主线程和新线程所共享的,一个线程修改这个变量,另一个线程立马就可以看到对应的资源
int g_val = 0;
// 主线程和新线程都可以调用这个方法
std::string fun()
{
return "我是一个独立的方法";
// 新线程
void *thread_routine(void *args)
{
const char *name = (const char *)args;
while (true)
{
fun();
cout << "我是新线程, 我正在运行! name: " << name << " : "<< fun() << " : " << g_val++ << " &g_val : " << &g_val << endl;
sleep(1);
}
}
int main()
{
pthread_t tid;
int n = pthread_create(&tid, nullptr, thread_routine, (void *)"thread one");
assert(0 == n);
(void)n;
// 主线程
while (true)
{
char tidbuffer[64];
snprintf(tidbuffer, sizeof(tidbuffer), "0x%x", tid);
cout << "我是主线程, 我正在运行!, 我创建出来的线程的tid: " << tidbuffer << " : " << g_val << " &g_val : " << &g_val << endl;
sleep(1);
}
return 0;
}
// 运行结果如下:
- 线程也一定要有自己私有的资源,什么资源应该是线程私有的呢?
- PCB属性私有
- 要有私有的上下文结构
- 每一个进程都要有自己独立的栈结构(如临时变量就需要在栈结构上存储,是属于一个线程私有的资源)
什么是线程
- 在一个程序里的一个执行路线就叫做线程(thread)。更准确的定义是:线程是“一个进程内部的控制序
列”
一切进程至少都有一个执行线程
线程在进程内部运行,本质是在进程地址空间内运行
在Linux系统中,在CPU眼中,看到的PCB都要比传统的进程更加轻量化
透过进程虚拟地址空间,可以看到进程的大部分资源,将进程资源合理分配给每个执行流,就形成了线程
执行流
线程的优点
创建一个新线程的代价要比创建一个新进程小得多
- 进程:创建PCB、进程地址空间、页表、构建进程地址空间和物理空间的映射关系、加载代码和数据等等。
- 线程:只需要创建PCB分配资源就可以了。
与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 进程:切换页表、切换地址空间、切换PCB、切换上下文等等
- 线程:切换PCB、切换上下文
- 线程切换cache只需要少量的数据更新,而进程切换,则需要全部更新
线程占用的资源要比进程少很多
能充分利用多处理器的可并行数量
在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- 计算密集型应用(CPU、加密、解密、算法等),就比如对一个文件进行解压,可以多个线程各自解压这个文件中的一部分数据。
- I/O密集型应用(外设、访问磁盘、显示器、网络等)
I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
线程的缺点
性能损失
- 一个很少被外部事件阻塞的计算密集型线程往往无法与其它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。(简单来说就是一个CPU如果是单核,那么一个线程的性能是最佳的,如果是多核那么可以有多个线程,所谓的核我们可以理解为是CPU内部的运算器,而控制器只有一个)
健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了
不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。
缺乏访问控制
- 进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。
编程难度提高
- 编写与调试一个多线程程序比单线程程序困难得多
线程异常
单个线程如果出现除零,野指针问题导致线程崩溃,进程也会随着崩溃
线程是进程的执行分支,线程出异常,就类似进程出异常,进而触发信号机制,终止进程,进程终止,该
进程内的所有线程也就随即退出
线程用途
合理的使用多线程,能提高CPU密集型程序的执行效率
合理的使用多线程,能提高IO密集型程序的用户体验(如生活中我们一边写代码一边下载开发工具,就是
多线程运行的一种表现)
Linux进程VS线程
进程是资源分配的基本单位
线程是调度的基本单位
线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
进程的多个线程共享同一地址空间,因此Text Segment、Data Segment都是共享的,如果定义一个函数,在各线程
中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
进程和线程的关系如下图:
验证线程的健壮性
- static_cast的用法
static_cast
是 C++ 中用于进行安全的强制类型转换的操作符。它可以在编译时检查类型转换的安全性,并提供了一些类型转换的功能。
static_cast
可以用于以下几种类型转换:
- 基本数据类型之间的转换:例如,将整数类型转换为浮点数类型,或者将浮点数类型转换为整数类型。
int num = 10; double d = static_cast<double>(num); // 将整数转换为浮点数
- 类层次结构中的指针或引用类型的转换:例如,将基类指针或引用转换为派生类指针或引用。
class Base {}; class Derived : public Base {}; Base* basePtr = new Derived(); Derived* derivedPtr = static_cast<Derived*>(basePtr); // 将基类指针转换为派生类指针
- 隐式转换的类型转换:例如,将枚举类型转换为整数类型。
enum Color { RED, GREEN, BLUE }; int num = static_cast<int>(RED); // 将枚举类型转换为整数类型
需要注意的是,
static_cast
并不能执行所有类型之间的转换,它只能执行编译器认为是安全的转换。如果进行不安全的转换,编译器可能会给出警告或错误。另外,对于类层次结构中的指针或引用类型的转换,如果转换不是合法的,即基类指针或引用指向的对象实际上不是派生类的对象,那么
static_cast
将无法进行安全的转换。在这种情况下,可以考虑使用dynamic_cast
进行运行时类型检查和转换。
makefile
mythread:mythread.cc
g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
rm -f mythread
mythread
- 一个线程如果出现了异常,会影响其他线程吗?
- 会的,一个线程异常退出,会导致其他线程也被退出(这种情况称为:线程的健壮性或者鲁棒性较差)
- 为什么会导致其他线程也退出呢?
- 这是因为:进程信号,信号是整体发给进程的,而在一个进程中的所有线程都会退出
- 使用下面的代码来进行演示:
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;
void *start_routine(void *args)
{
string name = static_cast<const char*>(args); // 安全的进行强制类型转化
while (true)
{
cout << "new thread create success, name: " << name << endl;
int *p = nullptr;
*p = 0; // 此处指针异常
}
}
int main()
{
pthread_t id; // 线程id
pthread_create(&id, nullptr, start_routine, (void *)"thread one");
while(true)
{
cout << "new thread create success, name: main thread" << endl;
sleep(1);
}
return 0;
}
Linux线程控制
-
POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
- 要使用这些函数库,要通过引入头文<pthread.h>
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项
-
创建线程
功能:创建一个新的线程
原型 :
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void (*start_routine)(void*), void *arg);
参数 thread:返回线程ID
attr:设置线程的属性,attr为NULL表示使用默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数 返回值:成功返回0;失败返回错误码
错误检查:
传统的一些函数是,成功返回0,失败返回-1,并且对全局变量errno赋值以指示错误。
pthreads函数出错时不会设置全局变量errno(而大部分其他POSIX函数会这样做)。而是将错误代码通过返回值返回
pthreads同样也提供了线程内的errno变量,以支持其它使用errno的代码。对于pthreads函数的错误,建议通过返回值业判定,因为读取返回值要比读取线程内的errno变量的开销更小
创建多个线程
makefile
mythread:mythread.cc
g++ -o $@ $^ -lpthread -std=c++11
.PHONY:clean
clean:
rm -f mythread
mythread.cc
#include <iostream>
#include <cstdlib>
#include <string>
#include <cassert>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;
// 当成结构体使用(都是共有的资源)
class ThreadData
{
public:
int number;
pthread_t tid;
char namebuffer[64];
};
// 1. start_routine, 现在是被几个线程执行呢?
// 目前有10个线程都在执行这个函数, 这个函数现在是重入状态
// 2. 该函数是可重入函数吗?
// 是的,根据可重入函数的定义,这个函数被重复进入,但是并没有出现二义性(也就是没有出现问题)
// 3. 在函数内定义的变量,都叫做局部变量,具有临时性
// 在多线程情况下, 也是同样使用的,这些局部变量独属于它的线程, 也从侧面验证了其实每一个线程都有自己独立的栈结构
void *start_routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args); // 安全的进行强制类型转化
int cnt = 5;
while (cnt)
{
cout << "cnt: " << cnt << " &cnt: " << &cnt << endl;
cnt--;
sleep(1);
}
// 释放我们new的资源(存储)
delete td;
return nullptr;
}
int main()
{
// 1. 创建一批线程,将创建线程的相关信息放入到threads中
vector<ThreadData*> threads;
#define NUM 10
for(int i = 0; i < NUM; i++)
{
ThreadData *td = new ThreadData();
td->number = i+1;
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);
pthread_create(&td->tid, nullptr, start_routine, td);
// 将线程id、线程名、和线程编号(也就是结构体td)存储到vector<ThreadData*> threads中
threads.push_back(td);
}
// 打印创建的新线程的名字,和线程id
for(auto &iter : threads)
{
cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;
}
while (true)
{
cout << "new thread create success, name: main thread" << endl;
sleep(1);
}
return 0;
}
线程终止
- exit() : 是不能够用来终止线程的,因为exit()是用来终止进程的,任何一个执行流调用exit()都会让整个进程退出。
- 线程执行完,返回nullptr,也可以终止线程
- 使用pthread_exit()终止线程
pthread_exit()
功能:pthread_exit - terminate calling thread // 终止要调用的线程
头文件:#include <pthread.h>
// 函数
void pthread_exit(void *retval);
// 参数
void *retval :是新线程退出后的退出结果,如果不关心这个,那么直接设置为nullptr就可以了
mythread.cc
#include <iostream>
#include <cstdlib>
#include <string>
#include <cassert>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;
class ThreadData
{
public:
int number;
pthread_t tid;
char namebuffer[64];
};
void *start_routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args); // 安全的进行强制类型转化
int cnt = 5;
while (cnt)
{
cout << "cnt: " << cnt << " &cnt: " << &cnt << endl;
cnt--;
sleep(1);
// 将其放在这里方便直观的看出线程退出,只是用来观察
// 退出线程的方法一
// return nullptr;
// 退出线程的方法二
// pthread_exit(nullptr);
}
delete td;
pthread_exit(nullptr);
}
int main()
{
vector<ThreadData*> threads;
#define NUM 10
for(int i = 0; i < NUM; i++)
{
ThreadData *td = new ThreadData();
td->number = i+1;
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);
pthread_create(&td->tid, nullptr, start_routine, td);
threads.push_back(td);
}
for(auto &iter : threads)
{
cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;
}
while (true)
{
cout << "new thread create success, name: main thread" << endl;
sleep(1);
}
return 0;
}
- 退出线程的方法一和方法二的打印结果
线程等待
- 线程也是需要被等待的,如果不等待,也会造成类似于僵尸进程的问题(内存泄漏,也就是PCB没有被回收)
- 获取新线程的退出信息
- 回收新线程对应的PCB等内核资源,防止内存泄漏
pthread_join
功能:pthread_join - join with a terminated thread
// 头文件
#include <pthread.h>
// 函数
int pthread_join(pthread_t thread, void **retval);
// 参数
pthread_t thread :线程id
void **retval :输出型参数,用来获取线程结束时,返回的退出结果
// 返回值
success, pthread_join() returns 0; on error, it returns an error number.
mythread.cc
#include <iostream>
#include <cstdlib>
#include <string>
#include <cassert>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;
class ThreadData
{
public:
int number;
pthread_t tid;
char namebuffer[64];
};
void *start_routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args); // 安全的进行强制类型转化
int cnt = 5;
while (cnt)
{
cout << "cnt: " << cnt << " &cnt: " << &cnt << endl;
cnt--;
sleep(1);
}
pthread_exit(nullptr);
}
int main()
{
vector<ThreadData*> threads;
#define NUM 10
for(int i = 0; i < NUM; i++)
{
ThreadData *td = new ThreadData();
td->number = i+1;
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);
pthread_create(&td->tid, nullptr, start_routine, td);
threads.push_back(td);
}
for(auto &iter : threads)
{
cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;
}
// 等待回收新线程
for(auto &iter : threads)
{
int n = pthread_join(iter->tid, nullptr);
assert(n == 0);
cout << "join : " << iter->namebuffer << " success " << endl;
delete iter;
}
while (true)
{
cout << "new thread create success, name: main thread" << endl;
sleep(1);
}
return 0;
}
线程的返回值
mythread.cc
#include <iostream>
#include <cstdlib>
#include <string>
#include <cassert>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;
class ThreadData
{
public:
int number;
pthread_t tid;
char namebuffer[64];
};
class ThreadReturn
{
public:
int exit_code;
int exit_result;
};
void *start_routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args); // 安全的进行强制类型转化
int cnt = 5;
while (cnt)
{
cout << "cnt: " << cnt << " &cnt: " << &cnt << endl;
cnt--;
sleep(1);
}
// 因为返回值的类型为void*,因此我们将其强转为void*类型
// Linux为64位,因此其指针是8byte,而整型是4byte,所以编译时,此处会报warning
// 相当于 void* ret = (void*)td->number
return (void*)td->number;
// 使用pthread_exit也可以返回退出结果
// pthread_exit((void*)321);
// 也可以返回堆空间的地址,或者是对象的地址,
// 但是不能够返回在栈上面创建的结构对象,因为栈帧被销毁,这个对象也就不存在了,所以一定要new,在堆空间上创建这个对象
// ThreadReturn * tr = new ThreadReturn();
// tr->exit_code = 1;
// tr->exit_result = 321;
// return (void*)tr;
}
int main()
{
vector<ThreadData*> threads;
#define NUM 10
for(int i = 0; i < NUM; i++)
{
ThreadData *td = new ThreadData();
td->number = i+1;
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);
pthread_create(&td->tid, nullptr, start_routine, td);
threads.push_back(td);
}
for(auto &iter : threads)
{
cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;
}
for(auto &iter : threads)
{
void* ret = nullptr;
// pthread_join的第二个参数是输出型参数
// void** retp = &ret;
// 解引用,就可以拿到新线程的返回值,也就是线程结束后,返回的退出结果
// *retp,也就是ret(对二级指针解引用,指向其存储的一级指针变量)
// *retp = return (void*)td->number
int n = pthread_join(iter->tid, &ret);
assert(n == 0);
// 此处的ret必须强转为long long类型而不是int类型,因为64位的指针是8字节的
cout << "join : " << iter->namebuffer << " success,number : " << (long long)ret << endl;
delete iter;
}
// 针对堆空间返回值的打印,因为堆空间返回的是结构体对象的地址,因此接收返回值的参数的变量的类型也要被修改,具体如下面这段代码所示
// for(auto &iter : threads)
// {
// ThreadReturn* ret = nullptr;
// int n = pthread_join(iter->tid, (void**)&ret);
// assert(n == 0);
// // 此处的ret必须强转为long long类型而不是int类型,因为64位的指针是8字节的
// cout << "join : " << iter->namebuffer << " success,exit_code: " << ret->exit_result << endl;
// delete iter;
// }
while (true)
{
cout << "new thread create success, name: main thread" << endl;
sleep(1);
}
return 0;
}
- 退出结果,返回过程的理解
- 程序运行的结果
线程取消(终止)
pthread_cancel
功能:pthread_cancel - send a cancellation request to a thread
头文件: #include <pthread.h>
// 函数
int pthread_cancel(pthread_t thread);
pthread.cc
#include <iostream>
#include <cstdlib>
#include <string>
#include <cassert>
#include <vector>
#include <pthread.h>
#include <unistd.h>
using namespace std;
class ThreadData
{
public:
int number;
pthread_t tid;
char namebuffer[64];
};
void *start_routine(void *args)
{
ThreadData *td = static_cast<ThreadData *>(args); // 安全的进行强制类型转化
int cnt = 5;
while (cnt)
{
cout << "cnt: " << cnt << " &cnt: " << &cnt << endl;
cnt--;
sleep(1);
}
return (void*)321;
}
int main()
{
vector<ThreadData*> threads;
#define NUM 10
for(int i = 0; i < NUM; i++)
{
ThreadData *td = new ThreadData();
td->number = i+1;
snprintf(td->namebuffer, sizeof(td->namebuffer), "%s:%d", "thread", i+1);
pthread_create(&td->tid, nullptr, start_routine, td);
threads.push_back(td);
}
for(auto &iter : threads)
{
cout << "create thread: " << iter->namebuffer << " : " << iter->tid << " suceesss" << endl;
}
// 线程是可以被cancel取消的
// 注意:线程要被取消,前提是这个线程已经跑起来了
// 线程如果是被取消的,退出码:-1
sleep(5);
for(int i = 0; i < threads.size()/2; i++)
{
pthread_cancel(threads[i]->tid);
cout << "pthread_cancel : " << threads[i]->namebuffer << " success" << endl;
}
for(auto &iter : threads)
{
void* ret = nullptr;
int n = pthread_join(iter->tid, (void**)&ret);
assert(n == 0);
cout << "join : " << iter->namebuffer << " success,exit_code: " << (long long)ret << endl;
delete iter;
}
while (true)
{
cout << "new thread create success, name: main thread" << endl;
sleep(1);
}
return 0;
}
c++11接口(线程)
#include <iostream>
#include <unistd.h>
#include <thread>
void thread_run()
{
while (true)
{
std::cout << "我是新线程..." << std::endl;
sleep(1);
}
}
int main()
{
// 任何语言,在linux中如果要实现多线程,必定要是用pthread库
// 如何看待C++11中的多线程呢?
// C++11 的多线程,在Linux环境中,本质是对pthread库的封装!
std::thread t1(thread_run);
while (true)
{
std::cout << "我是主线程..." << std::endl;
sleep(1);
}
t1.join();
return 0;
}
分离线程
默认情况下,新创建的线程是joinable的,线程退出后,需要对其进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
pthread_self()
功能:pthread_self - obtain ID of the calling thread // 获取调用线程的id
// 头文件
#include <pthread.h>
// 函数
pthread_t pthread_self(void);
// 返回值
This function always succeeds, returning the calling thread's ID.
pthread_detach()
功能:pthread_detach - detach a thread // 分离一个线程
头文件: #include <pthread.h>
// 函数
int pthread_detach(pthread_t thread);
mythread.cc
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
std::string changId(const pthread_t &thread_id)
{
char tid[128];
snprintf(tid, sizeof(tid), "0x%x", thread_id);
return tid;
}
void* start_routine(void* args)
{
std::string threadname = static_cast<const char*>(args);
int cnt = 5;
while(cnt--)
{
// 当新线程调用pthread_self(),则得到的返回值就是新线程的线程id
std::cout << threadname << " running......,其线程id为 "<< changId(pthread_self()) << std::endl;
sleep(1);
}
}
int main()
{
// 输出型参数,是创建的新线程的线程id
pthread_t tid;
pthread_create(&tid, nullptr, start_routine, (void*)"thread 1");
// 可以用主线程分离新线程,也可以用新线程分离自己,效果是一样的
// 此处,为了在主线程运行到pthread_join()前,就让主线程检测到新线程就被分离,所以在此处用主线程分离新线程
// 分离创建的新线程,分离新线程之后,主线程就不需要再等待回收新线程的资源了
// 新线程运行结束之后,OS会自动释放新线程的资源
pthread_detach(tid);
// 主线程调用ptherad_self(),则返回值为主线程的id
std::string main_id = changId(pthread_self());
// tid为创建的新线程的id
std::cout << "main thread running......,其创建的新线程id为 : " << changId(tid) << "; mian thread id : " << main_id << std::endl;
// 等待回收创建的新线程
// 一个线程默认是joinable的,如果设置了分离状态,就不能够进行等待了,否则就会等待失败
// pthread_join()返回错误码
int n = pthread_join(tid, nullptr);
std::cout << "result" << n << " : " << strerror(n) << std::endl;
while(1)
{
std::cout << "main thread running......" << std::endl;
sleep(1);
}
return 0;
}
解析线程id
线程局部存储
#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
// g_val为线程局部存储
// 添加__thread, 可以将一个内置类型(如:int,char)设置为线程局部存储
// 在线程库中,线程结构体中存在线程局部存储,将这个变量设置为局部存储,也就是将这个变量给每一个线程都设置一份到对应线程的线程局部存储中
// 虽然这个变量依旧是全部变量,但是每个线程之间是不会相互影响的
__thread int g_val = 100;
// g_val为全部变量
// int g_val = 100;
std::string changId(const pthread_t &thread_id)
{
char tid[128];
snprintf(tid, sizeof(tid), "0x%x", thread_id);
return tid;
}
void* start_routine(void* args)
{
std::string threadname = static_cast<const char*>(args);
while(true)
{
// 当新线程调用pthread_self(),则得到的返回值就是新线程的线程id
std::cout << threadname << " running......,其线程id为 "<< changId(pthread_self()) << "; g_val " << (int)g_val << " &g_val " << &g_val << std::endl;
sleep(1);
g_val++;
}
}
int main()
{
pthread_t tid;
pthread_create(&tid, nullptr, start_routine, (void*)"thread 1");
while(true)
{
std::cout << "main thread running.....; g_val :" << (int)g_val << " &g_val :" << &g_val << std::endl;
sleep(1);
}
return 0;
}
- 当int g_val为全局变量时
- 当 g_val为线程局部存储中的变量时
线程的封装
Thread.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cassert>
#include <functional>
#include <pthread.h>
// 声明这个类,因为在class Context中使用了class Thread,但是此时class Thread还没有被创建
class Thread;
//上下文,当成一个大号的结构体
class Context
{
public:
Thread *this_; // Thread类的this指针
void *args_; // 新线程将要调用函数方法的参数
public:
// 构造函数
Context()
:this_(nullptr)
, args_(nullptr)
{}
// 析构函数
~Context()
{}
};
class Thread
{
public:
// 定义函数的类型为func_t,函数的参数和返回值的类型都为void*
typedef std::function<void*(void*)> func_t;
// 存储线程名字数组的容量
const int num = 1024;
public:
// 构造函数(构造一个线程)
// 参数为,线程需要调用的方法func_t func, 这个方法需要传递的参数void *args = nullptr,
// 存储线程名字数组的容量
Thread(func_t func, void *args = nullptr, int number = 0)
: func_(func)
, args_(args)
{
char buffer[num];
snprintf(buffer, sizeof buffer, "thread-%d", number);
name_ = buffer;
// 开始创建线程
// 新线程所要执行的方法
// 在类内创建线程,想让线程执行对应的方法,需要将方法设置成为static
// 为什么需要将其设置为static呢?
// 这是因为此时的start_routine是类内成员,除了一个显示的参数void *args之外,还有一个参数是this指针(也就是实例对象的地址)
// 因此如果在类内调用这个函数,是需要通过将类的实例指针作为参数传递给线程创建函数。
// 静态成员函数在类的所有对象实例之间共享,它们没有this指针,因此可以直接通过类名调用。
// 这使得静态成员函数可以在没有类的实例的情况下执行。
// 解决方案
// 通过定义结构体class Context,将this指针,还有参数args_都定义在里面start_routine的两个参数
// 这样传递参数时,只需要通过一个结构体对象,就可以调用
Context *ctx = new Context();
ctx->this_ = this;
ctx->args_ = args_;
// 创建线程的系统调用函数为pthread_create()
// 通过传递对象ctx可以同时将this指针和start_routine线程调用方法的参数进行传递
int n = pthread_create(&tid_, nullptr, start_routine, ctx);
assert( n==0 );
// 编译debug的方式发布的时候存在,release方式发布,assert就不存在了,n就是一个定义了,但是没有被使用的变量
// 在有些编译器下会有warning
(void)n;
}
// 静态方法不能够调用成员变量或者成员方法,
/*
如果需要在静态方法中访问成员变量或成员方法,可以通过以下两种方式实现:
1.将成员变量或成员方法声明为静态:将需要在静态方法中访问的成员变量或成员方法声明为静态,
这样它们就可以在静态方法中直接访问,因为它们属于类而不是类的实例。
2.创建类的实例并调用成员变量或成员方法:在静态方法中创建类的实例,
并使用该实例来访问成员变量或成员方法。通过实例化类,就可以获得对非静态成员的访问权限。
需要注意的是,静态方法中只能直接访问静态成员,而不能直接访问非静态成员。*/
static void *start_routine(void *args)
{
// 将args安全的强转为Context *类型
Context *ctx = static_cast<Context *>(args);
// 通过实例来访问成员变量或成员方法。通过实例化类,就可以获得对非静态成员的访问权限。
void* ret = ctx->this_->run(ctx->args_);
// 释放ctx的空间
delete ctx;
return ret;
}
void join()
{
int n = pthread_join(tid_, nullptr);
assert(n == 0);
(void)n;
}
void *run(void *args)
{
// 运行线程将要执行的方法func_t func
return func_(args);
}
~Thread()
{
//do nothing
}
private:
std::string name_; // 线程名
func_t func_; // 新线程所要调用的方法
void *args_; // 新线程所要调用的方法的函数的参数,也就是func_的参数
pthread_t tid_; // 新线程的线程id
};
mythread.cc
#include "Thread.hpp"
#include <memory>
void* thread_run(void* args)
{
std::string work_type = static_cast<const char*>(args);
while(true)
{
std::cout << "我是一个新线程,我正在做: " << work_type << std::endl;
sleep(1);
}
return nullptr;
}
int main()
{
std::unique_ptr<Thread> thread(new Thread1(thread_run, (void*)"hellothread", 1));
std::unique_ptr<Thread> thread(new Thread2(thread_run, (void*)"countthread", 2));
std::unique_ptr<Thread> thread(new Thread3(thread_run, (void*)"logthread", 3));
thread1->join();
thread2->join();
thread3->join();
return 0;
}
5. Linux线程互斥
购买火车票
usleep()
功能:usleep - suspend execution for microsecond intervals // 以微秒为间隔暂停执行
// 头文件
#include <unistd.h>
// 函数
int usleep(useconds_t usec);
mythread.cc
// 使用我们线程封装的Thread.hpp
#include "Thread.hpp"
// 全局变量,多个线程的共享资源
// tickets代表火车票的剩余数量
int tickets = 10000;
void* getTicket(void* args)
{
std::string username = static_cast<const char*>(args);
while(true)
{
if(tickets > 0)
{
// 只有当剩余票数大于0,抢票才是有意义的
std::cout << username << "正在进行抢票" << tickets-- << std::endl;
// 1秒 = 1000毫秒 = 1000 000 微秒 = 10^9纳秒
// usleep()是以微秒为单位的
// 用这段时间来模拟真实的抢票要花费的时间
usleep(1000);
}
else
{
// 当没有票时,那么就直接跳出循环
break;
}
}
return nullptr;
}
int main()
{
std::unique_ptr<Thread> thread1(new Thread(getTicket, (void*)"user1", 1));
std::unique_ptr<Thread> thread2(new Thread(getTicket, (void*)"user2", 2));
std::unique_ptr<Thread> thread3(new Thread(getTicket, (void*)"user3", 3));
std::unique_ptr<Thread> thread4(new Thread(getTicket, (void*)"user4", 4));
thread1->join();
thread2->join();
thread3->join();
thread4->join();
return 0;
}
- 运行结果
-
此时,我们想要模拟一种抢票的极端环境(就是让多个线程不是串联的执行抢票,而是并联的执行),那么就需要尽可能的让多个线程交叉执行(也就是并联执行)
-
所谓的串联执行就是一个线程执行完之后,另一个线程再继续执行,而并联则是几个线程同时进行执行
-
多个线程交叉执行的本质:就是让调度器尽可能的频繁发生线程调度与切换
-
线程一般在时间片到了、来了更高优先级的线程、线程等待的时候发生线程切换。
-
线程是在什么时候检测上面的问题呢? ==》从内核态返回用户态的时候,线程要对调度状态进行检测,如果可以,就直接发生线程切换
-
#include "Thread.hpp"
int tickets = 10000;
void* getTicket(void* args)
{
std::string username = static_cast<const char*>(args);
while(true)
{
// 串联执行的话,某一个线程将票抢完之后,下一个线程会检测到tickets已经不大于0了,因此不会进入
// 4个线程是不可以同时执行判断ticket > 0的,假设只有一个cpu,那么在任何一个时刻,只允许有一个线程来执行这个判断
// 判断的本质逻辑: 1.读取内存数据到cpu的寄存器当中 2.进行判断
if(tickets > 0)
{
// 当线程在进行抢票前,先让这个线程进行休眠,那么这个线程就会被CPU切换走
// 这样当多个线程都执行到这里都被休眠的话,当休眠过后,哪个线程先被唤醒是由调度器决定的
// 是一个不确定的结果,这样就可以保证多个线程交叉执行了(也就是并联执行)
usleep(1000);
std::cout << username << "正在进行抢票" << tickets-- << std::endl;
}
else
{
break;
}
}
return nullptr;
}
int main()
{
std::unique_ptr<Thread> thread1(new Thread(getTicket, (void*)"user1", 1));
std::unique_ptr<Thread> thread2(new Thread(getTicket, (void*)"user2", 2));
std::unique_ptr<Thread> thread3(new Thread(getTicket, (void*)"user3", 3));
std::unique_ptr<Thread> thread4(new Thread(getTicket, (void*)"user4", 4));
thread1->join();
thread2->join();
thread3->join();
thread4->join();
return 0;
}
- 运行结果如下:
-
原因如下图所示:
- 图1
- 图2
- 图1
-
我们定义的全局变量,在没有保护的时候,往往是不安全的,像上面多个线程在交替执行造成的数据安全问题,发生了数据不一致问题。
- 提出解决方案:加锁
-
补充知识点:
- 1.多个执行流进行安全访问的共享资源,我们将其称为临界资源
- 2.我们把多个执行流中,访问临界资源的代码称为临界区(临界区往往是线程代码的很小的一部分)
- 3.想让多个线程串行访问共享资源,是需要多个线程之间存在互斥的(也就是只有当一个执行流访问完临界资源之后,另一个线程才可以对其进行访问)
- 4.对一个资源进行访问的时候,要么不做,要么做完,我们将这种行为称为原子性
- 5.如上述我们所说进行tickets–,对应的汇编语言是三条,但是在执行完两条汇编语言之后,线程就被切换走了,像这种没有执行完对应汇编语言的行为,这就不是原子性的。基于这些,我们对原子性下一个定义: 如果只用一条汇编语言就可以完成,就称为原子性,反之就不是原子的(当前理解,方便表述)。
加锁
pthread_mutex_init()
// 功能
pthread_mutex_destroy, pthread_mutex_init - destroy and initialize a mutex // 销毁并初始化互斥对象
// mutex n.互斥
// 头文件
#include <pthread.h>
// 如果这把锁是局部的,那么必须使用init来初始化,用destory来销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// pthread_mutex_t *restrict mutex 是需要初始化的锁
// const pthread_mutexattr_t *restrict attr 是属性,目前设置为nullptr就可以
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
// 如果这把锁是全局的或者是静态的,那么我们只需要用如下的方式来对锁初始化,而不需要对其进行销毁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock()
// 功能
pthread_mutex_lock, pthread_mutex_trylock, pthread_mutex_unlock - lock and unlock a mutex // ,锁定和解锁互斥锁
// 头文件
#include <pthread.h>
int pthread_mutex_lock(pthread_mutex_t *mutex); // 加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex); // 尝试申请加锁,如果加锁成功,那么就会拥有锁,如果不成功,则会出错返回
int pthread_mutex_unlock(pthread_mutex_t *mutex); // 解锁
mythread.cc
#include "Thread.hpp"
// 设置一个全局的锁 或者静态的锁,只需要用如下的方式将其进行初始化,且不需要对其进行销毁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int tickets = 10000;
void* getTicket(void* args)
{
std::string username = static_cast<const char*>(args);
while(true)
{
// 对临界区的资源进行加锁
pthread_mutex_lock(&lock);
if(tickets > 0)
{
usleep(1000);
std::cout << username << "正在进行抢票" << tickets-- << std::endl;
// 对临界区资源进行解锁
pthread_mutex_unlock(&lock);
}
else
{
// 对临界区资源进行解锁,必须在break之前进行解锁,如果执行了break,跳出了循环,那么将无法进行解锁
pthread_mutex_unlock(&lock);
break;
}
}
return nullptr;
}
int main()
{
std::unique_ptr<Thread> thread1(new Thread(getTicket, (void*)"user1", 1));
std::unique_ptr<Thread> thread2(new Thread(getTicket, (void*)"user2", 2));
std::unique_ptr<Thread> thread3(new Thread(getTicket, (void*)"user3", 3));
std::unique_ptr<Thread> thread4(new Thread(getTicket, (void*)"user4", 4));
thread1->join();
thread2->join();
thread3->join();
thread4->join();
return 0;
}
- 运行结果
- 设置一个局部的锁
#include <iostream>
#include <vector>
#include <string>
#include <cstring>
#include <pthread.h>
#include <memory>
#include <unistd.h>
int tickets = 10000;
class ThreadData
{
public:
// 构造函数
ThreadData(const std::string &threadname, pthread_mutex_t *mutex)
:threadname_(threadname)
,mutex_p_(mutex)
{}
// 析构函数
~ThreadData()
{}
public:
std::string threadname_; // 线程名字
pthread_mutex_t *mutex_p_; // 锁
};
void* getTicket(void* args)
{
ThreadData *td = static_cast<ThreadData *>(args);
while(true)
{
// 加锁和解锁的过程,多个线程是串行执行的,因此程序的运行速度就变慢了
// 锁只规定互斥访问,没有规定让那个线程优先执行
// 因此,那个线程先申请到锁,这是多个执行流进行竞争的结果
// 对临界区的资源进行加锁
pthread_mutex_lock(td->mutex_p_);
if(tickets > 0)
{
usleep(1000);
std::cout << td->threadname_ << "正在进行抢票" << tickets << std::endl;
tickets--;
// 对临界区资源进行解锁
pthread_mutex_unlock(td->mutex_p_);
}
else
{
// 对临界区资源进行解锁,必须在break之前进行解锁,如果执行了break,跳出了循环,那么将无法进行解锁
pthread_mutex_unlock(td->mutex_p_);
break;
}
// 抢完票之后,还要形成一个订单发送给用户,因此停顿一下来模拟这个过程
// 当我们这个停顿一下,对应的线程就会被切换走,就不会出现一个线程(执行流)一直在抢票的情况了
usleep(1000);
}
return nullptr;
}
int main()
{
#define NUM 4
pthread_mutex_t lock;
// 对于局部的锁,我们需要用init对其进行初始化,解锁之后我们要使用destory对其进行销毁
pthread_mutex_init(&lock, nullptr);
// 将创建的线程id放入tids中; pthread_t是unsigned long int(无符号的长整型)
// 初始化NUM个pthread_t的空间
std::vector<pthread_t> tids(NUM);
for(int i = 0; i < NUM; i++)
{
char buffer[64];
// 将线程名字格式化到buffer中
snprintf(buffer, sizeof(buffer), "thread %d", i);
// 通过td这个对象,我们就可以同时传递线程名字,和线程所需要的锁两个参数
ThreadData *td = new ThreadData(buffer, &lock);
// tids[i]是一个输出型参数,是线程id
pthread_create(&tids[i], nullptr, getTicket, td);
}
// 等待回收线程资源
for(const auto &tid : tids)
{
pthread_join(tid, nullptr);
}
// 局部的锁被解锁之后,还需要被销毁
pthread_mutex_destroy(&lock);
return 0;
}
- 运行结果如下:
深层理解锁(内涵锁的封装)
- 如何看待锁?
a.锁,本身就是一个共享资源,全局的变量是要被保护的,锁是用来保护全局的资源的,但是锁本身也是全局资源,那么锁的安全是谁来保护呢?
b.
pthread_mutex_lock,pthread_mutex_unlock
:加锁和解锁的过程必须是安全的。因为加锁和解锁的过程是原子的,因此其本质就是安全的(后续会说到)c. 如果申请锁申请成功,那么就继续向后执行,如果锁申请暂时没有成功,那么执行流将会如何?(执行流会被阻塞)
#include <iostream> #include <vector> #include <string> #include <cstring> #include <pthread.h> #include <memory> #include <unistd.h> // 设置一个全局的锁 或者静态的锁,只需要用如下的方式将其进行初始化,且不需要对其进行销毁 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; int tickets = 10000; void* getTicket(void* args) { std::string username = static_cast<const char*>(args); while(true) { // 对临界区的资源进行加锁 pthread_mutex_lock(&lock); // 如果我们在申请一把锁之后,我们再申请一把锁那么会怎么样呢? // 我们会发现程序运行阻塞,原因我们后续再说 pthread_mutex_lock(&lock); if(tickets > 0) { usleep(1000); std::cout << username << "正在进行抢票" << tickets-- << std::endl; // 对临界区资源进行解锁 pthread_mutex_unlock(&lock); } else { // 对临界区资源进行解锁,必须在break之前进行解锁,如果执行了break,跳出了循环,那么将无法进行解锁 pthread_mutex_unlock(&lock); break; } // 抢完票之后,还要形成一个订单发送给用户,因此停顿一下来模拟这个过程 // 当我们这个停顿一下,对应的线程就会被切换走,就不会出现一个线程(执行流)一直在抢票的情况了 usleep(1000); } return nullptr; } int main() { pthread_t t1, t2, t3, t4; pthread_create(&t1, nullptr, getTicket, (void *)"thread 1"); pthread_create(&t2, nullptr, getTicket, (void *)"thread 2"); pthread_create(&t3, nullptr, getTicket, (void *)"thread 3"); pthread_create(&t4, nullptr, getTicket, (void *)"thread 4"); pthread_join(t1, nullptr); pthread_join(t2, nullptr); pthread_join(t3, nullptr); pthread_join(t4, nullptr); return 0; }
运行结果如下:
d. 哪个线程持有锁,哪个线程才可以进入临界区
如何理解加锁和解锁的本质?
- 加锁的过程是原子的
- 经过上面的例子,我们已经意识到单纯的
i++
或者++i
都不是原子的,有可能会有数据一致性问题- 为了实现互斥锁操作,大多数体系结构都提供了
swap
或exchange
指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性。如果我们想简单的使用锁,该如何进行封装设计?
makefile
mythread:mythread.cc g++ -o $@ $^ -lpthread -std=c++11 .PHONY:clean clean: rm -f mythread
Mutex.hpp
#pragma once
#include <iostream>
#include <pthread.h>
class Mutex
{
public:
// 构造函数
Mutex(pthread_mutex_t *lock_p = nullptr)
:lock_p_(lock_p)
{}
// 加锁
void lock()
{
// 如果lock_p_不是空指针,这说明已经传递进来了一把锁了
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_;
};
mythread.cc
#include "Mutex.hpp"
#include <unistd.h>
// 设置一个全局的锁 或者静态的锁,只需要用如下的方式将其进行初始化,且不需要对其进行销毁
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int tickets = 10000;
void* getTicket(void* args)
{
std::string username = static_cast<const char*>(args);
while(true)
{
// RAII风格的加锁 : 资源获取即初始化(Resource Acquisition Is Initialization)
// lockgaurd是定义在while循环中的一个对象
// 在创建这个对象时,会调用这个这个对象的构造函数对临界区进行加锁
// 当运行到lockguard的生命周期结束,则会调用析构函数来解锁
// lockguard的生命周期就是while的一次循环,while每一次循环都会创建一个LockGuard对象
LockGuard lockguard(&lock);
if(tickets > 0)
{
usleep(1000);
std::cout << username << "正在进行抢票" << tickets-- << std::endl;
}
else
{
break;
}
// 抢完票之后,还要形成一个订单发送给用户,因此停顿一下来模拟这个过程
usleep(1000);
}
return nullptr;
}
int main()
{
pthread_t t1, t2, t3, t4;
pthread_create(&t1, nullptr, getTicket, (void *)"thread 1");
pthread_create(&t2, nullptr, getTicket, (void *)"thread 2");
pthread_create(&t3, nullptr, getTicket, (void *)"thread 3");
pthread_create(&t4, nullptr, getTicket, (void *)"thread 4");
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
return 0;
}
运行结果如下:
可重入VS线程安全
概念
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
常见的线程不安全的情况
- 不保护共享变量的函数
- 函数状态随着被调用,状态发生变化的函数
- 返回指向静态变量指针的函数
- 调用线程不安全函数的函数
常见的线程安全的情况
- 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
- 类或者接口对于线程来说都是原子操作
- 多个线程之间的切换不会导致该接口的执行结果存在二义性
常见不可重入的情况
- 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
- 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
- 可重入函数体内使用了静态的数据结构
常见可重入的情况
- 不使用全局变量或静态变量
- 不使用用malloc或者new开辟出的空间
- 不调用不可重入函数
- 不返回静态或全局数据,所有数据都有函数的调用者提供
- 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
可重入与线程安全联系
- 函数是可重入的,那就是线程安全的
- 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
- 如果一个函数中有全局变量,那么这个函数既不是线程安全的也不是可重入的。
可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数的锁还未释放则会产生死锁,因此是不可重入的。
6. 常见锁概念
死锁
死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
举一个简单的例子:你有一块钱,你朋友也有一块钱,你们都想要对方的一块钱且你们都不想给对方自己的一块钱,然而你们相互不停的询问对方要这一块钱,则双方都处于一种永久的等待状态。
死锁四个必要条件
- 互斥条件:一个资源每次只能被一个执行流使用
- 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
- 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
- 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
避免死锁
- 破坏死锁的四个必要条件
- 加锁顺序一致
- 避免锁未释放的场景
- 资源一次性分配
ullptr, getTicket, (void *)“thread 1”);
pthread_create(&t2, nullptr, getTicket, (void *)“thread 2”);
pthread_create(&t3, nullptr, getTicket, (void *)“thread 3”);
pthread_create(&t4, nullptr, getTicket, (void *)“thread 4”);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_join(t4, nullptr);
return 0;
}
运行结果如下:
[外链图片转存中...(img-z56XRtLR-1743561038766)]
# 可重入VS线程安全
## 概念
> - 线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
>
> - 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
## 常见的线程不安全的情况
> - 不保护共享变量的函数
> - 函数状态随着被调用,状态发生变化的函数
> - 返回指向静态变量指针的函数
> - 调用线程不安全函数的函数
## **常见的线程安全的情况**
> - 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
> - 类或者接口对于线程来说都是原子操作
> - 多个线程之间的切换不会导致该接口的执行结果存在二义性
## **常见不可重入的情况**
> - 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
> - 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
> - 可重入函数体内使用了静态的数据结构
## **常见可重入的情况**
> - 不使用全局变量或静态变量
> - 不使用用malloc或者new开辟出的空间
> - 不调用不可重入函数
> - 不返回静态或全局数据,所有数据都有函数的调用者提供
> - 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
## **可重入与线程安全联系**
> - 函数是可重入的,那就是线程安全的
> - 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
> - 如果一个函数中有全局变量,那么这个函数既不是线程安全的也不是可重入的。
## **可重入与线程安全区别**
> - 可重入函数是线程安全函数的一种
>
> - 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
>
> - 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数的锁还未释放则会产生死锁,因此是不可重入的。
# **6.** **常见锁概念**
## **死锁**
> - 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。
>
> - 举一个简单的例子:你有一块钱,你朋友也有一块钱,你们都想要对方的一块钱且你们都不想给对方自己的一块钱,然而你们相互不停的询问对方要这一块钱,则双方都处于一种永久的等待状态。
## 死锁四个必要条件
> - 互斥条件:一个资源每次只能被一个执行流使用
> - 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
> - 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
> - 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
## 避免死锁
> - 破坏死锁的四个必要条件
> - 加锁顺序一致
> - 避免锁未释放的场景
> - 资源一次性分配
[外链图片转存中...(img-llIEaloi-1743561038767)]