Linux:多线程(三.POSIX信号量、生产消费模型、线程池)

目录

1. 生产者消费者模型

1.1 阻塞队列(BlockingQueue)

1.2 一个实际应用的例子

2. POSIX信号量

2.1 引入

2.2 回顾加深理解信号量

2.3 信号量的操作接口

3. 基于循环队列的生产消费模型

3.1 循环队列

3.2 整个项目

4. 线程池

4.1 概念

4.2 线程池实现

1. 生产者消费者模型

超市(交易场所):

  • 定义:超市是数据“交易”的场所,即共享资源或临界资源的存储空间(也可以叫缓冲区)。在多线程编程中,这通常是一个数据结构(如队列、缓冲区等),用于临时存储数据,供生产者和消费者线程进行访问。

一般我们使用阻塞队列作为缓冲区

  • 功能:作为生产者和消费者之间数据传递的桥梁。生产者线程在此处添加(生产)数据,消费者线程在此处取走(消费)数据。

生产者(Producer):

  • 定义:生产者线程负责生成数据并将其放入超市(共享资源)中。

  • 并发度:生产者线程可以并发地运行,以提高数据的生成速度。但需要注意同步和互斥问题,以避免多个生产者同时写入数据导致的冲突。

  • 生产者之间都是互斥的:不能多个生产者同时都在往共享资源里面写

消费者(Consumer):

  • 定义:消费者线程负责从超市(共享资源)中取出数据并进行处理。

  • 并发度:消费者线程也可以并发地运行,以提高数据的处理速度。同样需要注意同步和互斥问题

  • 消费者之间都是互斥的:不能多个消费者同时都在从共享资源里面拿数据

3种关系:

生产者 vs 生产者 — 互斥

多个生产者线程可能同时试图向共享缓冲区(如队列或数组)中写入数据。为了防止数据竞争和不一致,我们需要使用互斥机制来确保同一时间只有一个生产者线程能够访问共享资源。

互斥通常通过互斥锁(Mutex)来实现。当一个生产者线程获得互斥锁时,其他生产者线程将被阻塞,直到锁被释放。这样,每个生产者线程在写入缓冲区时都能独占资源,从而避免了数据竞争。

消费者 vs 消费者 — 互斥

多个消费者线程可能同时试图从共享缓冲区中读取数据。为了确保数据的正确性和一致性,我们同样需要使用互斥机制来防止多个消费者线程同时访问缓冲区。

互斥锁在这里同样起到关键作用。当一个消费者线程获得互斥锁时,其他消费者线程将被阻塞,直到锁被释放。这样,每个消费者线程在读取缓冲区时都能独占资源,避免了潜在的冲突和不一致。

生产者 vs 消费者 — 互斥 && 同步

生产者线程和消费者线程需要共享一个缓冲区。这要求我们使用互斥机制来确保同一时间只有一个线程(生产者或消费者)能够访问缓冲区,以避免数据竞争和不一致。

但是,仅仅互斥是不够的。我们还需要使用同步机制来确保生产者和消费者之间的协调。例如,当缓冲区为空时,消费者线程应该被阻塞,直到生产者线程向其中添加了数据。同样地,当缓冲区满时,生产者线程也应该被阻塞,直到消费者线程从中取走了数据。

同步通常通过条件变量(Condition Variables)来实现。生产者线程在添加数据到缓冲区后,会向条件变量发送信号(signal),以唤醒等待的消费者线程。类似地,消费者线程在取走数据后,也会向条件变量发送信号,以唤醒等待的生产者线程。通过这种方式,生产者和消费者线程能够协调地工作,确保缓冲区的有效使用和数据的一致性。

