当前位置: 首页 > news >正文

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中的线程

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

【保姆级】Mac如何安装+切换Java环境

本文从如何下载不同版本的JDK,到如何丝滑的切换JDK,以及常见坑坑的处理方法,应有尽有,各位看官走过路过不要错过~~~ 下载⏬ 首先上官网: https://www.oracle.com/ 打不开的话可以使用下面👇这个中文的 https://www.oracle.com/cn/java/technologies/downloads/a…...

leetcode 31 Next Permutation

题意 找到下一个permutation是什么&#xff0c;对于一个数组[1&#xff0c;2&#xff0c;3]&#xff0c;下一个排列就是[1, 3, 2] 链接 https://leetcode.com/problems/next-permutation/ 思考 首先任何一个permutation满足一个性质&#xff0c;从某个位置往后一定是降序。…...

从RocketMQ到Dubbo:自研中间件技术的崛起

目录 一. 宝兰德 BES 中间件&#xff08;BES - Business Enterprise Server&#xff09; 二. TongWeb 中间件 三. 阿里巴巴 RocketMQ 四. 京东 JStorm 五. 华为 GaussDB 六. 华为 FusionInsight 七. 百度 Tinker 八. 中科曙光 Kylin 九. 滴滴 Druid 一. 宝兰德 BES 中间…...

计算机键盘简史 | 键盘按键功能和指法

注&#xff1a;本篇为 “计算机键盘简史 | 键盘按键功能和指法” 相关文章合辑。 英文部分机翻未校。 The Evolution of Keyboards: From Typewriters to Tech Marvels 键盘的演变&#xff1a;从打字机到技术奇迹 Introduction 介绍 The keyboard has journeyed from a humb…...

考研两三题

283. 移动零 - 力扣&#xff08;LeetCode&#xff09; 本题解法&#xff1a;使用双指针 解法一&#xff1a; 如上图&#xff0c;在一开始slow指向-1&#xff0c;fast指向数组起始位&#xff0c;进入while循环&#xff0c;保证fast不越界&#xff0c;因为代码中fast会自增&#…...

CentOS8或docker镜像centos8更换镜像源

因为 CentOS 8 已经结束生命周期&#xff0c;原来的镜像源不可用了。我们需要将镜像源改为 CentOS 8 的替代源。 在容器中运行以下命令&#xff1a; 首先备份原有的源 cd /etc/yum.repos.d/ mkdir backup mv *.repo backup/ 创建新的源文件 cat > /etc/yum.repos.d/Cent…...

集成方案 | Docusign + 泛微,实现全流程电子化签署!

本文将详细介绍 Docusign 与泛微的集成步骤及其效果&#xff0c;并通过实际应用场景来展示 Docusign 的强大集成能力&#xff0c;以证明 Docusign 集成功能的高效性和实用性。 在现代企业运营中&#xff0c;效率和合规性是至关重要的。泛微作为企业级办公自动化和流程管理的解决…...

知乎数据分析面试题及参考答案

请阐述大数定理和中心极限定理的内容及其在数据分析中的应用。 大数定理是指在随机试验中,每次出现的结果不同,但是大量重复试验出现的结果的平均值却几乎总是接近于某个确定的值。简单来说,当样本量足够大的时候,样本均值会趋近于总体均值。例如,抛硬币试验,当抛硬币的次…...

8.Vue------如何重新加载子组件key属性----vue知识积累

在子组件中使用key属性是一种强制重新渲染子组件的有效方法。‌通过改变key属性的值&#xff0c;Vue会 销毁旧组件并创建新组件&#xff0c;从而实现组件的重新加载。这种方法适用于需要完全重新渲染子组件&#xff0c; 包括其内部状态和生命周期钩子的场景&#xff0c;特别是…...

C语言动态内存管理

1.为什么要有动态内存分配 内存开辟方式: int val 20;//在栈空间上开辟四个字节 char arr[10] {0};//在栈空间上开辟10个字节的连续空间开辟空间的方式特点 空间开辟大小是固定德的数组在申明的时候&#xff0c;必须指明数组的长度&#xff0c;数组空间一旦确定了大小不能…...

