多线程总结(线程池 线程安全 常见锁)

news2024/11/23 22:02:00

  本篇文章主要是对线程池进行详解。同时引出了单例模式的线程池,也对线程安全问题进行了解释。其中包含了智能指针、STL容器、饿汉模式的线程安全。也对常见的锁:悲观锁(Pessimistic Locking)乐观锁(Optimistic Locking)互斥锁(Mutex Lock)读写锁(Read-Write Lock)自旋锁(Spin Lock)条件变量(Condition Variable)进行了讲解。重点对读写锁进行了讲解。希望本篇文章会对你有所帮助。

文章目录

一、线程池

1、1 什么是线程池

1、2 为什么要有线程池

1、3 线程池demo代码

1、3、1 设计思路

1、3、2 demo代码

1、4 懒汉方式的线程池 (线程安全版本)

二、线程安全

2、1 STL容器线程安全问题

2、2 智能指针线程安全问题

三、常见的几种锁

四、读者学者问题(读写锁)

4、1 简单理解读者学者问题

4、2 读写锁常用接口介绍

4、3 用互斥锁实现读写锁


🙋‍♂️ 作者:@Ggggggtm 🙋‍♂️

👀 专栏:Linux从入门到精通  👀

💥 标题:线程池、线程安全与常见锁问题💥

 ❣️ 寄语:与其忙着诉苦,不如低头赶路,奋路前行,终将遇到一番好风景 

一、线程池

1、1 什么是线程池

  线程池是一种线程使用模式。在应用程序中,创建和销毁线程会带来性能上的开销,因此线程池的出现可以减少这种开销并提高程序的效率。

  线程池内部维护了一个线程队列,其中包含了一定数量的可重复使用的线程。当有任务需要执行时,可以从线程池中获取一个空闲线程来执行任务,而不是每次都重新创建一个线程。任务执行完毕后,线程将被返回给线程池,以备下次任务执行

1、2 为什么要有线程池

  线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

1、3 线程池demo代码

1、3、1 设计思路

  线程池的情况较为特殊,是一开始就创建一定数量的线程。当有任务时,所有待命的线程去竞争这个任务。与之前不同的是,在处理任务时才创建线程。

  其设计思路如下:

  1. 线程池的构造函数ThreadPool(int thread_num)可接受一个参数thread_num,用于指定线程池中线程的数量,默认为THREAD_NUM。在构造函数中,通过循环创建thread_num个Thread对象,并将它们保存在_threads数组中。

  2. 线程的创建与等待进行了封装,封装成了一个Thread类

  3. 每个Thread对象都拥有一个线程编号和静态的routine函数。routine函数是线程执行的入口点,它接受一个ThreadData参数,其中包含了当前线程的相关数据。在routine函数中,使用while(true)循环来不断从任务队列中获取任务,并执行任务的运算操作。获取任务的过程中,需要先获取互斥锁mutex,然后使用条件变量cond进行等待或唤醒

  4. ThreadPool类提供了一系列辅助函数,例如getMutex()用于获取互斥锁mutex的指针,isEmpty()用于判断任务队列是否为空,waitCond()用于进入等待状态,getTask()用于获取队列中的任务。目的就是为了在routine函数中可以轻松获取相关参数

  5. 在run()函数中,通过遍历_threads数组,依次启动每个Thread对象,使它们开始执行任务。

  6. pushTask(const T& task)函数用于将任务入队列。在入队过程中,首先获取互斥锁mutex,然后将任务task添加到_task_queue队列中,并通过pthread_cond_signal函数对条件变量cond进行信号通知,以唤醒等待的线程

  7. 析构函数~ThreadPool()用于销毁线程池。在析构函数中,首先遍历_threads数组,调用每个Thread对象的join()函数来等待线程的结束,并释放Thread对象的内存。最后,调用pthread_mutex_destroy函数和pthread_cond_destroy函数来销毁互斥锁mutex和条件变量cond。

1、3、2 demo代码

LockGuard.hpp(对互斥锁的封装)

#pragma once 

class Mutex
{
public:
    Mutex(pthread_mutex_t *mtx):pmtx_(mtx)
    {}
    void lock() 
    {
        pthread_mutex_lock(pmtx_);
    }
    void unlock()
    {
        pthread_mutex_unlock(pmtx_);
    }
    ~Mutex()
    {}
private:
    pthread_mutex_t *pmtx_;
};
 