优点:

  • 解耦:由于引入了一个缓冲区作为中介,生产者和消费者之间并不直接相互调用,从而降低了它们之间的耦合度。这使得生产者和消费者的代码发生变化时,不会对对方产生直接影响,提高了系统的灵活性和可维护性。

  • 支持并发:生产者和消费者是两个独立的并发体,它们之间通过缓冲区进行通信。生产者只需将数据放入缓冲区,就可以继续生产下一个数据;消费者只需从缓冲区中取出数据,就可以继续处理。这种并发处理的方式可以避免因生产者和消费者速度不匹配而导致的阻塞问题

  • 支持忙闲不均:在生产者和消费者模型中,生产者和消费者的速度可以不相同。当生产者生产数据的速度过快,而消费者处理数据的速度较慢时,未处理的数据可以暂时存储在缓冲区中,等待消费者处理。这种机制可以平衡生产者和消费者之间的速度差异,避免资源的浪费和瓶颈的产生。

1.1 阻塞队列(BlockingQueue)

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。

其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

BlockQueue.hpp 

#include<iostream>
#include<pthread.h>
#include<queue>using namespace std;template<class T>
class BlockQueue
{
public://初始化BlockQueue(int max=20):_max(max){pthread_cond_init(&_cond1,nullptr);pthread_cond_init(&_cond2,nullptr);pthread_mutex_init(&_mutex,nullptr);}//头删,并且返回这个数,生产者使用的接口T pop(){pthread_mutex_lock(&_mutex);//加锁while(_q.size()==0)//如果队列中没有数据,则让线程等待{pthread_cond_wait(&_cond1,&_mutex);}T data=_q.front();_q.pop();pthread_cond_signal(&_cond2);//唤醒正在等待的尾插线程,可以插入了pthread_mutex_unlock(&_mutex);//解锁return data;}//尾插,消费者使用的接口void push(T& data){pthread_mutex_lock(&_mutex);//加锁while(_q.size()==_max)//如果队列中数据满了,则让线程等待{pthread_cond_wait(&_cond2,&_mutex);}_q.push(data);pthread_cond_signal(&_cond1);//唤醒正在等待的头删线程,可以删除了pthread_mutex_unlock(&_mutex);//解锁}//销毁~BlockQueue(){pthread_cond_destroy(&_cond1);pthread_cond_destroy(&_cond2);pthread_mutex_destroy(&_mutex);}private:pthread_cond_t _cond1;//头删条件变量pthread_cond_t _cond2;//尾插条件变量pthread_mutex_t _mutex;//定义锁queue<T> _q;//定义队列int _max;//队列的空间大小
};

1.2 一个实际应用的例子

  • BlockQueue.hpp:封装的阻塞队列

  • text.cc:测试代码

  • Task.hpp:任务类(这里只是进行一个加法)

Task.hpp

#include<string>std::string oper="+-*/%";class Task
{
public://初始化Task(int x,int y,char oper):_x(x),_y(y),_oper(oper),_result(0),_correct(0){}//运行int run(){switch (_oper){case '+':_result=_x+_y;break;case '-':_result=_x-_y;break;case '*':_result=_x*_y;break;case '/':if(_y==0) _correct=1;else _result=_x/_y;break;case '%':if(_y==0) _correct=1;else _result=_x%_y;break;default:_correct=2;break;}}//消费者拿的任务std::string GetResult(){std::string rs=std::to_string(_x);rs+=_oper;rs+=std::to_string(_y);rs+="=";rs+=std::to_string(_result);rs+="[";rs+=std::to_string(_correct);rs+="]";return rs;}//生产者生产的任务std::string GetTask(){std::string r = std::to_string(_x);r += _oper;r += std::to_string(_y);r += "=?";return r;}~Task(){}private:int _x;        int _y;int _result;      //结果char _oper;       //运算符int _correct;     //值是否正确
};

text.cc

#include<iostream>
#include"BlockQueue.hpp"
#include<pthread.h>
#include<ctime>
#include<unistd.h>
#include"Task.hpp"//消费者
void* conmuser(void* args)
{BlockQueue<Task>* td=static_cast<BlockQueue<Task>*>(args);while(true){Task t=td->pop();t.run();cout<<"conmuser get a task:"<<t.GetResult()<<endl;sleep(1);}
}//生产者
void* producer(void* args)
{int len=oper.size();srand(time(nullptr));BlockQueue<Task>* td=static_cast<BlockQueue<Task>*>(args);while(true){int data1=rand()%5;usleep(100);int data2=rand()%10;char op=oper[rand()%len];Task t(data1,data2,op);td->push(t);cout<<"producer a task:"<<t.GetTask()<<endl;sleep(1);}
}int main()
{BlockQueue<Task>* q=new BlockQueue<Task>;//创建队列pthread_t c,p;//定义生产者和消费者pthread_create(&c,nullptr,producer,(void*)q);//创建生产者的线程pthread_create(&c,nullptr,conmuser,(void*)q);//创建消费者的线程//回收线程pthread_join(c,nullptr);pthread_join(p,nullptr);//销毁队列delete q;return 0;
}

