Linux中的线程
目录
线程的概念
进程与线程的关系
线程创建
线程终止
线程等待
线程分离
原生线程库
线程局部存储
自己实现线程封装
线程的优缺点
多线程共享与独占资源
线程互斥
互斥锁
自己实现锁的封装
加锁实现互斥的原理
死锁
线程同步
线程的概念
回顾进程相关概念
● 进程 = 内核数据结构(pcb等) + 内存中的代码/数据
● 进程创建成本较高,需要创建pcb, 进程地址空间,页表,构建页表映射关系,将磁盘的代码和数据加载到内存中等一系列工作
● 一个进程访问的大部分资源都在物理内存中,需要通过进程地址空间+页表获取到,因此可以认为进程地址空间是进程的"资源窗口",因此进程是承担系统资源分配的基本实体
● 创建进程目的是为了让cpu去调度执行进程中的代码,访问相应的数据,完成任务,因此,之前的认知是: 一个进程本质就是一个执行流
线程的概念
● 在一个进程内只创建若干pcb,这些pcb指向同一个进程地址空间,通过同一个页表,映射到同一个内存,看到的是同一份资源
● 目前一个进程内有多个pcb了,本质就是有多个执行流了,多个执行流的地位是对等的,cpu调度时选择任意一个执行流调度即可,每个执行流本质就是一个线程,所以cpu调度的基本单位是线程
● 每个线程都有自己的pcb, 不同的pcb中保存同一个虚拟地址空间的不同起始地址,进而通过页表映射到不同的物理内存区域,相当于多线程瓜分了进程地址空间,从而并发执行同一个进程内的不同代码,共同完成一项任务
● 多个线程由于共用同一个进程地址空间,通过同一个页表映射,看到的是同一份资源,所以资源共享在线程之间显得非常容易,比如全局变量、环境变量、命令行参数等
● 线程是在进程内部执行的一种执行流
● 线程是更加轻量级的进程/线程是比进程更加轻量化的执行流
a.创建线程更加简单了,因为创建进程时该进程用到的资源都申请好了,一系列工作都已经做好了,创建线程只是在分配资源!!!
b.创建线程更加简单意味着释放线程也更加容易了!
c.线程调度也更加简单
c.1 因为不同的线程看到的是同一个地址空间,访问的是同一个资源,因此线程间切换时只需要把一个pcb切换成另一个pcb, 把保存线程临时数据的少量寄存器切换,而页表和地址空间不用切换!
c.2 cpu内部集成了高速缓存cache,线程间切换不需要切换cache, 因为cache中保存的是整个进程中高频访问的数据,但是进程间切换需要切换cache, 因为cache中的大部分数据都失效了!!! 这是线程创建更加简单的最主要的原因
● 创建线程时,线程会瓜分进程总体的时间片,因为时间片也是资源!
● 站在cpu角度,cpu不需要区分调度的是线程还是进程,只需要找到pcb,找到进程地址空间,通过页表映射执行代码即可
● Linux中并不存在真正的线程,只有"轻量级进程"的概念
一个进程内可能存在多个线程,要不要把所有的线程管理起来呢?? 要管理! 如何管理? 先描述,再组织! --- 描述结构体叫做 tcb, 而线程也要有自己的各种队列,线程id, 状态,调度算法等,这都是 tcb中的属性字段,最后把所有tcb用链表链接起来!!!
事实上,windows就是这样实现的,而Linux系统中,并没有单独为线程设计tcb,因为线程的大部分属性特征进程也是有的,线程和进程都是执行流, 不必为线程单独设计,反倒会增加程序员的负担,因此Linux中用pcb可以充当tcb, 所有代码在线程级别上复用即可, 一整套调度算法也可以直接复用!
进程与线程的关系
线程创建
● pthread_create 接口
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
功能: 创建一个新线程
参数:
thread: 输出型参数,获取新线程id
attr: 创建线程时设置的线程属性,直接设为nullptr即可
start_routine:新线程执行的函数
arg: 新线程执行函数的参数
返回值: 创建成功,返回0,创建失败,返回错误码
● 创建线程代码示例
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;//新线程
void* ThreadRoutine(void* arg)
{const char* threadName = (const char*)arg;while(true){cout << "I am a new thread "<< threadName << endl;sleep(1);}
}int main()
{pthread_t tid;//主线程pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread 1");while(true){cout << "I am main thread" << endl;sleep(1);}return 0;
}
● 尽管有主线程和新线程两个线程,但始终只有1个进程,因此打印出的进程pid是一样的
#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;void* ThreadRoutine(void* arg)
{const char* threadName = (const char*)arg;while(true){cout << "I am a new thread" << ", pid: " << getpid() << ", " << threadName << endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, (void*)"thread 1");while(true){cout << "I am main thread" << ", pid: " << getpid() << endl;sleep(1);}return 0;
}
● ps -aL 指令查看系统内的线程,cpu调度线程依据的是LWP(light weight process), PID和LWP一样,就是主线程,否则是新线程
● 线程之间看到同一份资源是非常容易的,比如定义一个全局变量,线程就都能看到了!
#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
using namespace std;int gcnt = 100;void *ThreadRoutine(void *arg)
{while (true){cout << "I am a new thread, gcnt: " << gcnt << ", &gcnt : " << &gcnt << endl;gcnt--;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, nullptr);while (true){cout << "I am main thread, gcnt: " << gcnt << ", &gcnt : " << &gcnt << endl;sleep(1);}return 0;
}
● 创建多线程
#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <functional>
#include <vector>
using namespace std;using func_t = function<void()>;class ThreadData
{
public:ThreadData(const string &name, const uint64_t &ctime, func_t f): threadname(name), createtime(ctime), func(f){}public:string threadname;uint64_t createtime;func_t func;
};void Print()
{cout << "我是线程执行的大任务的一部分" << endl;
}// 新线程
void *ThreadRoutine(void *args)
{ThreadData *td = static_cast<ThreadData *>(args); // 安全强转while (true){cout << "new thread" << ", thread name :" << td->threadname<< ", create time : " << td->createtime << endl;td->func();sleep(1);}
}int main()
{for (int i = 0; i < 3; i++) {char threadname[64]; snprintf(threadname, sizeof(threadname), "%s-%d", "thread", i);ThreadData *td = new ThreadData(threadname, (uint64_t)time(nullptr), Print);pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, td);sleep(1);}while (true){cout << "main thread" << endl;sleep(1);}return 0;
}
● pthread_create 接口的最后一个参数类型是void*,可以接收任意数据类型的地址,因此除了给线程执行方法传递常规的数据类型,还可以传递我们自己封装的类对象
● 类对象中可以封装自定义函数,传递给线程执行方法,在线程内部进行回调
线程终止
● pthread_self()接口可以获取调用该接口的线程id,本质是一个地址
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
using namespace std;//十进制数转十六进制数
string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%x", tid);return id;
}void* threadRoutine(void* args)
{string name = static_cast<const char*>(args);usleep(1000);while(true){cout << "new thread is running, thread name: " << name << ", my thread id: " << ToHex(pthread_self()) << endl;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread-1");while(true){cout << "I am main thread, my thread id :" << ToHex(pthread_self()) << endl;sleep(1);}return 0;
}
● 线程终止有很多方法,比如在线程中直接return(终止线程)/exit(本质是终止整个进程),也可以调用pthread_exit()接口终止线程
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
using namespace std;string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%x", tid);return id;
}void* threadRoutine(void* args)
{string name = static_cast<const char*>(args);usleep(1000);int cnt = 5;while(cnt--){cout << "new thread is running, thread name: " << name << ", my thread id: " << ToHex(pthread_self()) << endl;sleep(1);}pthread_exit(nullptr); //终止调用该接口的线程
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread-1");while(true){cout << "I am main thread, my thread id :" << ToHex(pthread_self()) << endl;sleep(1);}return 0;
}
● 主线程中调用pthread_cancle() 可以取消指定的线程
int pthread_cancel(pthread_t thread);
参数: 要取消的线程id
返回值: 成功,返回0; 失败,返回错误码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <functional>
using namespace std;void* threadRoutine(void* args)
{int cnt = 10;while(cnt--){cout << "thread is running..." << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);sleep(3);pthread_cancel(tid); //取消tid线程cout << "我是主线程,取消了新线程" << endl;return 0;
}
● 一个线程异常,整个进程都会终止
#include <iostream>
#include <cstring>
#include <pthread.h>
#include <unistd.h>
#include <time.h>
#include <functional>
#include <vector>
using namespace std;using func_t = function<void()>;class ThreadData
{
public:ThreadData(const string &name, const uint64_t &ctime, func_t f): threadname(name), createtime(ctime), func(f){}public:string threadname;uint64_t createtime;func_t func;
};void Print()
{cout << "我是线程执行的大任务的一部分" << endl;
}// 新线程
void *ThreadRoutine(void *args)
{int a = 10;ThreadData *td = static_cast<ThreadData *>(args); // 安全强转while (true){cout << "new thread" << ", thread name :" << td->threadname<< ", create time : " << td->createtime << endl;td->func();//异常终止if(td->threadname == "thread-2"){cout << td->threadname << "触发了异常" << endl;a /= 0;}sleep(1);}
}int main()
{for (int i = 0; i < 3; i++){char threadname[64];snprintf(threadname, sizeof(threadname), "%s-%d", "thread", i);ThreadData *td = new ThreadData(threadname, (uint64_t)time(nullptr), Print);pthread_t tid;pthread_create(&tid, nullptr, ThreadRoutine, td);sleep(1);}while (true){cout << "main thread" << endl;sleep(1);}return 0;
}
线程等待
● 线程退出但没有被等待,也会出现和进程类似的僵尸问题
● 新线程退出时需要让主线程等待,从而获取新线程的退出信息
● 当一个新线程出异常了,其他线程也会受到影响,整个进程都终止了,主线程再等待新线程也就没有了意义
● pthread_join 线程等待代码演示
int pthread_join(pthread_t thread, void **retval);
参数:
thread: 被等待的线程id
retval: 输出型参数,根据threadRoutine的返回值可以获取子进程的退出信息,如果不关心新线程的退出信息,该参数直接设置为nullptr即可
返回值: 成功返回0,失败返回错误码
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <functional>
using namespace std;void* threadRoutine(void* args)
{int cnt = 5;while(cnt--){cout << "thread is running..." << endl;sleep(1);}char* ret = "新线程正常退出啦!!!";return ret;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);sleep(3);void* ret = nullptr;pthread_join(tid, &ret); cout << "main thread join done, thread return: "<< (char*)ret << endl;return 0;
}
● 线程执行方法的返回值是void*,可以返回任意类型的数据,自定义类对象也是可以的!
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
using namespace std;string ToHex(pthread_t tid)
{char id[64];snprintf(id, sizeof(id), "0x%x", tid);return id;
}class ThreadReturn
{
public:ThreadReturn(pthread_t id, const string& info, int code):id_(id),info_(info),code_(code){}public:pthread_t id_;string info_;int code_;
};void *threadRoutine(void *args)
{string name = static_cast<const char *>(args);usleep(1000);int cnt = 5;while (cnt--){cout << "我是新线程, 正在运行噢, 我的线程id是: " << ToHex(pthread_self()) << endl;sleep(1);}ThreadReturn* ret = new ThreadReturn(pthread_self(),"thread quit normal", 10);return ret;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void *)"thread-1");cout << "I am main thread, my thread id :" << ToHex(pthread_self()) << endl;void *ret = nullptr;pthread_join(tid, &ret);ThreadReturn* r = static_cast<ThreadReturn*>(ret); cout << "main thread get new thread info : " << r->code_ << ", " << ToHex(r->id_)<< ", " << r->info_<< endl;return 0;
}
线程分离
● 大部分软件,跑起来之后都是死循环,比如用户打开qq, 打开网易云音乐等等,打开后不会自动退出的,直到用户手动关掉。也就是说,新线程大多数是不需要被等待的,主线程创建出新线程之后就让新线程去跑了,主线程就不管了
● 线程默认是joinable状态的,但如果主线程就是不想等待新线程,不关心新线程的退出状态, 主线程自己直接做其他事情,那么就可以将新线程设置为分离状态
● 可以在主线程中将新线程设置为分离状态,新线程也可以让自己设置成分离状态
● 线程分离代码演示
int pthread_detach(pthread_t thread);
参数:分离的线程id
返回值: 成功,返回0,失败,返回错误码
#include <iostream>
#include <pthread.h>
#include <cstring>
#include <unistd.h>
using namespace std;void* threadRoutine(void* args)
{pthread_detach(pthread_self()); //新线程中将自己分离int cnt = 5;while(cnt--){cout << "thread is running..." << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);//pthread_detach(tid); //主线程中将tid线程分离 int n = pthread_join(tid, nullptr);cout << n << endl;return 0;
}
● 线程被分离后,可以被取消,但不能被join,取消线程后线程返回值是PTHREAD_CANCELED
#define PTHREAD_CANCELED ((void *) -1)
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <functional>
using namespace std;void* threadRoutine(void* args)
{int cnt = 5;while(cnt--){cout << "thread is running..." << endl;sleep(1);}return nullptr;
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, nullptr);sleep(1);pthread_detach(tid); int n = pthread_cancel(tid); //取消tid线程cout << "main thread cancel done, " << " n: " << n << endl;void* ret = nullptr;n = pthread_join(tid, &ret); cout << "main thread join done, " << " n: " << n << ", thread return: "<< (int64_t)ret << endl;return 0;
}
原生线程库
● Linux下没有真线程,只有轻量级进程,所以OS只会提供轻量级进程创建的系统调用,不会提供线程创建的系统调用
● 但用户只认线程,而且windows下是有真线程的,因此Linux在内核和用户层之间加了一层软件层,也就是pthread原生线程库,对内核的轻量级进程(LWP)接口进行封装,向上提供线程的一系列接口,同时管理多个线程,先描述,再组织,因此pthread库中是包含了一堆描述线程属性的结构体
● 原生线程库意思是任何一款操作系统都要默认有的,不属于C/C++语言本身, 因此编译时要带-l
● 作为用户,如果想知道一共创建了几个线程,每个线程的状态,当前有几个线程,一个线程退出了,退出结果是多少等信息,就直接去pthread库中获取即可
● 线程要有自己的一些独立属性:
1.上下文数据(被OS以轻量级进程形式维护在tcb中)
2.栈结构(栈大小,栈在哪里等信息,都必须在线程库中维护)
但是线程有多个,而地址空间中栈只有1个,如何分配??
clone接口 --- 创建轻量级进程, pthread_create的底层和fork的底层都是clone
第一个参数是线程执行的函数
第二个参数是线程库在堆区new的一段空间的起始地址,作为栈起始地址
第三个参数flags表示是创建轻量级进程还是创建一个真正的子进程
● 进程地址空间中的栈默认由主线程使用
● 线程库是共享的, 所以线程内部要管理整个系统中, 多个用户启动的多个线程!
● 而库要管理线程,就要在库中存在管理线程的结构体 --- struct pthread
● 线性局部存储是存放一些只能被线程自己看见的数据
● 线程栈就是保存了堆区new出来的一块空间的起始地址
● 每个线程在库中都是这三部分,可以把多个这部分看成一个数组,因此对线程的管理就转化成了对数组的增删查改
● 当线程退出时,退出结果会保存到库中的struct pthread中,因此主线程只需要去库中的struct pthread拷贝数据,拿到结果即可!
● 结论: pthread_t tid 表示的是线程属性集合在库中的地址!!! LWP是内核的概念!
● C++的线程库本质是对pthread的封装, 因为去掉-lpthread选项之后报链接错误
线程局部存储
● 全局变量本身就是被线程共享的,而如果定义全局变量时带上__thread,会发现全局变量不是只有1份了,而是每个线程都有一份!
● __thread修饰全局变量,会把全局变量拷贝到每个线程内的线程局部存储空间中!
#include <iostream>
#include <pthread.h>
#include <unistd.h>
using namespace std;//__thread是一个编译选项, 编译的时候就会把线程控制块中的空间开辟出来 --- 拷贝到线程局部存储空间中!
__thread int g_val = 100; void* threadRoutine(void* args)
{string name = static_cast<const char* >(args);while(true){cout << "I am new thread" << ", thread name: " << name << ", g_val:" << g_val << ", &g_val: " << &g_val << endl << endl;g_val++;sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread-1");while(true){cout << "I am main thread" << ", g_val:" << g_val << ", &g_val: " << &g_val << endl << endl;sleep(1);}pthread_join(tid, nullptr); return 0;
}
● 线程局部存储的用途: 定义一个全局变量,用__thread修饰,这样就可以在每个线程内部获取到线程的lwp
#include <iostream>
#include <pthread.h>
#include <sys/types.h>
#include <sys/syscall.h>
#include <unistd.h>
using namespace std;__thread pid_t lwp; void* threadRoutine(void* args)
{string name = static_cast<const char* >(args);lwp = syscall(SYS_gettid); //系统调用获取当前线程的lwpwhile(true){cout << "I am new thread" << ", thread name: " << name << "new thread lwp: " << lwp << endl; sleep(1);}
}int main()
{pthread_t tid;pthread_create(&tid, nullptr, threadRoutine, (void*)"thread-1");lwp = syscall(SYS_gettid); //系统调用获取当前线程的lwpwhile(true){cout << "I am new thread" << "new thread lwp: " << lwp << endl; sleep(1);}pthread_join(tid, nullptr); return 0;
}
注意:
●__thread string threadname; //err, __thread只能存储内置类型,不能存储一些容器
● 线程中可以fork, 本质是在创建子进程, 也可以调用execl, 不过替换的是整个进程,会影响其他所有线程,因此不建议在线程中excel,如果要execl,建议先fork, 再execl
自己实现线程封装
Thread.hpp
#pragma once #include <iostream>
#include <string>
#include <pthread.h>
#include <functional>
using namespace std;//设计方的视角template<class T>
using func_t = function<void(T)>; //返回值为void, 参数为T的类型template<class T>
class Thread
{
public:Thread(const string& threadname, func_t<T> func, T data):_tid(0),_threadname(threadname),_isrunning(false),_func(func),_data(data){}//改为static, 参数就没有this指针了!static void* ThreadRoutine(void* args) //不加static, 类内方法, 默认携带this指针{Thread* ts = static_cast<Thread *>(args); ts->_func(ts->_data);return nullptr; }//启动线程(内部调用线程创建)bool start(){int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);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;}}string ThreadName(){return _threadname;}bool IsRunning(){return _isrunning;}~Thread(){}private:pthread_t _tid; //线程idstring _threadname; //线程名bool _isrunning; //线程是否在运行func_t<T> _func; //线程执行方法T _data;
};
main.cc
#include <iostream>
#include <unistd.h>
#include <vector>
#include "thread.hpp"//应用方的视角
string GetThreadName()
{static int number = 1;char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}void Print(int num)
{while(num){cout << "hello world: " << num-- << endl;sleep(1);}
}int main()
{Thread<int> t(GetThreadName(), Print, 10);t.start();t.join();return 0;
}
线程的优缺点
优点:
● 创建一个新线程的代价要比创建一个新进程小得多
● 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
● 线程占用的资源要比进程少很多
● 能充分利用多处理器的可并行数量
● 在等待慢速I/O操作结束的同时程序可执行其他的计算任务
●计算密集型应用,为了能在多处理器系统上运行, 将计算分解到多个线程中实现
● I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
缺点:
● 缺乏访问控制
进程是访问控制的基本粒度, 由于大部分资源都是共享的,在一个线程中调用某些OS函数会对整个进程造成影响,而同步和互斥就是在解决这个问题
● 健壮性/鲁棒性降低
多线程中,一个线程崩溃,整个进程都崩溃,而多进程程序,一个进程崩溃,不影响其他进程,因为进程具有独立性
● 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
● 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与其他线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。
多线程共享与独占资源
多线程之间共享的资源
1. 进程代码段
2. 进程的公有数据(利用这些共享的数据,线程很容易的实现相互之间的通讯)
3. 进程打开的文件描述符
4. 信号的处理器
5. 进程的当前目录
6. 进程用户ID与进程组ID。
多线程之间独立的资源
1.线程ID
2.寄存器组的值
3.线程的堆栈
4.错误返回码
5.线程的信号屏蔽码
6.线程的优先级
线程互斥
下面是一段模拟多线程抢票的代码
#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"//构造线程名称
string GetThreadName()
{static int number = 1;char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}//抢票逻辑
int ticket = 10000; // 全局的共享资源
void GetTicket(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()
{string name1 = GetThreadName();Thread<string> t1(name1, GetTicket, name1);sleep(2);string name2 = GetThreadName();Thread<string> t2(name2, GetTicket, name2);sleep(2);string name3 = GetThreadName();Thread<string> t3(name3, GetTicket, name3);sleep(2);t1.start();sleep(2);t2.start();sleep(2);t3.start();sleep(2);t1.join();t2.join();t3.join();return 0;
}
运行代码发现最后出现了票数出现了负数,但是我们在if语句中判断票数>0了呀,为啥还会出现票数为负数呢???
显然票数是公共资源,可以被多个执行流同时访问,而多个执行流同时访问公共资源显然出现了问题,因此我们需要把公共资源保护起来,使得任何一个时刻,只允许一个线程正在访问公共资源,此时公共资源就叫做临界资源! 而我们的代码中只有一部分代码会去访问临界资源的,进程中访问临界资源的代码叫做临界区
任何时刻只允许一个执行流进入临界区,使得多个执行流只能串行访问临界资源,叫做互斥!
++/--本质是三条汇编语句, 每一条汇编语句都是原子性的, 而执行每一条汇编语句都有可能被中断, 三条汇编语句过程是 先把内存中的a拷贝到cpup寄存器中,然后在寄存器中对a++, 最后将寄存器的a拷贝会回内存空间中!
多线程同时访问公共资源有什么问题呢??? 举个例子!
比如有A线程和B线程, 公共资源是int a = 10, 两个线程都要进行a++操作, 目前的情况是A线程把汇编的第二步执行完毕,寄存器中a为11, 然后被切换走了,于是A线程的上下文数据中就保存了a为11, 此时线程B被cpu调度,一直将内存空间中的a++到了100,此时被切走了,线程A被调度,接着执行第三条汇编语句,将自己的上下文数据,a=11恢复到寄存器中,然后将寄存器内容写回内存,于是内存空间中的a改为了11,就出现了数据不一致的问题!!
而我们今天的抢票代码最后票出现了负数原因是:
当票数减为1时,多个线程进行了if条件判断,都是成立的,语句进入到了if循环内部,此时某个线程被调度,将内存中的tickets--到了0, 此时其他线程都执行过了if判断, 再对票数--, 就会将内存中的票数--到负数!
互斥锁
● 互斥锁的功能就是用来实现互斥,使得临界资源只能同时被一个执行流访问
● 尽量要给少的代码块加锁 (因为加锁之后,同一时间内只允许一个线程访问临界区资源,如果给大量代码加锁,多线程就没有意义了,效率可能大大降低)
● 一般都是给临界区加锁
● 使用锁的相关接口
定义全局互斥锁并初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
使用全局锁实现互斥访问临界资源
//定义全局锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;//抢票逻辑
int ticket = 10000; // 全局的共享资源
void GetTicket(string name)
{while (true){pthread_mutex_lock(&mutex); if (ticket > 0){// 充当抢票花费的时间usleep(1000);printf("%s get a ticket : %d\n", name.c_str(), ticket);ticket--; pthread_mutex_unlock(&mutex); }else{pthread_mutex_unlock(&mutex); break;}// 实际情况, 还有后续的动作}
}
注意:
● 为了实现互斥访问临界资源,我们定义了一把全局锁,而全局锁也是全局变量,也是公共资源的,也得保证申请锁是安全的呀!!! 而申请锁本身是原子性操作,是安全的!
● 加锁是由程序员自己保证的,是一种规则,不遵守就是自己写的bug
● 根据互斥的定义, 任何时刻,只允许一个线程申请锁成功! 就注定了会有多个线程申请锁失败,失败的线程默认会在mutex锁上阻塞,阻塞的本质就是等待!
● 一个线程在临界区访问临界资源的时候, 是完全有可能发生线程切换的,但是切换走的线程依旧没有释放锁,可以理解成把锁带走了,其他线程依旧访问不了临界资源
● 加锁的情况下,if里面的代码块也表现出"原子性", 因为这段代码要么不执行,要么执行完,别的线程才能访问
定义局部互斥锁
初始化局部锁(第二个参数可以设置锁属性,传nullptr即可)
int pthread_mutex_init(pthread_mutex_t *restrict mp,const pthread_mutexattr_t *restrict mattr);
释放局部锁
int pthread_mutex_destroy(pthread_mutex_t *mutex)
加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
使用局部锁实现互斥访问临界资源
#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"string GetThreadName()
{static int number = 1;char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}int ticket = 10000; // 全局的共享资源void GetTicket(pthread_mutex_t* mutex)
{while (true){ pthread_mutex_lock(mutex); if (ticket > 0){// 充当抢票花费的时间usleep(1000);printf("get a ticket : %d\n", ticket);ticket--;pthread_mutex_unlock(mutex);}else{pthread_mutex_unlock(mutex);break;}// 实际情况, 还有后续的动作}
}int main()
{pthread_mutex_t mutex; //定义局部锁pthread_mutex_init(&mutex, nullptr); //初始化局部锁string name1 = GetThreadName();Thread<pthread_mutex_t*> t1(name1, GetTicket, &mutex);string name2 = GetThreadName();Thread<pthread_mutex_t*> t2(name2, GetTicket, &mutex);string name3 = GetThreadName();Thread<pthread_mutex_t*> t3(name3, GetTicket, &mutex);t1.start();t2.start();t3.start();t1.join();t2.join();t3.join();pthread_mutex_destroy(&mutex); //释放局部锁return 0;
}
自己实现锁的封装
LockGuard.hpp
#include <iostream>
#include <string>
#include <pthread.h>
using namespace std;//不定义锁, 默认外部会传入锁对象
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.Lock();}~LockGuard(){_mutex.Unlock();}
private:Mutex _mutex;
};
main.cc
#include <iostream>
#include <unistd.h>
#include <vector>
#include "Thread.hpp"
#include "LockGuard.hpp"string GetThreadName()
{static int number = 1;char name[64];snprintf(name, sizeof(name), "Thread-%d", number++);return name;
}int ticket = 10000; // 全局的共享资源
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 全局锁对象void GetTicket(string name)
{while (true){LockGuard lockguard(&mutex);{if (ticket > 0){// 充当抢票花费的时间usleep(1000);printf("get a ticket : %d\n", ticket);ticket--;}else{break;}}// 实际情况, 还有后续的动作}
}int main()
{string name1 = GetThreadName();Thread<string> t1(name1, GetTicket, name1);string name2 = GetThreadName();Thread<string> t2(name2, GetTicket, name2);string name3 = GetThreadName();Thread<string> t3(name3, GetTicket, name3);t1.start();t2.start();t3.start();t1.join();t2.join();t3.join();return 0;
}
加锁实现互斥的原理
● 大多数体系结构都提供了swap和exchange指令, 作用是把寄存器内容和内存单元的数据进行交换
● 而锁本质是一个结构体,可以简单认为内部有一个变量 int mutex = 1
● A线程加锁时,先将寄存器%al内容置成0,然后交换寄存器%al的内容和内存变量mutex内容,于是%al的内容变为了1, mutex变为了0
此时就算线程A被切走了,会把上下文数据包括%al的内容带走,线程B开始调度运行, 内存内容已经是0了,因此尽管线程B交换寄存器%al的值和内存中的mutex的值,交换完之后还是0,此时进入else分支,挂起等待。因此可以认为,线程切换时把锁带走了!
● 所以交换本质是将一个共享的mutex资源,交换到自己的上下文中,属于线程自己了!
● 解锁就是直接将mutex置为1,其他线程申请锁时就正常执行上述汇编语句即可!
● 加锁和解锁的一般规则: 谁加锁,谁解锁
死锁
● 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资 源而处于的一种永久等待状
● 死锁的四个必要条件
1. 互斥条件:一个资源每次只能被一个执行流使用
2. 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放
3. 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
4. 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系
● 避免死锁:
1. 破坏死锁的四个必要条件
2. 加锁顺序一致
3. 避免锁未释放的场景
4. 资源一次性分配
● 避免死锁算法
1.死锁检测算法
2.银行家算法
线程同步
● 同步本质是让多执行流访问临界资源具有一定的顺序
多线程访问公共资源可能会出现问题,因此我们使用互斥锁保证任何一个时刻只有1个线程访问公共资源,但有些线程竞争锁的能力很强,每次竞争时都会拿到锁,导致其他线程长时间访问不了公共资源,也就是会导致线程饥饿问题
● 互斥能保证访问资源的安全性,但只有安全是不够的,同步能够较为充分高效的使用资源
● 条件变量本质就是实现线程同步的一种机制,是pthread库提供的一个线程向另一个线程通知信息的方式
● 举个例子理解条件变量
一张桌子,两个人,一个人放苹果,另一个蒙着眼睛的人拿苹果,放苹果的时候不能拿,拿的时候不能放,因此要加锁实现互斥!而拿苹果的人不知道放苹果的人什么时候放,因此拿苹果的人不断的申请锁,检测,释放锁,导致了放苹果人的饥饿问题!
于是放了一个铃铛,让放苹果的人放苹果之后,敲一下铃铛,此时拿苹果的人再去拿苹果!
上述的铃铛本质就是条件变量,可以理解成以下结构体:
struct cond
{int flag; //条件是否就绪tcb_queue; //条件不满足,就排队!
}
● 上述例子中只有1个拿苹果的人,实际可以有多个拿苹果的人,可以认为所有拿苹果的人都要排队,拿完苹果之后再去队尾重新排队,这就使得多执行流可以较为均衡地按照一定顺序访问资源
● 条件变量使用接口
在cond条件变量下进行等待
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
唤醒条件变量下等待的一个线程
int pthread_cond_signal(pthread_cond_t *cond);
唤醒条件变量下等待的所有线程
int pthread_cond_broadcast(pthread_cond_t *cond);
● 条件变量使用示例
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstring>
using namespace std;pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 定义条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义全局锁void *threadRoutine(void *args)
{const char* threadname = static_cast<const char*>(args);while(true){pthread_mutex_lock(&mutex);pthread_cond_wait(&cond, &mutex); //在指定的条件变量下等待cout << "I am a new thread: " << threadname << endl;pthread_mutex_unlock(&mutex);}
}int main()
{pthread_t t1, t2, t3;pthread_create(&t1, nullptr, threadRoutine, (void*)"thread-1");pthread_create(&t2, nullptr, threadRoutine, (void*)"thread-2");pthread_create(&t3, nullptr, threadRoutine, (void*)"thread-3");sleep(5); // 5s之后唤醒线程while(true){pthread_cond_signal(&cond); //每次唤醒一个线程sleep(1);}pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);
}
说明:
● cout是往显示器上打印,多个线程都执行cout语句,访问显示器资源,此时显示器资源也是公共资源,打印会出现混乱的情况,因此显示器资源也需要被保护起来,因此在cout语句前后加锁解锁
● 只加锁,发现线程1、2、3打印没有任何的顺序性,且一个线程一打印就是一批语句,这就是竞争锁的能力不同而导致的
● 为了让线程1、2、3打印具有一定的顺序性,我们引入了条件变量,在加锁和解锁之间使用pthread_cond_wait 接口让线程在条件变量下等待,在主线程中,每隔1s唤醒一个线程,从而使得打印结果具有明显的顺序性
● 如果在主线程中,每隔1s唤醒所有线程,那么所有线程都会去参与锁的竞争,因此最后打印的顺序依旧不确定
抢票代码加入线程同步:
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <cstring>
using namespace std;pthread_cond_t cond = PTHREAD_COND_INITIALIZER; // 定义条件变量
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 定义全局锁int tickets = 5000;void *threadRoutine(void *args)
{string threadname = static_cast<const char *>(args);while (true){pthread_mutex_lock(&mutex);if (tickets > 0){usleep(1000); // 充当抢票花费的时间cout << threadname << ": get a ticket, ticket : " << tickets << endl;tickets--;}else{cout << threadname << ", 没有票了" << endl;pthread_cond_wait(&cond, &mutex); // 没有票了, 就去条件变量下等待}pthread_mutex_unlock(&mutex);}
}int main()
{pthread_t t1, t2, t3;pthread_create(&t1, nullptr, threadRoutine, (void *)"thread-1");pthread_create(&t2, nullptr, threadRoutine, (void *)"thread-2");pthread_create(&t3, nullptr, threadRoutine, (void *)"thread-3");sleep(5); // 5s之后唤醒线程while (true){sleep(6);pthread_mutex_lock(&mutex);tickets += 1000; // 每隔6s, 就再放1000张票pthread_mutex_unlock(&mutex);pthread_cond_signal(&cond); // 唤醒一个线程// pthread_cond_broadcast(&cond); // 唤醒所有线程}pthread_join(t1, nullptr);pthread_join(t2, nullptr);pthread_join(t3, nullptr);return 0;
}
说明:
● 加锁和解锁之间,往往要访问临界资源,但是临界资源不一定是满足条件的,所以我们要判断,条件不满足,就应该让线程去条件变量下等待
● 线程在条件变量下等待的时候, 会自动释放锁
● 线程被唤醒的时候,是在临界区内被唤醒的,当线程被唤醒,在wait函数返回时,要重新申请并持有锁,才能真正被唤醒,这也就是 pthread_cond_wait 的参数中同时有条件变量和锁的原因,而重新申请并持有锁也是要参与锁的竞争的!
相关文章:

Linux中的线程
目录 线程的概念 进程与线程的关系 线程创建 线程终止 线程等待 线程分离 原生线程库 线程局部存储 自己实现线程封装 线程的优缺点 多线程共享与独占资源 线程互斥 互斥锁 自己实现锁的封装 加锁实现互斥的原理 死锁 线程同步 线程的概念 回顾进程相关概念 …...

AI大模型学习笔记|多目标算法梳理、举例
多目标算法学习内容推荐: 1.通俗易懂讲算法-多目标优化-NSGA-II(附代码讲解)_哔哩哔哩_bilibili 2.多目标优化 (python pyomo pareto 最优)_哔哩哔哩_bilibili 学习笔记: 通过网盘分享的文件:多目标算法学习笔记 链接: https://pan.baidu.com…...
蓝桥杯刷题——day3
蓝桥杯刷题——day3 题目一题干题目解析代码 题目二题干题目解析代码 题目一 题干 每张票据有唯一的 ID 号,全年所有票据的 ID 号是连续的,但 ID 的开始数码是随机选定的。因为工作人员疏忽,在录入 ID 号的时候发生了一处错误,造…...

企业级日志分析系统ELK之ELK概述
ELK 概述 ELK 介绍 什么是 ELK 早期IT架构中的系统和应用的日志分散在不同的主机和文件,如果应用出现问题,开发和运维人员想排 查原因,就要先找到相应的主机上的日志文件再进行查找和分析,所以非常不方便,而且还涉及…...

【开源项目】经典开源项目数字孪生体育馆—开源工程及源码
飞渡科技数字孪生体育馆管理平台,融合物联网IOT、BIM数据模型、三维GIS等技术,实现体育馆的全方位监控和实时全局掌握,同时,通过集成设备设施管理、人员管理等子系统,减少信息孤岛,让场馆“可视、可控、可管…...

C++多线程实战:掌握图像处理高级技巧
文章结尾有最新热度的文章,感兴趣的可以去看看。 本文是经过严格查阅相关权威文献和资料,形成的专业的可靠的内容。全文数据都有据可依,可回溯。特别申明:数据和资料已获得授权。本文内容,不涉及任何偏颇观点,用中立态度客观事实描述事情本身 导读 在当今的计算世界中,…...

解决MAC装win系统投屏失败问题(AMD显卡)
一、问题描述 电脑接上HDMI线后,电脑上能显示有外部显示器接入,但是外接显示器无投屏画面 二、已测试的方法 1 更改电脑分辨,结果无效 2 删除BootCamp,结果无效 3更新电脑系统,结果无效 4 在设备管理器中&#…...
网易游戏分享游戏场景中MongoDB运行和分析实践
在游戏行业中,数据库的稳定和性能直接影响了游戏质量和用户满意度。在竞争激烈的游戏市场中,一个优秀的数据库产品无疑能为游戏的开发和后期的运营奠定良好的基础。伴随着MongoDB在不同类型游戏场景中的应用越来越广泛,许多知名的游戏公司都在…...
Android14 AOSP 允许system分区和vendor分区应用进行AIDL通信
在Android14上,出于种种原因,system分区的应用无法和vendor分区的应用直接通过AIDL的方法进行通信,但是项目的某个功能又需要如此。 好在Binder底层其实是支持的,只是在上层进行了屏蔽。 修改 frameworks/native/libs/binder/Bp…...

R学习——因子
目录 1 定义因子(factor函数) 2因子的作用 一个数据集中的 只需要考虑可以用哪个数据来进行分类就可以了,可以用来分类就可以作为因子。 Cy1这个因子对应的水平level是4 6 8: 1 定义因子(factor函数) 要…...

pytest入门三:setup、teardown
https://zhuanlan.zhihu.com/p/623447031 function对应类外的函数,每个函数调用一次 import pytest def setup_module():print(开始 module)def teardown_module():print(结束 module)def setup_function():print(开始 function)def teardown_function():print(结…...
前端面试准备问题2
1.防抖和节流分别是什么,应用场景 防抖:在事件被触发后,只有在指定的延迟时间内没有再次触发,才执行事件处理函数。 在我的理解中,简单的说就是在一个指定的时间内,仅触发一次,如果有多次重复触…...
web前端sse封装
这是一个基于microsoft/fetch-event-source包封装的sse函数,包含开始、停止功能; 可传更多参数、使用非常简单。 使用前: 安装 microsoft/fetch-event-source 代码: // sse import { fetchEventSource } from microsoft/fetch-event-source import { …...

智能家居WTR096-16S录放音芯片方案,实现语音播报提示及录音留言功能
前言: 在当今社会的高速运转之下,夜幕低垂之时,许多辛勤工作的父母尚未归家。对于肩负家庭责任的他们而言,确保孩童按时用餐与居家安全成为心头大事。此时,家居留言录音提示功能应运而生,恰似家中的一位无形…...
【创建模式-蓝本模式(Prototype Pattern)】
目录 Overview应用场景代码演示JDK Prototype pattern 更优实践泛型克隆接口 https://doc.hutool.cn/pages/Cloneable/#%E6%B3%9B%E5%9E%8B%E5%85%8B%E9%9A%86%E7%B1%BB The prototype pattern is a creational design pattern in software development. It is used when the t…...
Spring Boot应用开发深度解析与实战案例
Spring Boot应用开发深度解析与实战案例 在当今快速发展的软件开发领域,Spring Boot凭借其“约定优于配置”的理念,极大地简化了Java应用的开发、配置和部署过程,成为了微服务架构下不可或缺的技术选型。本文将深入探讨Spring Boot的核心特性、最佳实践,并通过一个具体的…...

优化Go语言中的网络连接:设置代理超时参数
网络连接优化的重要性 在分布式系统和微服务架构中,网络请求的效率直接影响到整个系统的响应速度。合理的超时设置可以防止系统在等待网络响应时陷入无限期的阻塞,从而提高系统的吞吐量和用户体验。特别是在使用代理服务器时,由于增加了网络…...
《神经网络与深度学习》(邱锡鹏) 内容概要【不含数学推导】
第1章 绪论 基本概念:介绍了人工智能的发展历程及不同阶段的特点,如符号主义、连接主义、行为主义等。还阐述了深度学习在人工智能领域的重要地位和发展现状,以及其在图像、语音、自然语言处理等多个领域的成功应用。术语解释 人工智能&…...
原创 传奇996_55——后端如何点击npc隐藏主界面
点击图片退出,举例: |linkexit Img|ax0.5|ay0.5|percentx50|percenty50|imgpublic/touming2.png|hideMain1|linkexit <Img|x0|y0|esc1|show4|bg1|move0|imgcustom/new/longhun/bg.png|loadDelay0|reset1|hideMain1>...

RabbitMQ中的Work Queues模式
在现代分布式系统中,消息队列(Message Queue)是实现异步通信和解耦系统的关键组件之一。RabbitMQ 是一个广泛使用的开源消息代理软件,支持多种消息传递模式。其中,Work Queues(工作队列)模式是一…...

大型活动交通拥堵治理的视觉算法应用
大型活动下智慧交通的视觉分析应用 一、背景与挑战 大型活动(如演唱会、马拉松赛事、高考中考等)期间,城市交通面临瞬时人流车流激增、传统摄像头模糊、交通拥堵识别滞后等问题。以演唱会为例,暖城商圈曾因观众集中离场导致周边…...
【位运算】消失的两个数字(hard)
消失的两个数字(hard) 题⽬描述:解法(位运算):Java 算法代码:更简便代码 题⽬链接:⾯试题 17.19. 消失的两个数字 题⽬描述: 给定⼀个数组,包含从 1 到 N 所有…...

第一篇:Agent2Agent (A2A) 协议——协作式人工智能的黎明
AI 领域的快速发展正在催生一个新时代,智能代理(agents)不再是孤立的个体,而是能够像一个数字团队一样协作。然而,当前 AI 生态系统的碎片化阻碍了这一愿景的实现,导致了“AI 巴别塔问题”——不同代理之间…...

c#开发AI模型对话
AI模型 前面已经介绍了一般AI模型本地部署,直接调用现成的模型数据。这里主要讲述讲接口集成到我们自己的程序中使用方式。 微软提供了ML.NET来开发和使用AI模型,但是目前国内可能使用不多,至少实践例子很少看见。开发训练模型就不介绍了&am…...
Pinocchio 库详解及其在足式机器人上的应用
Pinocchio 库详解及其在足式机器人上的应用 Pinocchio (Pinocchio is not only a nose) 是一个开源的 C 库,专门用于快速计算机器人模型的正向运动学、逆向运动学、雅可比矩阵、动力学和动力学导数。它主要关注效率和准确性,并提供了一个通用的框架&…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...

使用Spring AI和MCP协议构建图片搜索服务
目录 使用Spring AI和MCP协议构建图片搜索服务 引言 技术栈概览 项目架构设计 架构图 服务端开发 1. 创建Spring Boot项目 2. 实现图片搜索工具 3. 配置传输模式 Stdio模式(本地调用) SSE模式(远程调用) 4. 注册工具提…...

AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...

【JVM】Java虚拟机(二)——垃圾回收
目录 一、如何判断对象可以回收 (一)引用计数法 (二)可达性分析算法 二、垃圾回收算法 (一)标记清除 (二)标记整理 (三)复制 (四ÿ…...
【Android】Android 开发 ADB 常用指令
查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...