// RAII风格的加锁方式
class LockGuard
{
public:
    LockGuard(pthread_mutex_t *mtx):mtx_(mtx)
    {
        mtx_.lock();
    }
    ~LockGuard()
    {
        mtx_.unlock();
    }
private:
    Mutex mtx_;
};

Task.hpp(所派发的任务)

#pragma once

typedef function<int(int, int)> fun_t;
 
class Task
{
 
public:
    Task(){}
    Task(int x, int y, fun_t func)
        :x_(x)
        , y_(y)
        , func_(func)
    {}
    int operator ()()
    {
        return func_(x_, y_);
    }
public:
    int x_;
    int y_;
    // int type;
    fun_t func_;
};

Thread.hpp(创建线程的封装)

#pragma once

// typedef std::function<void* (void*)> func_t; 

typedef void *(*func_t)(void *);	// 定义一个函数类型
// (要传递给)线程的信息
class ThreadData
{
public:
	void* _args;		// 线程参数
	std::string _name;	// 线程名称
};
// 线程类
class Thread
{
public:
	Thread(int num, func_t callback, void* args)
	: _func(callback)
	{
		char threadName[64];
		snprintf(threadName, sizeof(threadName), "Thread:[%d]", num);
		_name = threadName;
		
		_td._args = args;	// 给线程传递参数
		_td._name = _name;
	}
	~Thread()
	{}
	// 创建线程
	void start()
	{
		pthread_create(&_tid, nullptr, _func, (void*)&_td);
	}

	void join()
	{
		pthread_join(_tid, nullptr);
	}

	std::string name()
	{
		return _name;
	}

private:
	ThreadData _td;		// 要传递给线程的信息
	std::string _name;	// 线程名称
	pthread_t _tid;		// 线程ID
	func_t _func;		// 线程函数
};

ThreadPool.hpp(线程池)

#include<string>
#include <vector>
#include <queue>
#include <ctime>
#include <cstdlib>
#include <ctime>
#include <cstdlib>
#include <iostream>
#include <unistd.h>
#include <functional>

using namespace std;

#include "Thread.hpp"
#include "LockGuard.hpp"
#include "Task.hpp"

#define THREAD_NUM 5
template<class T>

class ThreadPool
{
public:
	pthread_mutex_t* getMutex()
	{
		return &mutex;
	}

	bool isEmpty()
	{
		return _task_queue.empty();
	}

	void waitCond()
	{
		pthread_cond_wait(&cond,&mutex);
	}

	T getTask()
	{
		T n=_task_queue.front();
		_task_queue.pop();
		return n;
	}
public:
	static void* routine(void* args)
	{
		ThreadData* td = (ThreadData*)args;
		ThreadPool<T>* tp = (ThreadPool<T>*)td->_args;
		while(true)
		{
			T task;
			{
				LockGuard lockguard(tp->getMutex());
				while(tp->isEmpty())
					tp->waitCond();
				
				task=tp->getTask();
			}
			cout<< "线程" << td->_name << "运算结果是 : " << task() << endl;
		}
	}

	ThreadPool(int thread_num = THREAD_NUM)
	: _num(thread_num)
	{
		for(int i = 1; i <= _num; i++)
		{
			// 参数列表对应着Thread的构造函数
			_threads.push_back(new Thread(i, routine, this));
		}

		pthread_mutex_init(&mutex,nullptr);
		pthread_cond_init(&cond,nullptr);
	}

	// 线程执行任务
	void run()
	{
		for(auto& it : _threads)
		{
			it->start();
			std::cout << "线程开始执行任务:"<<it->name() << std::endl;
		}
	}

	// void joins()
	// {
	// 	for(auto& it:_threads)
	// 	{
	// 		it->join();
	// 	}
	// }

	// 将任务入队列
	void pushTask(const T& task)
	{
		LockGuard lockguard(&mutex);
		_task_queue.push(task);
		pthread_cond_signal(&cond);
	}

	~ThreadPool()
	{
		for(auto& it : _threads)
		{
			it->join();
			delete it;
		}

		pthread_mutex_destroy(&mutex);
		pthread_cond_destroy(&cond);
	}
private:
	std::vector<Thread*> _threads;		// 保存线程的数组
	std::queue<T> _task_queue;			// 保存任务的队列
	int _num;							// 线程的个数
	pthread_mutex_t mutex;
	pthread_cond_t cond;
};

TestMain.cpp

int myAdd(int x, int y)
{
	return x + y;
}