2. POSIX信号量

2.1 引入

上次我们使用了阻塞队列的生产消费模型,在先前的生产者-消费者模型代码中,当一个线程想要操作临界资源时,必须确保临界资源处于满足条件的状态才能进行修改;否则无法修改。例如,在push接口中,当队列已满时,临界资源处于条件不可用的状态,无法继续进行push操作。此时,线程应该进入条件变量队列cond中等待。如果队列未满,即临界资源条件已准备好,那么可以继续push,调用队列_qpush接口。

观察代码可以看到,在判断临界资源是否就绪之前,必须先获取锁,因为判断临界资源实质上就是对临界资源的访问,而访问临界资源自然需要加锁以保护。因此,代码通常会先获取锁,然后手动检查临界资源的就绪状态,根据状态判断是等待还是直接操作临界资源。

但是如果事先知道临界资源的状态是否就绪,则无需一上来就加锁。一旦提前知道临界资源的就绪状态,便不再需要手动检查资源状态。在这种情况下,若有一个计数器来表示临界资源中小块资源的数量(如队列中每个空间),线程在访问临界资源前会先请求该计数器。若计数器大于0,则表明队列中有空余位置,可以直接向队列push数据;若计数器等于0,则说明队列已满,不能继续push数据,应该阻塞等待,直至计数器再次大于0,方可继续向队列push数据。