【蓝桥杯备战】Day 1

1.基础题目 LCR 018.验证回文串 给定一个字符串 s &#xff0c;验证 s 是否是 回文串 &#xff0c;只考虑字母和数字字符&#xff0c;可以忽略字母的大小写。 本题中&#xff0c;将空字符串定义为有效的 回文串 。 示例 1: 输入: s "A man, a plan, a canal: Panama…...

电子应用设计方案-51:智能镜子系统方案设计

智能镜子系统方案设计 一、引言 智能镜子作为一种新兴的智能家居设备&#xff0c;将传统镜子与现代科技相结合&#xff0c;为用户提供了丰富的功能和便捷的体验。本方案旨在设计一款功能强大、用户友好、外观美观的智能镜子系统。 二、系统概述 1. 系统目标 - 提供清晰的镜面…...

[2015~2024]SmartMediaKit音视频直播技术演进之路

技术背景 2015年&#xff0c;因应急指挥项目需求&#xff0c;我们实现了RTMP推送音视频采集推送&#xff08;采集摄像头和麦克风数据&#xff09;模块&#xff0c;在我们做好了RTMP推送模块后&#xff0c;苦于没有一个满足我们毫秒级延迟诉求的RTMP播放器&#xff0c;于是第一…...

图片添加水印的实验原理,实验代码,实验现象

一、图片添加水印的实验原理 1. 选择水印类型 可见水印&#xff1a;这些水印可以直接被肉眼看到&#xff0c;通常用于防止未经授权的使用。它们可以是文字、标志或图案等形式&#xff0c;并且放置在图像的显著位置。不可见水印&#xff1a;这类水印不容易被察觉&#xff0c;但…...

Python Web 开发案例解析

一、Flask 基础应用案例 &#xff08;一&#xff09;项目搭建与初始化 环境准备 安装 Python 解释器&#xff0c;确保版本符合 Flask 要求&#xff0c;如 Python 3.6 及以上。使用pip安装 Flask 库&#xff0c;命令为pip install flask。可以创建虚拟环境&#xff0c;如python…...

JPG 转 PDF:免费好用的在线图片转 PDF 工具

JPG 转 PDF&#xff1a;免费好用的在线图片转 PDF 工具 在日常工作和生活中&#xff0c;我们经常需要将图片转换为 PDF 格式。无论是制作电子文档、准备演示材料&#xff0c;还是整理照片集&#xff0c;将图片转换为 PDF 都是一个常见的需求。今天为大家介绍一款完全免费、无需…...

代码随想录44天

1143.最长公共子序列 class Solution:def longestCommonSubsequence(self, s: str, t: str) -> int:n, m len(s), len(t)cache # 缓存装饰器&#xff0c;避免重复计算 dfs 的结果&#xff08;记忆化&#xff09;def dfs(i: int, j: int) -> int:if i < 0 or j <…...

C语言-详细解答-重组字符串并16进制转10进制

1.题目要求 2.代码实现 #include <stdio.h> #include <ctype.h> #include <string.h>int hexToDec(char hex[]) {int len strlen(hex);int base 1;int dec 0;for (int i len - 1; i > 0; i--) {if (isdigit(hex[i])) {dec (hex[i] - 0) * base;} e…...

scala列表

1 不可变 List 说明 &#xff08;1&#xff09;List 默认为不可变集合 &#xff08;2&#xff09;创建一个 List&#xff08;数据有顺序&#xff0c;可重复&#xff09; &#xff08;3&#xff09;遍历 List &#xff08;4&#xff09;List 增加数据 &#xff08;5&#…...

医疗急救技能大赛理论题库