int main()
{
    srand((uint64_t)time(nullptr) ^ 0x333 ^ getpid());
    ThreadPool<Task> *tp=new ThreadPool<Task>();

    tp->run();
    while(true)
    {
        int x=rand()%100+1;
        usleep(666);
        int y=rand()%88+1;
        Task t(x,y,myAdd);

        cout<<"制作任务完成 :"<< x << " + "<< y << " = ?"<<endl;
        tp->pushTask(t);

        sleep(1);
    }
    //td->joins();
    return 0;
}

1、4 懒汉方式的线程池 (线程安全版本)

  我们知道懒汉方式时单例模式中的一种。我们下面给出懒汉方式的线程池伪代码:

template<class T>
class ThreadPool
{
private:
    // 将构造函数私有化,当然还有其拷贝构造和赋值重载
	ThreadPool(int thread_num = THREAD_NUM)
	: _num(thread_num)
	{
		for(int i = 1; i <= _num; i++)
		{
			// 参数列表对应着Thread的构造函数
			_threads.push_back(new Thread(i, routine, this));
		}

		pthread_mutex_init(&mutex,nullptr);
		pthread_cond_init(&cond,nullptr);
	}
    
    // 提供获取线程池指针的函数
    static ThreadPool<T>* getThreadPtr()
	{

		if(nullptr==thread_ptr)
		{
			thread_ptr=new ThreadPool<T>();
		}
		return thread_ptr;
	}
private:
	std::vector<Thread*> _threads;		// 保存线程的数组
	std::queue<T> _task_queue;			// 保存任务的队列
	int _num;							// 线程的个数
	pthread_mutex_t mutex;
	pthread_cond_t cond;
	static ThreadPool<T>* thread_ptr;
}
template<class T>
ThreadPool<T>* ThreadPool<T>::thread_ptr=nullptr;

  我们在没有学习多线程之前看上述代码似乎并没有问题。实际上有多个执行流在执行时,就会出现问题。如果当前有多个线程同时想要申请线程池对象呢,可能就会不是单例了!!!所以我们也应该在获取线程池对象指针的函数中加锁进行保护。实际代码如下:

	static ThreadPool<T>* getThreadPtr()
	{
        //{
            //	LockGuard lockguard(&g_mutex);
		    //	if(nullptr==thread_ptr)
		    //	{
		    //		thread_ptr=new ThreadPool<T>();
		    //	}
        //}

		if(nullptr==thread_ptr)
		{
			LockGuard lockguard(&g_mutex);
			if(nullptr==thread_ptr)
			{
				thread_ptr=new ThreadPool<T>();
			}
		}
		return thread_ptr;
	}

  上述代码加锁加的很巧妙。我们知道单例模式只能实例出一个对象。所以我们先判断其是否为空,也就是是否已经实例出对象了。然后再申请锁。这样不但保护了申请对象的安全,同时也减少了申请锁的次数。

二、线程安全

2、1 STL容器线程安全问题

  STL中的容器是否是线程安全的呢?答案是:STL中的容器不是线程安全的

  原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响。而且对于不同的容器,加锁方式的不同,性能可能也不同(例如hash表的锁表和锁桶)。因此 STL 默认不是线程安全。如果需要在多线程环境下使用,往往需要调用者自行保证线程安全。

2、2 智能指针线程安全问题

  对于 unique_ptr, 由于只是在当前代码块范围内生效,因此不涉及线程安全问题

  对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题。但是标准库实现的时候考虑到了这个问题,,基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数。