void push(T& data){pthread_mutex_lock(&_mutex);//加锁while(_q.size()==_max)//如果队列中数据满了,则生产者等待{pthread_cond_wait(&_cond2,&_mutex);}_q.push(data);if(_q.size()>0){pthread_cond_signal(&_cond1);//通知消费者可以消费了}pthread_mutex_unlock(&_mutex);//解锁}

2.2 回顾加深理解信号量

信号量是一种用于进程间通信和同步的机制。它本质上是一个计数器,用于衡量系统中的资源可用数量。通过信号量,可以实现对临界资源的访问控制,确保多个进程或线程能够安全地共享资源而不发生冲突。

在访问临界资源之前,程序可以通过申请信号量来获取对资源的访问权限。如果信号量的值大于0,表示资源可用,程序可以继续访问资源;如果信号量的值等于0,表示资源已被占用,程序需要等待,直到资源可用为止。

信号量并不仅仅是简单的计数器,它是通过原子操作实现的,确保信号量的操作是线程安全的。常用的信号量操作包括P操作(等待操作)和V操作(释放操作),也称为PV操作。P操作会将信号量的值减1,用于占用资源;V操作会将信号量的值加1,用于释放资源。

通过合理地使用信号量和PV操作,可以实现多线程或多进程之间的同步和互斥,避免资源竞争和死锁等并发问题。信号量是操作系统中重要的同步工具,广泛应用于进程间通信、资源管理、线程同步等场景。

system信号量和POSIX信号量都是用于进程间通信和同步的机制,但它们之间存在一些区别。

1. 系统信号量:

  • 系统信号量是Linux中的一种系统调用,用于进程间通信和同步。

  • 系统信号量是以系统级资源的形式存在,可以跨越进程边界,不仅可以用于线程之间的同步,也可以用于进程之间的同步。

  • 系统信号量是一个全局的计数器,可以通过系统调用函数来创建、初始化、P操作(等待操作)和V操作(释放操作)等。

  • 系统信号量的操作是通过系统调用函数来实现的,如semget、semop等。

2. POSIX信号量:

  • POSIX信号量是基于POSIX标准的一种同步机制

  • POSIX信号量与系统信号量类似,但是在接口和使用上有些许差异。

  • POSIX信号量允许用于进程间通信和线程间同步。

  • POSIX信号量通过调用相关的POSIX函数来创建、初始化、等待和释放,如sem_open、sem_wait、sem_post等。

系统信号量是Linux系统提供的一种进程间通信和同步机制,而POSIX信号量是基于POSIX标准的一种同步机制,二者都可以实现进程或线程间的同步和互斥操作

2.3 信号量的操作接口

初始化信号量:

使用sem_init函数可以初始化信号量,给定的value值会成为信号量的初始值。如果信号量是线程间共享的,可以被多个线程同时使用;如果是进程间共享的,可以被多个进程使用

#include <semaphore.h>//下面的函数都这此头文件int sem_init(sem_t *sem, int pshared, unsigned int value);
  • sem: 指向要初始化的信号量的指针(我们使用sem_t 类型直接定义)

  • pshared: 0 表示该信号量为线程间共享;非零值表示信号量为进程间共享

  • value: 信号量的初始值

  • 若成功,返回值为0,表示初始化信号量成功。

  • 若出现错误,返回值为-1,表示初始化失败,并设置errno来指示具体错误。(下面都是一样的)

销毁信号量:

使用sem_destroy函数可以销毁之前初始化的信号量。在销毁信号量之前,要确保所有线程或进程都已经停止使用该信号量。

int sem_trywait(sem_t *sem);
  • sem: 要销毁的信号量的指针

等待信号量:(P操作- -)

使用sem_wait函数可以等待信号量,即执行P操作。如果信号量的值大于0,则将其减1并立即返回,否则线程(或进程)会阻塞等待信号量变为大于0。

int sem_wait(sem_t *sem);
  • sem: 要等待的信号量的指针

发布信号量:(V操作++)

使用sem_post函数可以发布(释放)信号量,即执行V操作。对信号量执行V操作会将其值加1,并唤醒可能正在等待该信号量的线程(或进程)。

int sem_post(sem_t *sem);
  • sem: 要发布的信号量的指针

3. 基于循环队列的生产消费模型

3.1 循环队列

之前在阻塞队列里,我们不能实现出队列与入队列的同时进行。现在因为是循环队列我们使用了两个索引,而两个索引不同时可以同时进行出和入

当为空时或者满时,二者只能有一个开始执行。然后就不再相等了,也是能分开进行了

#include<iostream>
#include<string>
#include<vector>
#include<pthread.h>
#include<semaphore.h>using namespace std;template<class T>
class RoundQwueue
{
private://信号量--void P(sem_t& sub){sem_wait(&sub);}//信号量++void V(sem_t& add){sem_post(&add);}//加锁void Lock(pthread_mutex_t& mutex){pthread_mutex_lock(&mutex);}//解锁void Unlock(pthread_mutex_t& mutex){pthread_mutex_unlock(&mutex);}
public://初始化RoundQwueue(int max=20):_q(max),_max(max),_con(0),_pro(0){pthread_mutex_init(&_mutex1,nullptr);pthread_mutex_init(&_mutex2,nullptr);sem_init(&_sem1,0,max);sem_init(&_sem2,0,0);}//生产者生产数据void push(T& data){P(_sem1);           //生产者的信号量--,也就是可放数据的空间减一Lock(_mutex1);_q[_pro++]=data;      //将数据添加到队列中//_pro++;_pro%=_q.size();    //保证队列的循环,使_pro的下标不会超过队列的最大空间值Unlock(_mutex1);V(_sem2);           //消费者的信号量++,也就是可拿数据的空间加一}//消费者拿数据void pop(T& data){P(_sem2);           //消费者的信号量--,也就是可拿数据的空间减一Lock(_mutex2);data=_q[_con];      //将队列中的数据往外拿_con++;_con%=_q.size();    //保证队列的循环,使_con的下标不会超过队列的最大空间值Unlock(_mutex2);V(_sem1);           //生产者的信号量++,也就是可放数据的空间加一}//回收~RoundQwueue(){pthread_mutex_destroy(&_mutex1);pthread_mutex_destroy(&_mutex2);sem_destroy(&_sem1);sem_destroy(&_sem2);}private:vector<T> _q;              //这个一定要初始化int _max;                  //队列空间的最大值int _con;                  //消费者的下标int _pro;                  //生产者的下标pthread_mutex_t _mutex1;   //生产者的锁pthread_mutex_t _mutex2;   //消费者的锁sem_t _sem1;               //生产者信号量,一开始为队列的最大值sem_t _sem2;               //消费者信号量,一开始为0
};

3.2 整个项目

  • RingQueue.hpp:封装的循环队列

  • text.cc:程序的主体

  • Task.hpp:任务类(这里只是一个function包装器)

Task.hpp

#include<string>std::string oper="+-*/%";class Task
{
public://初始化Task(){}Task(int x,int y,char oper):_x(x),_y(y),_oper(oper),_result(0),_correct(0){}//运行void run(){switch (_oper){case '+':_result=_x+_y;break;case '-':_result=_x-_y;break;case '*':_result=_x*_y;break;case '/':if(_y==0) _correct=1;else _result=_x/_y;break;case '%':if(_y==0) _correct=1;else _result=_x%_y;break;default:_correct=2;break;}}//消费者拿的任务std::string GetResult(){std::string rs=std::to_string(_x);rs+=_oper;rs+=std::to_string(_y);rs+="=";rs+=std::to_string(_result);rs+="[";rs+=std::to_string(_correct);rs+="]";return rs;}//生产者生产的任务std::string GetTask(){std::string r = std::to_string(_x);r += _oper;r += std::to_string(_y);r += "=?";return r;}~Task(){}private:int _x;        int _y;int _result;      //结果char _oper;       //运算符int _correct;     //值是否正确
};

text.cc

#include<iostream>
#include"RoundQueue.hpp"
#include"Task.hpp"
#include<pthread.h>
#include<ctime>
#include<unistd.h>using namespace  std;//生产者
void* producer(void* args)
{srand(time(nullptr));int len=oper.size();RoundQwueue<Task>* td=static_cast<RoundQwueue<Task>*>(args);while(true){int data1=rand()%10;usleep(100);int data2=rand()%5;char op=oper[rand()%len];Task t(data1,data2,op);td->push(t);cout<<"producer give a task:"<<t.GetTask()<<endl;sleep(1);}
}//消费者
void* consumer(void* args)
{RoundQwueue<Task>* td=static_cast<RoundQwueue<Task>*>(args);while(true){Task t;td->pop(t);t.run();cout<<"consumer get a task:"<<t.GetResult()<<endl;}
}int main()
{RoundQwueue<Task>* tr=new RoundQwueue<Task>;//创造队列pthread_t tid1;pthread_t tid2;pthread_create(&tid1,nullptr,producer,(void*)tr);pthread_create(&tid2,nullptr,consumer,(void*)tr);pthread_join(tid1,nullptr);pthread_join(tid2,nullptr);delete tr;return 0;
}

4. 线程池

4.1 概念

  线程池:见名知义,就是多个线程构成的集合。其中线程的个数是确定的,并不是固定的

  为什么要有线程池?

  • 如果每次都只创建一个线程,首先当用户请求过多时,每次都需要创建一个线程,创建线程需要时间和调度开销,这样会影响缓存的局部性和整体的性能。其次,如果无上限一直创建线程,还会导致CPU的过分调度。

  • 线程池已经创建好了一定数量的线程,等待着分配任务,这样避免了处理任务时的线程创建和销毁。线程池里线程个数确定,能够保证内核的充分利用,还能防止过分调度。

  • 线程池中可用线程数量取决于可用额并发处理器,处理器内核,内存,网络socket等的数量。

线程池的应用场景:

  • 需要大量的线程来完成任务,且完成人物的时间比较短。比如:WEB服务器完成网页请求这样的任务,因为当个任务小,并且任务量巨大,你可以想象一个热门网站的请求次数。但是对于长时间的任务,线程池的优先就不明显了。比如:一个Telnet连接请求,因为Telnet会话时间比线程创建时间大多了。

  • 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。

  • 接收突发性的大量请求,但是不至于使服务器因此产生大量线程应用。突发性大量客户请求,在没有线程池的情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量的线程可能使内存到达极限,出现错误。

4.2 线程池实现

线程池实际也是一个生产者消费者模型,接收任务,往任务队列中放任务的是生产者,从任务队列中拿任务并执行的是消费者。

 主线程是生产者,用来接收任务和放任务。

Task.hpp

#include<string>std::string oper="+-*/%";class Task
{
public://初始化Task(int x,int y,char oper):_x(x),_y(y),_oper(oper),_result(0),_correct(0){}//运行int run(){switch (_oper){case '+':_result=_x+_y;break;case '-':_result=_x-_y;break;case '*':_result=_x*_y;break;case '/':if(_y==0) _correct=1;else _result=_x/_y;break;case '%':if(_y==0) _correct=1;else _result=_x%_y;break;default:_correct=2;break;}}//消费者拿的任务std::string GetResult(){std::string rs=std::to_string(_x);rs+=_oper;rs+=std::to_string(_y);rs+="=";rs+=std::to_string(_result);rs+="[";rs+=std::to_string(_correct);rs+="]";return rs;}//生产者生产的任务std::string GetTask(){std::string r = std::to_string(_x);r += _oper;r += std::to_string(_y);r += "=?";return r;}~Task(){}private:int _x;        int _y;int _result;      //结果char _oper;       //运算符int _correct;     //值是否正确
};

threadpool.hpp

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <queue>using namespace std;class threadname
{
public:string _name;       //线程的名字pthread_t tid;      //线程的tid
};template <class T>
class Threadpool
{
private://加锁void Lock(){pthread_mutex_lock(&_mutex);}//解锁void Unlock(){pthread_mutex_unlock(&_mutex);}//线程等待void Wait(){pthread_cond_wait(&_cond, &_mutex);}//唤醒线程void signal(){pthread_cond_signal(&_cond);}//输出线程的名字string Getthreadname(pthread_t tid){for (auto ti : _v){if (ti.tid == tid){return ti._name;}}return nullptr;}public://构造函数Threadpool(int max = 10): _max(max), _v(max)                        //初始化数组和_max{pthread_mutex_init(&_mutex, nullptr);       //初始化锁pthread_cond_init(&_cond, nullptr);         //初始化条件变量}static void *hander(void *args){Threadpool<T> *td = static_cast<Threadpool<T> *>(args);string name = td->Getthreadname(pthread_self());     //通过tid,将该线程的名字从数组中拿出while (true){td->Lock();while (td->_q.empty())       //队列如果为空,则进行线程等待{td->Wait();}T t = td->pop();             //将任务从队列中拿出td->Unlock();t.run();cout << name << " a result:" << t.GetResult() << endl;}}//创造线程void Create(){int num = _v.size();for (int i = 0; i < num; i++){_v[i]._name = "thread_" + to_string(i + 1);   //将线程的名字存入数组中pthread_create(&(_v[i].tid), nullptr, hander, (void *)this);cout<<_v[i]._name<<endl;                      //打印创造的线程名字}}//将任务添加到队列中void push(T &data){Lock();_q.push(data);cout << "thread produser a task:" << data.GetTask() << endl;signal();Unlock();}//从队列中拿出任务T pop(){T t = _q.front();_q.pop();return t;}//析构函数~Threadpool(){pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}private:vector<threadname> _v;    //定义一个数组,用来存放线程的名字和tidqueue<T> _q;              //队列,用来存放任务int _max;	              // 数组的大小pthread_mutex_t _mutex;   // 锁pthread_cond_t _cond;     
};

text.cc

#include "threadpool.hpp"
#include "Task.hpp"
#include <ctime>using namespace std;int main()
{Threadpool<Task> *tp = new Threadpool<Task>;tp->Create();srand(time(nullptr));int len = oper.size();while (true){int x = rand() % 10 + 1;usleep(100);int y = rand() % 20 + 1;char op = oper[rand() % len];Task t(x, y, op);tp->push(t);sleep(1);}delete tp;return 0;
}

结果:

线程池是一开始,我们就创建好n个线程,然后将任务添加到线程池中,由之前创建好的线程去挣抢完成任务

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/897494.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

关于前后端整合和打包成exe文件的个人的总结和思考

前言 感觉有很多东西&#xff0c;不知道写什么&#xff0c;随便写点吧。 正文 前后端合并 就不说怎么开发的&#xff0c;就说点个人感觉重要的东西。 前端用ReactViteaxios随便写一个demo&#xff0c;用于CRUD。 后端用Django REST Framework。 设置前端打包 import { …

Android15 Camera框架中的StatusTracker

StatusTracker介绍 StatusTracker是Android15 Camera框架中用来协调Camera3各组件之间状态转换的类。 StatusTracker线程名&#xff1a;std::string("C3Dev-") mId "-Status" Camera3 StatusTracker工作原理 StatusTracker实现批处理&#xff08;状态…

利用OpenResty拦截SQL注入

需求 客户的一个老项目被相关部门检测不安全&#xff0c;报告为sql注入。不想改代码&#xff0c;改项目&#xff0c;所以想到利用nginx去做一些数据校验拦截。也就是前端传一些用于sql注入的非法字符或者数据库的关键字这些&#xff0c;都给拦截掉&#xff0c;从而实现拦截sql…

警惕AI神话破灭:深度解析大模型缺陷与禁用场景指南

摘要 当前AI大模型虽展现强大能力&#xff0c;但其本质缺陷可能引发系统性风险。本文从认知鸿沟、数据困境、伦理雷区、技术瓶颈四大维度剖析大模型局限性&#xff0c;揭示医疗诊断、法律决策等8类禁用场景&#xff0c;提出可信AI建设框架与用户防护策略。通过理论分析与实操案…

颠覆语言认知的革命!神经概率语言模型如何突破人类思维边界?

颠覆语言认知的革命&#xff01;神经概率语言模型如何突破人类思维边界&#xff1f; 一、传统模型的世纪困境&#xff1a;当n-gram遇上"月光族难题" 令人震惊的案例&#xff1a;2012年Google语音识别系统将 用户说&#xff1a;“我要还信用卡” 系统识别&#xff…

【Linux】详谈 基础I/O

目录 一、理解文件 狭义的理解&#xff1a; 广义理解&#xff1a; 文件操作的归类认知 系统角度 二、系统文件I/O 2.1 标志位的传递 系统级接口open ​编辑 open返回值 写入文件 读文件 三、文件描述符 3.1&#xff08;0 & 1 & 2&#xff09; 3.2 文件描…

超分之DeSRA

Desra: detect and delete the artifacts of gan-based real-world super-resolution models.DeSRA&#xff1a;检测并消除基于GAN的真实世界超分辨率模型中的伪影Xie L, Wang X, Chen X, et al.arXiv preprint arXiv:2307.02457, 2023. 摘要 背景&#xff1a; GAN-SR模型虽然…

Vue3 Pinia 符合直觉的Vue.js状态管理库

Pinia 符合直觉的Vue.js状态管理库 什么时候使用Pinia 当两个关系非常远的组件&#xff0c;要传递参数时使用Pinia组件的公共参数使用Pinia

Web Worker如何在本地使用

首先了解一下什么是Web Worker Web Worker 是一种在后台线程中运行 JavaScript 的机制&#xff0c;允许你在不阻塞主线程的情况下执行耗时的任务。这对于保持网页的响应性和流畅性非常重要&#xff0c;特别是在需要进行复杂计算或大量数据处理时。 主要特点 多线程&#xff1…

Javaweb后端文件上传@value注解

文件本地存储磁盘 阿里云oss准备工作 阿里云oss入门程序 要重启一下idea&#xff0c;上面有cmd 阿里云oss案例集成 优化 用spring中的value注解

MAC-禁止百度网盘自动升级更新

通过终端禁用更新服务(推荐)​ 此方法直接移除百度网盘的自动更新组件,无需修改系统文件。 ​步骤: ​1.关闭百度网盘后台进程 按下 Command + Space → 输入「活动监视器」→ 搜索 BaiduNetdisk 或 UpdateAgent → 结束相关进程。 ​2.删除自动更新配置文件 打开终端…

数据结构:有序表的插入

本文是我编写的针对计算机专业考研复习《数据结构》所用资料内容选刊。主要目的在于向复习这门课程的同学说明&#xff0c;此类问题不仅仅使用顺序表&#xff0c;也可以使用链表。并且&#xff0c;在复习中&#xff0c;两种数据结构都要掌握。 若线性表中的数据元素相互之间可以…

DeepSeek大语言模型下几个常用术语

昨天刷B站看到复旦赵斌老师说的一句话“科幻电影里在人脑中植入芯片或许在当下无法实现&#xff0c;但当下可以借助AI人工智能实现人类第二脑”&#xff08;大概是这个意思&#xff09; &#x1f49e;更多内容&#xff0c;可关注公众号“ 一名程序媛 ”&#xff0c;我们一起从 …

Html5学习教程,从入门到精通, HTML5超链接应用的详细语法知识点和案例代码(18)

HTML5超链接应用的详细语法知识点和案例代码 超链接&#xff08;Hyperlink&#xff09;&#xff0c;也称为跃点链接&#xff0c;是互联网和文档编辑中的一种重要概念。 超链接的定义 超链接是指从一个网页指向一个目标的连接关系&#xff0c;这个目标可以是另一个网页&#…

MYSQL之创建数据库和表

创建数据库db_ck &#xff08;下面的创建是最好的创建方法&#xff0c;如果数据库存在也不会报错&#xff0c;并且指定使用utf8mb4&#xff09; show databases命令可以查看所有的数据库名&#xff0c;可以找到刚刚创建的db_ck数据库 使用该数据库时&#xff0c;发现里面没有…

[pytest] 配置

这里写目录标题 PytestInitRun3. 根据命令行选项将不同的值传递给测试函数 Report1. 向测试报告标题添加信息2. 分析测试持续时间 pytest --durations33. 增量测试 - 测试步骤--junitxml{report}.xml1. testsuite1.1 在测试套件级别添加属性节点 record_testsuite_property 2. …

Manus联创澄清:我们并未使用MCP技术

摘要 近日&#xff0c;Manus联创针对外界关于其产品可能涉及“沙盒越狱”的疑问进行了正式回应。公司明确表示并未使用Anthropic的MCP&#xff08;模型上下文协议&#xff09;技术&#xff0c;并强调MCP是一个旨在标准化应用程序与大型语言模型&#xff08;LLM&#xff09;之间…

初始化E9环境,安装Sqlserver数据库

title: 初始化E9环境,安装Sqlserver数据库 date: 2025-03-10 19:27:19 tags: E9SqlServer初始化E9环境,安装Sqlserver数据库 安装E9本地环境安装Sql server 数据库1、检查SQL Server服务是否开启2、检查SQL Server网络网络配置是否开启创建一个ecology数据库点击初始化数据库…

推荐一个基于Koin, Ktor Paging等组件的KMM Compose Multiplatform项目

Kotlin Multiplatform Mobile&#xff08;KMM&#xff09;已经从一个雄心勃勃的想法发展成为一个稳定而强大的框架&#xff0c;为开发人员提供了在多个平台上无缝共享代码的能力。通过最近的稳定版本里程碑&#xff0c;KMM已成为跨平台开发领域的改变者。 环境设置 带有Kotli…

在WSL2-Ubuntu中安装CUDA12.8、cuDNN、Anaconda、Pytorch并验证安装

#记录工作 提示&#xff1a;整个过程最好先开启系统代理&#xff0c;也可以用镜像源&#xff0c;确保有官方发布的最新特性和官方库的完整和兼容性支持。 期间下载会特别慢&#xff0c;需要在系统上先开启代理&#xff0c;然后WSL设置里打开网络模式“Mirrored”,以设置WSL自动…