医疗急救技能大赛理论题库 题型包括&#xff1a;A1 型题 400题&#xff0c;A2 型题40题&#xff0c;A3/A4 型40 题&#xff0c; B 型题 80 题 &#xff0c;X 型题 160 题&#xff0c;判断题 80 题。 一、A1 型题:共计400 题 &#xff08;即最佳回答题&#xff0c;它由一个叙…...

深入浅出WebRTC—ULPFEC

FEC 通过在发送端添加额外的冗余信息&#xff0c;使接收端即使在部分数据包丢失的情况下也能恢复原始数据&#xff0c;从而减轻网络丢包的影响。在 WebRTC 中&#xff0c;FEC 主要有两种实现方式&#xff1a;ULPFEC 和 FlexFEC&#xff0c;FlexFEC 是 ULPFEC 的扩展和升级&…...

SQL 在线格式化 - 加菲工具

SQL 在线格式化 打开网站 加菲工具 选择“SQL 在线格式化” 或者直接访问 https://www.orcc.online/tools/sql 输入sql&#xff0c;点击上方的格式化按钮即可 输入框得到格式化后的sql结果...

到达率的变化动态调整服务器的服务率,实现负载均衡,提高资源利用效率

中心可以根据任务到达率的变化动态调整服务器的服务率,实现负载均衡,提高资源利用效率 服务率和到达率 中心可以根据任务到达率的变化动态调整服务器的服务率,实现负载均衡,提高资源利用效率服务率(Service Rate)到达率(Arrival Rate)控制参数实现负载均衡的方法在云计…...

【Golang】Go语言编程思想(六):Channel,第四节,Select

使用 Select 如果此时我们有多个 channel&#xff0c;我们想从多个 channel 接收数据&#xff0c;谁来的快先输出谁&#xff0c;此时应该怎么做呢&#xff1f;答案是使用 select&#xff1a; package mainimport "fmt"func main() {var c1, c2 chan int // c1 and …...

认证插件介绍

本文档是针对 UOS 登录器插件给出开发指南&#xff0c;目的是为了让开发人员了解如何在 UOS 登录器上增加一种自定义认证方式&#xff0c;对插件接口做了详细说明以及实战练习。 文章目录 一、认证插件可以做什么&#xff1f;二、认证流程三、术语说明四、安全性五、可靠性六、…...

MindSearch深度解析实践

1. 课程内容 1.1 MindSearch 简介 MindSearch 是一个开源的 AI 搜索引擎框架&#xff0c;具有与 Perplexity.ai Pro 相同的性能。我们可以轻松部署它来构建自己的专属搜索引擎&#xff0c;可以基于闭源的LLM&#xff08;如GPT、Claude系列&#xff09;&#xff0c;也可以使用…...