三、常见的几种锁

  在多线程编程中,常见的几种锁包括悲观锁(Pessimistic Locking)​​​​​​​、乐观锁(Optimistic Locking)互斥锁(Mutex Lock)读写锁(Read-Write Lock)自旋锁(Spin Lock)条件变量(Condition Variable)。下面对它们进行详细解释:

  1. 悲观锁(Pessimistic Locking):

    • 悲观锁的基本思想是,对共享资源的访问持保守态度,认为并发操作可能会产生冲突,因此,在访问共享资源之前,先获取锁来确保独占访问。
    • 使用悲观锁时,当一个线程需要对共享资源进行读或写操作时,首先尝试获取锁。如果锁已被其他线程持有,则当前线程会被阻塞,直到锁被释放。
    • 悲观锁通常使用互斥锁(Mutex Lock)或读写锁(Read-Write Lock)等来实现。
  2. 乐观锁(Optimistic Locking):

    • 乐观锁的基本思想是,每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。
    • 使用乐观锁时,当一个线程需要对共享资源进行读或写操作时,先读取当前版本号或时间戳,并在执行操作前记录下来。然后,当要提交修改时,先检查版本号或时间戳是否与之前读取的一致,如果一致则提交成功,否则表示发生了冲突,需要进行回滚或重试操作。
    • 乐观锁通常使用版本号或时间戳等机制来实现,如数据库中的乐观并发控制。
  3. 互斥锁(Mutex Lock):

    • 互斥锁用于保护临界区,确保同一时间只有一个线程能够进入临界区进行操作,从而避免数据竞争。
    • 在获取互斥锁之前,如果锁已经被其他线程占用,则线程会被阻塞,并等待锁的释放;一旦获取到锁,线程可以进入临界区执行操作,执行完毕后释放锁供其他线程使用。
    • 互斥锁的实现可以是阻塞式的(Blocking Mutex),也可以是非阻塞式的(Non-blocking Mutex)。
  4. 读写锁(Read-Write Lock):

    • 读写锁用于在读多写少的情况下提高并发性能。它分为读共享模式和写独占模式
    • 读共享模式允许多个线程同时对共享资源进行读取操作,不互斥;而写独占模式则排斥地获取锁,一次只能有一个线程进行写入操作,且当前不能有读操作
    • 当有线程持有读锁时,其他线程可以继续持有读锁而不会被阻塞;但当有线程持有写锁时,其他读写线程都会被阻塞等待。
    • 读写锁允许多个线程同时读取共享资源,从而提高并发性能。
  5. 自旋锁(Spin Lock):

    • 自旋锁是一种忙等的锁机制,用于保护临界区,并在获取锁失败时自旋等待锁的释放。
    • 当一个线程尝试获取自旋锁时,如果锁已被其他线程占用,则该线程不会被阻塞,而是通过循环不断地检查锁是否被释放。
    • 自旋锁适用于临界区的锁定时间较短且线程竞争不激烈的情况下,避免了线程切换的开销,但也可能导致CPU资源的浪费。
    • 什么情况下使用自旋锁呢?决定因素就是等待临界资源就绪的时间。如果等待临界资源就绪时间过长,一直在循环检测不就是一种浪费吗!!!如果临界区的代码执行很快,那么忙等待所消耗的时间可能比线程挂起与唤醒的时间更短,从而提高了性能。其次,自旋锁适用于并发竞争较小的情况。因为自旋锁是通过忙等待来获取锁,如果并发竞争激烈,那么会导致大量的线程在忙等待,浪费了大量的CPU资源。
  6. 条件变量(Condition Variable):

    • 条件变量用于在线程之间进行等待和通知,用来解决生产者-消费者等经典同步问题。
    • 线程可以通过条件变量等待某个条件成立,在条件不满足时将自己放入等待队列,等待其他线程发出通知唤醒自己。
    • 条件变量通常与互斥锁一起使用,等待前需要先加锁,唤醒后也会自动解锁。
    • 在满足条件的情况下,其他线程可以发送信号或广播(signal/broadcast)来唤醒等待的线程。

  这些锁机制提供了不同的线程同步方式,应根据具体的多线程场景和需求选择合适的锁来保证并发操作的正确性和性能。

四、读者学者问题(读写锁)

4、1 简单理解读者学者问题

  在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。这也就是读者写者问题。

  读者写者问题就是基于读写锁来实现的。我们可结合下图理解读者写着的读写操作:

4、2 读写锁常用接口介绍

  常用的读写锁接口有以下几个:

  1. 初始化锁:int pthread_rwlock_init(pthread_rwlock_t *rwlock, const pthread_rwlockattr_t *attr); 该函数用于初始化一个读写锁对象,参数rwlock为要初始化的读写锁对象的指针,attr为锁属性,一般使用默认值NULL即可。

  2. 销毁锁:int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); 该函数用于销毁一个读写锁对象,释放相关资源。

  3. 加读锁:int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); 该函数用于对读写锁对象加读锁,如果有其他线程持有写锁,则当前线程会被阻塞,直到写锁释放。

  4. 加写锁:int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); 该函数用于对读写锁对象加写锁,如果有其他线程持有读锁或写锁,则当前线程会被阻塞,直到所有的读锁和写锁释放。

  5. 尝试加读锁:int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); 该函数尝试对读写锁对象加读锁,如果无法获取到锁(有其他线程持有写锁),则立即返回错误码。

  6. 尝试加写锁:int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); 该函数尝试对读写锁对象加写锁,如果无法获取到锁(有其他线程持有读锁或写锁),则立即返回错误码。

  7. 解锁:int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); 该函数用于释放读写锁对象的锁,如果是读锁,则允许其他线程继续获取读锁或写锁;如果是写锁,则允许其他线程获取读锁或写锁。