[oeasy]python052_[系统开发语言为什么默认是c语言

[系统开发语言为什么默认是c语言 [趣味拓展]c语言发祥史_c是如何成为第一系统语言的 上次我们了解了 标识符 要求 首字符 为 字母和下划线 后面字符 为 字母和下划线 外加 数字 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 使用isidentifier函数 可…...

【java常用算法和应用场景】

java常用算法和应用场景 Java中常用的算法涵盖多个领域&#xff0c;包括排序算法、查找算法、字符串匹配算法、图论算法、动态规划算法、贪心算法、分治算法等。以下是Java中一些常用算法及其应用场景和示例代码&#xff1a; 一、排序算法 排序算法是计算机科学中的一种基本…...

D96【python 接口自动化学习】- pytest进阶之fixture用法

day96 pytest的fixture详解&#xff08;三&#xff09; 学习日期&#xff1a;20241211 学习目标&#xff1a;pytest基础用法 -- pytest的fixture详解&#xff08;三&#xff09; 学习笔记&#xff1a; fixture(scop"class") (scop"class") 每一个类调…...

知从科技总经理受邀参加上海临港新片区商会“湖畔TECS”技术分享沙龙(第五期)

11月26日&#xff0c;上海知从科技有限公司创始人陈荣波先生受邀出席临港新片区商会 “湖畔TECS”技术分享沙龙&#xff08;第五期&#xff09;活动&#xff0c;并在活动上为参会嘉宾们做了主题分享。本次活动由临港新片区商会主办&#xff0c;智能网联汽车创新联盟协办&#x…...

scala的泛型应用场景

用代码表示泛型类&#xff1a;//泛型&#xff1a;类型参数化 //Pair约定一对数据 class Pair[T](var x:T,var y:T) //class 类名&#xff08;成员名&#xff1a;数据类型&#xff09;--->class 类名[泛型名]&#xff08;成员名&#xff1a;泛型名&#xff09;/.参考代码&…...

三种策略改进的沙猫群优化算法(MSCSO)

三种策略改进的沙猫群优化算法(MSCSO) 目录 三种策略改进的沙猫群优化算法(MSCSO)效果一览基本介绍程序设计参考资料效果一览 基本介绍 改进点: 三角形游走策略Levy飞行游走策略透镜成像反向学习SCSO是 2022年提出的元启发式优化算法,该算法灵感来源于沙猫的捕食行为,沙猫群…...

Google Cloud Database Option(数据库选项说明)

关系数据库 在关系数据库中&#xff0c;信息存储在表、行和列中&#xff0c;这通常最适合结构化数据。因此&#xff0c;它们用于数据结构不经常更改的应用程序。与大多数关系数据库交互时使用 SQL&#xff08;结构化查询语言&#xff09;。它们为数据提供 ACID 一致性模式&am…...

java抽奖系统登录下(四)

6.4 关于登录 最简单的登录&#xff1a; 1、web登录页填写登录信息&#xff0c;前端发送登录信息到后端&#xff1b; 2、后端接受登录信息&#xff0c;并校验。校验成功&#xff0c;返回成功结果。 这种登录会出现一个问题&#xff0c;用户1成功登录之后&#xff0c;获取到后台…...

Linux入门攻坚——41、Linux集群系统入门-lvs(2)

lvs-dr&#xff1a;GATEWAY Director只负责请求报文&#xff0c;响应报文不经过Director&#xff0c;直接由RS返回给Client。 lvs-dr的报文路线如上图&#xff0c;基本思路就是报文不会回送Director&#xff0c;第①种情况是VIP、DIP、RIP位于同一个网段&#xff0c;这样&…...

Android Freezer

Freezer原理 Android按照优先级将一般的APP从高到低分为: 前台进程 --> 可感知进程–> 服务进程 --> Cached进程。 Freezer通过冻住cached进程,来迫使这些进程让出CPU&#xff0c;以达到优化系统资源使用的目的。 Cached进程是怎么判定的呢&#xff1f; 由于andro…...

GeeCache-单体并发缓存

实现LRU中value接口的缓存类 使用互斥锁封装LRU缓存类&#xff0c;实现并发访问 实现Group组&#xff0c;用名称对缓存分类 Getter为缓存击穿时调用的回调函数 若缓存击穿则调用回调函数&#xff0c;并把读取到的值加载到缓存中...

ctfshow-web 151-170-文件上传

151. 我们首先想到就是上传一句话木马。但是看源代码限制了png。 &#xff08;1&#xff09;改前端代码。 这里是前端限制了上传文件类型&#xff0c;那我们就改一下就好了嘛,改成php。 这里直接修改不行&#xff0c;给大家推荐一篇简短文章&#xff0c;大家就会了&#xff08…...

汽车车牌标记支持YOLO,COCO,VOC三种格式标记,4000张图片的数据集

本数据集支持YOLO&#xff0c;COCO&#xff0c;VOC三种格式标记汽车车牌&#xff0c;无论是新能源汽车还是油车都能识别标记&#xff0c;该数据集一共包含4000张图片 数据集分割 4000总图像数 训练组 70&#xff05; 2800图片 有效集 20&#xff05; 800图片 测…...

解决VSCode无法识别相对路径的问题

前言&#xff1a; 近日在学习python文件操作时&#xff0c;发现使用VSCode作为编辑器时&#xff0c;文件的相对路径会出问题&#xff0c;报错“指定路径下找不到文件”&#xff0c;无法找到想要的文件。 知识点①&#xff1a;不同操作系统所使用的路径斜杠不同&#xff1a;Lin…...

OCR 技术在验证码识别中的应用

OCR 技术在验证码识别中的应用 一、验证码识别的背景与挑战二、OCR 技术简介三、验证码识别的环境搭建四、使用 OCR 进行验证码识别的方法五、DdddOcr 子项在验证码识别中的应用六、验证码识别的应用场景与注意事项七、总结 在当今数字化时代&#xff0c;验证码作为一种安全验证…...

恶补英语初级第21天,《讨论天气变化》

对话 What’s the weather like in spring? It’s often windy in March. It’s always warm in April and May, but it rains sometimes. What’s it like in summer? It’s always hot in June, July and August. The sun shines every day. Is it cold or warm in autumn?…...

牛客网刷题SQL--高级查询

目录 SQL16--查找GPA最高值 描述 示例1 答案 其他方法&#xff1a; SQL17--计算男生人数以及平均GPA 描述 示例1 答案 SQL18--分组计算练习题 描述 示例1 答案 SQL19--分组过滤练习题 描述 示例1 答案 SQL20--分组排序练习题 描述 示例1 答案 SQL16--查找GP…...

用ffmpeg将MP4视频转换为m3u8格式

原文网址&#xff1a;用ffmpeg将MP4视频转换为m3u8格式_IT利刃出鞘的博客-CSDN博客 简介 说明 本文介绍如何使用ffmpeg将MP4视频转换为m3u8格式。 什么是m3u8 M3U8视频格式是一种M3U&#xff0c;只是它的编码格式是UTF-8格式。M3U用Latin-1字符集编码。M3U8格式特点是带有…...

【Qt】qt基础

目录 一、使用Qt Creator创建qt项目 二、项目文件解析 三、Qt中创建图形化界面的程序的两种方法 四、对象树 一、使用Qt Creator创建qt项目 1.选择项目模板 选中第一类模板Application(Qt应用程序&#xff0c;包含普通窗体程序和QtQuick程序)&#xff0c; 然后选中右侧的第…...

VLC还是SmartPlayer?Windows平台RTSP播放器低延迟探讨

技术背景 好多开发者在用过大牛直播SDK的RTSP播放器后&#xff0c;都希望我们也分享下&#xff0c;如何在Windows平台实现低延迟的RTSP播放&#xff1f;低延迟的RTSP播放器&#xff0c;说起来容易做起来难&#xff0c;下面&#xff0c;我们从以下维度做个探讨&#xff1a; 播…...

极验决策引擎如何凭借独特优势,弯道超车传统风控?

前言 市场上的规则决策引擎产品众多&#xff0c;但大多局限于IP、设备、账号等层面&#xff0c;提供的是现成的风控标签和规则。然而&#xff0c;真正的风控&#xff0c;需要的不仅仅是标签和规则。 极验的业务规则决策引擎与众不同&#xff0c;这款决策引擎以界面流程编排为…...

Spring Boot集成Knife4j文档工具

Knife4j 搭建 Knife4j环境的的搭建和Swagger一样都比较简单&#xff0c;只需要极简的配置即可。 maven依赖 我使用的是较高版本的基于openapi规范的依赖包&#xff0c;OpenAPI2(Swagger)规范是Knife4j之前一直提供支持的版本&#xff0c;底层依赖框架为Springfox。 此次在4…...

html|本地实现浏览器m3u8播放器,告别网络视频卡顿

前言 网络上经常是以m3u8文件传输视频流的 &#xff0c;但是有时网络慢往往导致视频播放卡顿。于是我在想能不能先下载然后再播放呢&#xff1f;于是尝试下载然后实现本地播放m3u8视频。 正文 1.找到网络视频流的m3u8连接 一般在浏览器按F12就可以看到有请求视频流的连接。 …...