4、3 用互斥锁实现读写锁

  当我们了解到读写锁后,那么怎么用互斥锁来实现一个读写锁的功能呢? 大概思路就是我们用一个表变量来统计读者的数量(也就是来记录读锁的个数)。当读者为0时,才可进行写操作。读者之间不互斥。给出伪代码如下:

int cnt = 0;

pthread_mutex_t rd_count_mtx;
pthread_mutex_t wt_mtx;

void read()
{
    pthread_mutex_lock(&rd_count_mtx);
    cnt++;
    if(cnt == 1) // 表示已经有读者,且只加一次锁就可以
        pthread_mutex_lock(&wt_mtx);
    pthread_mutex_unlock(&rd_count_mtx);

    // 进行读操作
    // ......

    pthread_mutex_lock(&rd_count_mtx);
    cnt--;
    if(cnt == 0) // 表示已经没有读者,可以进行写操作
        pthread_mutex_unlock(&wt_mtx);
    pthread_mutex_unlock(&rd_count_mtx);
}

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

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

相关文章

使用GDIView排查GDI对象泄漏导致的程序UI界面绘制异常问题

目录 1、问题说明 2、初步分析 3、查看任务管理器&#xff0c;并使用GDIView工具分析 4、GDIView可能对Win10兼容性不好&#xff0c;显示的GDI对象个数不太准确 5、采用历史版本比对法&#xff0c;确定初次出现问题的时间点&#xff0c;并查看前一天的代码修改记录 6、将…

visual studio下载安装

一、官网下载 地址&#xff1a;https://visualstudio.microsoft.com/zh-hans/ 点击免费visual studio 二、安装 运行下载好的exe文件&#xff0c;自定义安装目录 三、选择需要的组件安装 只需要选择标记组件&#xff0c;然后点击安装 等待安装完成就行 四、重启电脑 安装完之后…

windows系统关闭软件开机自启的常用两种方法

win10中安装软件时经常会默认开机自启动&#xff0c;本文主要介绍两种关闭软件开机自启动方法。 方法1 通过任务管理器设置 1.在任务管理器中禁用开机自启动&#xff1a;打开任务管理器&#xff0c;右键已启动的软件&#xff0c;选择禁用。 方法2 通过windows服务控制开机自启…

SpringBoot——常用注解

Spring Web MVC与Spring Bean注解 Controller/RestController Controller是Component注解的一个延伸&#xff0c;Spring 会自动扫描并配置被该注解标注的类。此注解用于标注Spring MVC的控制器。 Controller RequestMapping("/api/v1") public class UserApiContr…

ssm+vue的OA办公管理系统(有报告)。Javaee项目,ssm vue前后端分离项目。

演示视频&#xff1a; ssmvue的OA办公管理系统&#xff08;有报告&#xff09;。Javaee项目&#xff0c;ssm vue前后端分离项目。 项目介绍&#xff1a; 采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&#xff09;三层体系结构&am…

【Java 集合】准备知识

目录 初识集合框架 什么是集合框架 包装器 1. 基本数据类型和对应的包装器 2. 装箱和拆箱 3. 自动装箱和拆箱 4. Integer 存储机制 5. 包装器的作用 泛型 1. 什么是泛型 2. 引出泛型 2.1 泛型语法 3. 泛型类的使用 4. Java泛型实现的机制 -- 擦除机制 5. 泛型的…

PHP变量底层原理

前言 PHP是解释型的语言&#xff0c;它的执行顺序主要会经过以下几步&#xff1a; 1. 进行词法分析 2. 进行语法分析 3. 通过zend编译器&#xff0c;编译成opcode 4. zend虚拟机执行opcode 我们在写PHP代码的时候就知道&#xff0c;PHP是弱语言类型&#xff0c;而PHP底层又是由…

亚马逊无线充UL62368报告办理 无线充UL2738+UL2056标准UL认证办理亚马逊类目审核

什么是UL测试报告和UL认证有什么区别&#xff1f; 美国是一个对安全要求非常严格的国家&#xff0c;美国本土的所有电子产品生产企业早在很多年前就要求有相关安规检测。而随着亚马逊在全球商业的战略地位不断提高&#xff0c;境外的电子设备通过亚马逊不断涌入美国市场&#…

幂级数和幂级数的和函数有什么关系?

幂级数和幂级数的和函数有什么关系&#xff1f; 本文例子引用自&#xff1a;80_1幂级数运算&#xff0c;逐项积分、求导【小元老师】高等数学&#xff0c;考研数学 求幂级数 ∑ n 1 ∞ 1 n x n \sum\limits_{n1}^{\infty}\frac{1}{n}x^n n1∑∞​n1​xn 的和函数 &#xff…

What are the differences between lsof and netstat on linux?

参考&#xff1a;https://stackoverflow.com/questions/49381124/what-are-the-differences-between-lsof-and-netstat-on-linux https://www.cnblogs.com/pc-boke/articles/10012112.html LSOF: List of Open Files. It lists all the open files belonging to all active pr…

C语言实现——简易通讯录

前言&#xff1a;小伙伴们又见面啦&#xff01;这几天通过我们对自定义数据类型的学习&#xff0c;我们已经掌握了如何同时对多种数据类型进行管理&#xff0c;那么今天这篇文章&#xff0c;我们就来干一件大事——实现简易的通讯录。 一.思路分析 先来想想通讯录有哪些功能&a…

Spring Boot:利用JPA进行数据库的查删

目录标题 DAO 、Service 、 Controller 层控制器文件示例代码-单个查找查找成功示例代码-列表查找查找成功示例代码-删除删除成功 DAO 、Service 、 Controller 层 DAO 层负责数据库访问&#xff0c;它封装了对数据库的访问操作&#xff0c;例如查询、插入、更新和删除等。 Q…

《向量数据库指南》——向量数据库Elasticsearch -> Milvus 2.x

Elasticsearch -> Milvus 2.x 1. 准备 ES 数据 要迁移 ES 数据,前提假设您已经拥有属于自己的 es Server(自建、ElasticCloud、阿里云 ES 等),向量数据存储在 dense_vector,以及其他字段在 index 中,index mapping 形式如: 2. 编译打包 首先下载迁移项目源码:https…

第P7周—咖啡豆识别(1)

数据集及wen件目录介绍&#xff1a; 数据集&#xff1a;工作台 - Heywhale.com 一、前期工作 1.1 数据详情 import torch import torch.nn as nn import torchvision.transforms as transforms import torchvision from torchvision import transforms, datasets import os,…

Ros2 学习02- ubuntu22.04 安装ros2

设置镜像源 sudo vim /etc/apt/sources.list#阿里源 deb http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ jammy main restricted universe multiverse deb http://mirrors.aliyun.com/ubuntu/ jamm…

小波变换学习笔记【1】

【声明】本博客为学习B站视频小波分解与重构所做笔记&#xff0c;供自己和大家查阅学习&#xff0c;想查看 up 原视频请移步 B 站&#xff0c;侵删。 1.1 小波变换的由来 傅里叶变换基本思想&#xff1a;将信号分解成一系列不同频率的连续正弦波的叠加。 其缺点是&#xff0c;…

数据结构 | 二叉树

基本形状 可参照 数据结构&#xff1a;树(Tree)【详解】_数据结构 树_UniqueUnit的博客-CSDN博客 二叉树的性质 三种顺序遍历

基于Java的演唱会网上订票系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

记录一次SQL注入src挖掘过程

记录一次SQL注入src挖掘过程&#xff0c;小白挖洞 先是使用谷歌语法找到一个可以注入的网站 谷歌语法&#xff1a; 公司inurl:php?id 然后该公司的URL为 URL:XXXXXXXXXX/xxx.php?id1 输入测试有无注入&#xff0c;有没有waf 发现有报错 使用sqlmap寻找注入点 python…

golang 结合 cobra 使用 chatgpt qdrant 实现 ai知识库 cli

golang 结合 cobra 使用 chatgpt qdrant 实现 ai知识库 cli 流程 将数据集 通过 openai embedding 得到向量组装payload,存入 qdrant用户进行问题搜索,通过 openai embedding 得到向量,从 qdrant 中搜索相似度大于0.8的数据从 qdrant 中取出数据得到参考答案将问题标题参考答案…