手写线程池 - C++版 - 笔记总结
1.线程池原理
创建一个线程,实现很方便。
缺点:若并发的线程数量很多,并且每个线程都是执行一个时间较短的任务就结束了。
由于频繁的创建线程和销毁线程需要时间,这样的频繁创建线程会大大降低
系统的效率。
2.思考 :怎么样实现线程复用,让线程执行完一个任务后不被销毁,还可以继续执行其他的任务?
答:线程池
3.思考 :什么是线程池?
可以点击看爱编程的大丙写的这边文章!写的很好
4.线程池的组成:任务队列、工作的线程、管理者线程
(1)任务队列
听到任务队列之后,咱们在脑海里应该闪现出另外一个模型,就是生产者和消费者模型。因为只要
有任务队列,就离不开生产者和消费者模型。为什么要有任务队列呢?就是因为他有生产者要把生
产出来的数据或者商品进行存储,存储起来之后让对应的某些消费者去消费。在我们写程序的时候,
就是有一些生产者线程还是负责往这个任务队列里边放任务,然后有一些消费者线程负责把任务从
任务队列中取出去,并且把它处理掉。在线程池里边有任务队列,也就意味着这个线程池它是生产者
和消费者模型里边的一部分。哪一个部分呢?这个线程池主要是负责维护一个任务队列,然后再维护
若干个消费者线程。它维护的线程都是消费者线程。
那么生产者是谁呢?谁使用任务队列,谁就是生产者。那么这个生产者它把任务放到任务队列里边。
怎么放呢?肯定是通过线程池提供的api接口,把这个任务放到任务队列,,放进来之后,这个消费
者线程通过一个循环,不停地从这个任务队列里边去取任务,
假设说咱们这个任务队列为空了,消费者线程就需要阻塞了。可以使用条件变量把它阻塞掉就行了。
如果让消费者线程去阻塞,它就放弃了CPU时间片了,这个对系统的资源消耗是一点都没有的。
对于生产者来说,假设这个任务队列已经满了,需要阻塞生产者。当这个消费者消费了这个产品之后,
任务队列就不再满了,那么任务队列不再满之后,咱们就让消费者把这个生产者唤醒,让他们继续生
产。这个任务队列是两个角色,而不是三个角色,这一点要搞明白的!!!(2)工作的线程
这个任务队列里边的任务都是函数地址,工作的线程在处理这个任务的时候,它就基于这个函数地址
对那个函数进行调用。也就说这个任务队列里边的任务都是回调函数。
什么时候去回调呢?
就是当线程池里边的这个消费者线程把它取出去之后,它就被调用了。如果说没有被取出来,他就不
被调用了。(3)管理者线程
管理者线程的职责非常单一,就是不停的去检测当前任务队列里边任务的个数,还有当前工作的线程
的线程数,然后针对于它们的数量进行一个运算。看一看现在需要添加线程还是销毁线程。干这个事
的时候,可以给它设置一个频率,比如说你可以让他五秒钟去做一次或者十秒钟去做一次。sleep管理
者是非常轻松的
(一)任务类Task的定义
#pragma once
#include <queue>
#include <pthread.h>using callback = void (*)(void *);// 任务结构体
template <typename T>
struct Task
{Task<T>(){function = nullptr;arg = nullptr;}Task<T>(callback f, void *arg){this->arg = (T *)arg;function = f;}callback function;T *arg;
};
(二)任务队列TaskQueue的定义
template <typename T>
class TaskQueue
{
public:TaskQueue();~TaskQueue();// 添加任务void addTask(Task<T> task);void addTask(callback f, void *arg);// 取出一个任务Task<T> takeTask();// 获取当前任务的个数inline size_t taskNumber(){return m_taskQ.size();}private:std::queue<Task<T> > m_taskQ;pthread_mutex_t m_mutex;
};
(三)线程池 ThreadPool 类的声明
#pragma once
#include "TaskQueue.h"
template <typename T>
class ThreadPool
{
public:// 创建线程池并初始化ThreadPool(int min, int max);// 销毁线程池~ThreadPool();// 给线程池添加任务void addTask(Task<T> task);// 获取线程池中工作的线程的个数int getBusyNum();// 获取线程池中活着的线程个数int getAliveNum();private:// 工作的线程是(消费者线程)任务函数static void *worker(void *arg);// 管理者线程任务函数static void *manager(void *arg);// 单个线程退出void threadExit();private:// 任务队列TaskQueue<T> *taskQ;pthread_t managerID; // 管理者线程IDpthread_t *threadIDs; // 工作的线程IDint minNum; // 最小线程数量int maxNum; // 最大线程数量int busyNum; // 忙的线程的个数int liveNum; // 存活的线程的个数int exitNum; // 要销毁的线程个数pthread_mutex_t mutexPool; // 锁整个的线程池pthread_cond_t notEmpty; // 任务队列是不是空了bool shutdown = false; // 是不是要销毁线程池static const int NUMBER = 2;
};
(四)线程池 构造函数
template <typename T>
ThreadPool<T>::ThreadPool(int min, int max)
{do{// 实例化任务队列taskQ = new TaskQueue<T>;if (taskQ == nullptr){std::cout << "new taskQ fail..." << std::endl;break;}// 根据线程的最大上限给线程数组分配内存threadIDs = new pthread_t[max];if (threadIDs == nullptr){std::cout << "new threadIDs fail..." << std::endl;break;}// 初始化memset(threadIDs, 0, sizeof(pthread_t) * max);minNum = min;maxNum = max;busyNum = 0;liveNum = min; // 和最小个数相等exitNum = 0;// 初始化互斥锁,条件变量if (pthread_mutex_init(&mutexPool, NULL) != 0 ||pthread_cond_init(¬Empty, NULL) != 0){std::cout << "mutex or condition init fail...\n"<< std::endl;break;}shutdown = false;/// 创建线程 ////"void *(ThreadPool::*)(void *arg)" 类型的实参与 "void *(*)(void *)" 类型的形参不兼容// 类的静态成员或者类的外部的非类普通函数,只要定义出来之后就有地址// 如果是类的成员函数,并且不是静止的,这个函数定义出来是没有地址的// 什么时候有地址呢?我们必须要给这个类进行实例化,创建类的对象,这个// 函数才有地址// this指针指向当前被实例化的对象,如果要是在外边new出来一个ThreadPool对象,// 那么这个this指针就指向该实例化对象// 为什么要把this传给manager呢?// 因为manager是一个静态方法,静态方法它只能访问类里边的静态成员变量,// 它不能访问类的非静态成员变量。// 因此如果我们想要访问这些非静态成员变量,就必须要给这个静态方法传进去一个// 实例化对象,通过传进去的这个实例化对象来访问里边的非静态成员函数和变量// 创建管理者线程,1个pthread_create(&managerID, NULL, manager, this);// 根据最小线程个数,创建线程for (int i = 0; i < min; i++){pthread_create(&threadIDs[i], NULL, worker, this);}return;} while (0);// 释放资源if (threadIDs)delete[] threadIDs;if (taskQ)delete taskQ;
}
(五)工作的线程的任务函数
// 工作线程任务函数
template <typename T>
void *ThreadPool<T>::worker(void *arg)
{ThreadPool *pool = static_cast<ThreadPool *>(arg);// 一直不停的工作while (true){// 访问任务队列(共享资源)加锁pthread_mutex_lock(&pool->mutexPool);// 当前任务队列是否为空,如果为空工作线程阻塞while (pool->taskQ->taskNumber() == 0 && !pool->shutdown){//printf("thread %ld waiting...\n",pthread_self());// 阻塞工作线程pthread_cond_wait(&pool->notEmpty, &pool->mutexPool);// 解除阻塞之后,判断是不是要销毁线程if (pool->exitNum > 0){pool->exitNum--;if (pool->liveNum > pool->minNum){pool->liveNum--;pthread_mutex_unlock(&pool->mutexPool);pool->threadExit();}}}// 判断线程池是否被关闭了if (pool->shutdown){pthread_mutex_unlock(&pool->mutexPool);pool->threadExit();}// 从任务队列中取出一个任务Task<T> task = pool->taskQ->takeTask();// 工作的线程+1pool->busyNum++;// 线程池解锁pthread_mutex_unlock(&pool->mutexPool);// 执行任务printf("thread %ld start working...\n", pthread_self());// std::cout<<"thread "<<to_string(pthread_self()) <<" start working..."<<std::endl;task.function(task.arg);delete task.arg;task.arg = nullptr;// 任务处理结束printf("thread %ld end working...\n", pthread_self());// std::cout<<"thread "<<std::string to_string(pthread_self()) <<" end working..."<<std::endl;pthread_mutex_lock(&pool->mutexPool);pool->busyNum--;pthread_mutex_unlock(&pool->mutexPool);}return nullptr;
}
(六)线程退出函数
//线程退出
template <typename T>
void ThreadPool<T>::threadExit()
{pthread_t tid = pthread_self();for (int i = 0; i < maxNum; ++i){if (threadIDs[i] == tid){threadIDs[i] = 0;// std::cout<<"threadExit() called, "<<std::string to_string(tid)<<" exiting..."<<std::endl;printf("threadExit() called,%ld exiting...\n", tid);break;}}pthread_exit(NULL); // 这个是标准C的函数
}
(七)管理者线程的任务函数
// 管理者线程任务函数
template <typename T>
void *ThreadPool<T>::manager(void *arg)
{ThreadPool *pool = static_cast<ThreadPool *>(arg);// 如果线程池没有关闭,就一直检测while (!pool->shutdown){// 每隔3s检测一次sleep(3);// 取出线程池中任务的数量和当前线程的数量 取出(工作的)忙的线程的数量pthread_mutex_lock(&pool->mutexPool);int queueSize = pool->taskQ->taskNumber();int liveNum = pool->liveNum;int busyNum = pool->busyNum;pthread_mutex_unlock(&pool->mutexPool);// 添加线程// 任务的个数>存货的线程个数 && 存活的线程数 < 最大线程数if (queueSize > liveNum && liveNum < pool->maxNum){pthread_mutex_lock(&pool->mutexPool);int counter = 0;for (int i = 0; i < pool->maxNum && counter < NUMBER && pool->liveNum < pool->maxNum; ++i){if (pool->threadIDs[i] == 0){pthread_create(&pool->threadIDs[i], NULL, worker, pool);counter++;pool->liveNum++;}}pthread_mutex_unlock(&pool->mutexPool);}// 销毁多余的线程// 忙的线程 * 2 < 存活的线程数 && 存活的线程 > 最小线程数if (busyNum * 2 < liveNum && liveNum > pool->minNum){pthread_mutex_lock(&pool->mutexPool);pool->exitNum = NUMBER;pthread_mutex_unlock(&pool->mutexPool);// 让工作的线程自杀for (int i = 0; i < NUMBER; ++i){pthread_cond_signal(&pool->notEmpty);}}}return nullptr;
}
(八)给线程池添加任务
// 给线程池添加任务
template <typename T>
void ThreadPool<T>::addTask(Task<T> task)
{if (shutdown){return;}// 添加任务,不需要加锁,任务队列中有锁taskQ->addTask(task);// 唤醒工作的线程pthread_cond_signal(¬Empty); // 通知消费者消费
}
(九)获取线程池忙的线程个数和活着的线程个数
template <typename T>
int ThreadPool<T>::getBusyNum()
{pthread_mutex_lock(&mutexPool);int busyNum = this->busyNum;pthread_mutex_unlock(&mutexPool);return busyNum;
}template <typename T>
int ThreadPool<T>::getAliveNum()
{pthread_mutex_lock(&mutexPool);int aliveNum = this->liveNum;pthread_mutex_unlock(&mutexPool);return aliveNum;
}
(十)线程池析构
template <typename T>
ThreadPool<T>::~ThreadPool()
{// 关闭线程池shutdown = true;// 阻塞回收管理者线程pthread_join(managerID, NULL);// 唤醒阻塞的消费者线程for (int i = 0; i < liveNum; i++){pthread_cond_signal(¬Empty);}// 释放堆内存if (taskQ) delete taskQ;if (threadIDs) delete[] threadIDs;pthread_mutex_destroy(&mutexPool);pthread_cond_destroy(¬Empty);
}
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
完整代码:
TaskQueue.h
#pragma once
#include <queue>
#include <pthread.h>using callback = void (*)(void *);// 任务结构体
template <typename T>
struct Task
{Task<T>(){function = nullptr;arg = nullptr;}Task<T>(callback f, void *arg){this->arg = (T *)arg;function = f;}callback function;T *arg;
};template <typename T>
class TaskQueue
{
public:TaskQueue();~TaskQueue();// 添加任务void addTask(Task<T> task);void addTask(callback f, void *arg);// 取出一个任务Task<T> takeTask();// 获取当前任务的个数inline size_t taskNumber(){return m_taskQ.size();}private:std::queue<Task<T> > m_taskQ;pthread_mutex_t m_mutex;
};
TaskQueue.cpp
#include "TaskQueue.h"
#include <pthread.h>template <typename T>
TaskQueue<T>::TaskQueue()
{// 初始化互斥锁pthread_mutex_init(&m_mutex, NULL);
}template <typename T>
TaskQueue<T>::~TaskQueue()
{// 销毁互斥锁pthread_mutex_destroy(&m_mutex);
}template <typename T>
void TaskQueue<T>::addTask(Task<T> task)
{pthread_mutex_lock(&m_mutex);m_taskQ.push(task);pthread_mutex_unlock(&m_mutex);
}template <typename T>
Task<T> TaskQueue<T>::takeTask()
{Task<T> t;pthread_mutex_lock(&m_mutex);if (!m_taskQ.empty()){t = m_taskQ.front();m_taskQ.pop();}pthread_mutex_unlock(&m_mutex);return t;
}template <typename T>
void TaskQueue<T>::addTask(callback f, void *arg)
{pthread_mutex_lock(&m_mutex);m_taskQ.push(Task<T>(f, arg));pthread_mutex_unlock(&m_mutex);
}
ThreadPool.h
#pragma once
#include "TaskQueue.h"
template <typename T>
class ThreadPool
{
public:// 创建线程池并初始化ThreadPool(int min, int max);// 销毁线程池~ThreadPool();// 给线程池添加任务void addTask(Task<T> task);// 获取线程池中工作的线程的个数int getBusyNum();// 获取线程池中活着的线程个数int getAliveNum();private:// 工作的线程是(消费者线程)任务函数static void *worker(void *arg);// 管理者线程任务函数static void *manager(void *arg);// 单个线程退出void threadExit();private:// 任务队列TaskQueue<T> *taskQ;pthread_t managerID; // 管理者线程IDpthread_t *threadIDs; // 工作的线程IDint minNum; // 最小线程数量int maxNum; // 最大线程数量int busyNum; // 忙的线程的个数int liveNum; // 存活的线程的个数int exitNum; // 要销毁的线程个数pthread_mutex_t mutexPool; // 锁整个的线程池pthread_cond_t notEmpty; // 任务队列是不是空了bool shutdown = false; // 是不是要销毁线程池static const int NUMBER = 2;
};
ThreadPool.cpp
#include "ThreadPool.h"
#include <iostream>
#include <string.h>
#include <stdlib.h>#include <unistd.h>template <typename T>
ThreadPool<T>::ThreadPool(int min, int max)
{do{// 实例化任务队列taskQ = new TaskQueue<T>;if (taskQ == nullptr){std::cout << "new taskQ fail..." << std::endl;break;}// 根据线程的最大上限给线程数组分配内存threadIDs = new pthread_t[max];if (threadIDs == nullptr){std::cout << "new threadIDs fail..." << std::endl;break;}// 初始化memset(threadIDs, 0, sizeof(pthread_t) * max);minNum = min;maxNum = max;busyNum = 0;liveNum = min; // 和最小个数相等exitNum = 0;// 初始化互斥锁,条件变量if (pthread_mutex_init(&mutexPool, NULL) != 0 ||pthread_cond_init(¬Empty, NULL) != 0){std::cout << "mutex or condition init fail...\n"<< std::endl;break;}shutdown = false;/// 创建线程 ////"void *(ThreadPool::*)(void *arg)" 类型的实参与 "void *(*)(void *)" 类型的形参不兼容// 类的静态成员或者类的外部的非类普通函数,只要定义出来之后就有地址// 如果是类的成员函数,并且不是静止的,这个函数定义出来是没有地址的// 什么时候有地址呢?我们必须要给这个类进行实例化,创建类的对象,这个// 函数才有地址// this指针指向当前被实例化的对象,如果要是在外边new出来一个ThreadPool对象,// 那么这个this指针就指向该实例化对象// 为什么要把this传给manager呢?// 因为manager是一个静态方法,静态方法它只能访问类里边的静态成员变量,// 它不能访问类的非静态成员变量。// 因此如果我们想要访问这些非静态成员变量,就必须要给这个静态方法传进去一个// 实例化对象,通过传进去的这个实例化对象来访问里边的非静态成员函数和变量// 创建管理者线程,1个pthread_create(&managerID, NULL, manager, this);// 根据最小线程个数,创建线程for (int i = 0; i < min; i++){pthread_create(&threadIDs[i], NULL, worker, this);}return;} while (0);// 释放资源if (threadIDs)delete[] threadIDs;if (taskQ)delete taskQ;
}// 工作线程任务函数
template <typename T>
void *ThreadPool<T>::worker(void *arg)
{ThreadPool *pool = static_cast<ThreadPool *>(arg);// 一直不停的工作while (true){// 访问任务队列(共享资源)加锁pthread_mutex_lock(&pool->mutexPool);// 当前任务队列是否为空,如果为空工作线程阻塞while (pool->taskQ->taskNumber() == 0 && !pool->shutdown){//printf("thread %ld waiting...\n",pthread_self());// 阻塞工作线程pthread_cond_wait(&pool->notEmpty, &pool->mutexPool);// 解除阻塞之后,判断是不是要销毁线程if (pool->exitNum > 0){pool->exitNum--;if (pool->liveNum > pool->minNum){pool->liveNum--;pthread_mutex_unlock(&pool->mutexPool);pool->threadExit();}}}// 判断线程池是否被关闭了if (pool->shutdown){pthread_mutex_unlock(&pool->mutexPool);pool->threadExit();}// 从任务队列中取出一个任务Task<T> task = pool->taskQ->takeTask();// 工作的线程+1pool->busyNum++;// 线程池解锁pthread_mutex_unlock(&pool->mutexPool);// 执行任务printf("thread %ld start working...\n", pthread_self());// std::cout<<"thread "<<to_string(pthread_self()) <<" start working..."<<std::endl;task.function(task.arg);delete task.arg;task.arg = nullptr;// 任务处理结束printf("thread %ld end working...\n", pthread_self());// std::cout<<"thread "<<std::string to_string(pthread_self()) <<" end working..."<<std::endl;pthread_mutex_lock(&pool->mutexPool);pool->busyNum--;pthread_mutex_unlock(&pool->mutexPool);}return nullptr;
}//线程退出
template <typename T>
void ThreadPool<T>::threadExit()
{pthread_t tid = pthread_self();for (int i = 0; i < maxNum; ++i){if (threadIDs[i] == tid){threadIDs[i] = 0;// std::cout<<"threadExit() called, "<<std::string to_string(tid)<<" exiting..."<<std::endl;printf("threadExit() called,%ld exiting...\n", tid);break;}}pthread_exit(NULL); // 这个是标准C的函数
}// 管理者线程任务函数
template <typename T>
void *ThreadPool<T>::manager(void *arg)
{ThreadPool *pool = static_cast<ThreadPool *>(arg);// 如果线程池没有关闭,就一直检测while (!pool->shutdown){// 每隔3s检测一次sleep(3);// 取出线程池中任务的数量和当前线程的数量 取出(工作的)忙的线程的数量pthread_mutex_lock(&pool->mutexPool);int queueSize = pool->taskQ->taskNumber();int liveNum = pool->liveNum;int busyNum = pool->busyNum;pthread_mutex_unlock(&pool->mutexPool);// 添加线程// 任务的个数>存货的线程个数 && 存活的线程数 < 最大线程数if (queueSize > liveNum && liveNum < pool->maxNum){pthread_mutex_lock(&pool->mutexPool);int counter = 0;for (int i = 0; i < pool->maxNum && counter < NUMBER && pool->liveNum < pool->maxNum; ++i){if (pool->threadIDs[i] == 0){pthread_create(&pool->threadIDs[i], NULL, worker, pool);counter++;pool->liveNum++;}}pthread_mutex_unlock(&pool->mutexPool);}// 销毁多余的线程// 忙的线程 * 2 < 存活的线程数 && 存活的线程 > 最小线程数if (busyNum * 2 < liveNum && liveNum > pool->minNum){pthread_mutex_lock(&pool->mutexPool);pool->exitNum = NUMBER;pthread_mutex_unlock(&pool->mutexPool);// 让工作的线程自杀for (int i = 0; i < NUMBER; ++i){pthread_cond_signal(&pool->notEmpty);}}}return nullptr;
}// 给线程池添加任务
template <typename T>
void ThreadPool<T>::addTask(Task<T> task)
{if (shutdown){return;}// 添加任务,不需要加锁,任务队列中有锁taskQ->addTask(task);// 唤醒工作的线程pthread_cond_signal(¬Empty); // 通知消费者消费
}template <typename T>
int ThreadPool<T>::getBusyNum()
{pthread_mutex_lock(&mutexPool);int busyNum = this->busyNum;pthread_mutex_unlock(&mutexPool);return busyNum;
}template <typename T>
int ThreadPool<T>::getAliveNum()
{pthread_mutex_lock(&mutexPool);int aliveNum = this->liveNum;pthread_mutex_unlock(&mutexPool);return aliveNum;
}template <typename T>
ThreadPool<T>::~ThreadPool()
{// 关闭线程池shutdown = true;// 阻塞回收管理者线程pthread_join(managerID, NULL);// 唤醒阻塞的消费者线程for (int i = 0; i < liveNum; i++){pthread_cond_signal(¬Empty);}// 释放堆内存if (taskQ) delete taskQ;if (threadIDs) delete[] threadIDs;pthread_mutex_destroy(&mutexPool);pthread_cond_destroy(¬Empty);
}
main.cpp
#include <iostream>
#include <pthread.h>
#include "ThreadPool.h"
#include "ThreadPool.cpp"
#include "TaskQueue.cpp"
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>void taskFunc(void *arg)
{int num = *(int *)arg;printf("thread %ld is working, number = %d \n", pthread_self(), num);sleep(1);
}int main()
{// 创建线程池ThreadPool<int> pool(3, 10);for (int i = 0; i < 100; ++i){int *num = new int(i + 100);*num = i + 100;pool.addTask(Task<int>(taskFunc, num));}sleep(20);return 0;
}
跟着这个老师的教程学习的,总结的笔记!!!
手写线程池 - C改C++版 | 爱编程的大丙 (subingwen.cn)
https://subingwen.cn/linux/threadpool-cpp/
相关文章:
手写线程池 - C++版 - 笔记总结
1.线程池原理 创建一个线程,实现很方便。 缺点:若并发的线程数量很多,并且每个线程都是执行一个时间较短的任务就结束了。 由于频繁的创建线程和销毁线程需要时间,这样的频繁创建线程会大大降低 系统的效率。 2.思考 …...
PHP 容器化引发线上 502 错误状态码的修复
最后更新时间 2023-01-24. 背景 笔者所在公司技术栈为 Golang PHP,目前部分项目已经逐步转 Go 语言重构,部分 PHP 业务短时间无法用 Go 重写。 相比 Go 语言,互联网公司常见的 Nginx PHP-FPM 模式,经常会出现性能问题—— 特…...
QT中UDP之UDPsocket通讯
目录 UDP: 举例: 服务器端: 客户端: 使用示例: 错误例子并且改正: UDP: (User Datagram Protocol即用户数据报协议)是一个轻量级的,不可靠的࿰…...
【C语言】10-三大结构之循环结构-1
1. 引言 在日常生活中经常会遇到需要重复处理的问题,例如 统计全班 50 个同学平均成绩的程序求 30 个整数之和检查一个班级的同学程序是否及格要处理以上问题,最原始的方法是分别编写若干个相同或相似的语句或者程序段进行处理 例如:处理 50 个同学的平均成绩可以先计算一个…...
Windows下RocketMQ的启动
下载地址:下载 | RocketMQ 解压后 一、修改runbroker.cmd 修改 bin目录下的runbroker.cmd set "JAVA_OPT%JAVA_OPT% -server -Xms2g -Xmx2g" set "JAVA_OPT%JAVA_OPT% -XX:MaxDirectMemorySize15g" set "JAVA_OPT%JAVA_OPT% -cp %CLASSP…...
linux内核升级 docker+k8s更新显卡驱动
官方驱动 | NVIDIA在此链接下载对应的显卡驱动 # 卸载可能存在的旧版本nvidia驱动(如果没有安装过可跳过,建议执行) sudo apt-get remove --purge nvidia* # 安装驱动需要的依赖 sudo apt-get install dkms build-essential linux-headers-generic sudo vim /etc/mo…...
express学习笔记2 - 三大件概念
中间件 中间件是一个函数,在请求和响应周期中被顺序调用(WARNING:提示:中间件需要在响应结束前被调用) 路由 应用如何响应请求的一种规则 响应 / 路径的 get 请求: app.get(/, function(req, res) {res…...
Steam搬砖蓝海项目
这个项目早在很久之前就已经存在,并且一直非常稳定。如果你玩过一些游戏,你一定知道Steam是什么平台。Steam平台是全球最大的综合性数字发行平台之一,玩家可以在该平台购买、下载、讨论、上传和分享游戏和软件。 今天我给大家解释一下什么是…...
就业并想要长期发展选数字后端还是ic验证?
“就业并想要长期发展选数字后端还是ic验证?” 这是知乎上的一个热点问题,浏览量达到了13,183。看来有不少同学对这个问题感到疑惑。之前更新了数字后端&数字验证的诸多文章,从学习到职业发展,都写过,唯一没有做过…...
当服务器域名出现解析错误的问题该怎么办?
域名解析是互联网用户接收他们正在寻找的域的地址的过程。更准确地说,域名解析是人们在浏览器中输入时使用的域名与网站IP地址之间的转换过程。您需要站点的 IP 地址才能知道它所在的位置并加载它。但,在这个过程中,可能会出现多种因素…...
面试必考精华版Leetcode2095. 删除链表的中间节点
题目: 代码(首刷看解析 day22): class Solution { public:ListNode* deleteMiddle(ListNode* head) {if(head->nextnullptr) return nullptr;ListNode *righthead;ListNode *lefthead;ListNode *NodeBeforeLeft;while(right!n…...
对 Redis 实现分布式事务的探索与实现
对 Redis 实现分布式事务的探索与实现 一、简介简介优势 二、Redis 的事务机制事务WATCH 命令MULTI 命令EXEC 命令UNWATCH 命令 三、Redis 的分布式事务集群架构分布式事务分布式事务实现方式1. 两阶段提交(2PC)方式Paxos 算法实现方式Raft 算法实现方式…...
Matlab实现Spectral Clustering算法
Spectral Clustering算法是一种基于图论的聚类算法,它可以将数据点按照图结构进行划分,发现复杂和非线性可分的结构。在这篇博客中,我将介绍Spectral Clustering算法的原理和步骤,并给出一个用Matlab实现的代码示例。 目录 一、…...
Android 测试
工程目录图 1- Espresso 2- uiautomator Espresso 文档UI Automator文档ui-automator 英文文档 请点击下面工程名称,跳转到代码的仓库页面,将工程 下载下来 Demo Code 里有详细的注释 代码:testespresso 参考文献 Android 利用 espre…...
全面解析大语言模型的工作原理
当ChatGPT在去年秋天推出时,在科技行业乃至世界范围内引起了轰动。当时,机器学习研究人员尝试研发了多年的语言大模型(LLM),但普通大众并未十分关注,也没有意识到它们变得多强大。 如今,几乎每个…...
cmake+pybind11打包c++库成python wheel安装包
目录 写在前面准备1、pybind11获取源码编译安装 2、conda demo官方源码修改CMakeLists.txt编译生成安装测试 参考完 写在前面 1、本文内容 有时候我们需要用c代码,供python调用,本文提供将c库封装成python接口的方法,并将库打包成可通过pip安…...
史上最细,接口自动化测试框架-Pytest+Allure+Excel整理(代码)
目录:导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结(尾部小惊喜) 前言 Allure框架 Allu…...
【计算机视觉中的 GAN 】 - 条件图像合成和 3D 对象生成(2)
一、说明 上文 【计算机视觉中的 GAN 】或多或少是GANs,生成学习和计算机视觉的介绍。我们达到了在 128x128 图像中生成可区分图像特征的程度。但是,如果你真的想了解GAN在计算机视觉方面的进展,你肯定必须深入研究图像到图像的翻译。…...
智安网络|常见的网络安全陷阱:你是否掉入了其中?
在数字化时代,网络安全成为了一个重要的议题。随着我们越来越多地在互联网上进行各种活动,诸如在线银行交易、社交媒体分享和在线购物等,我们的个人信息也更容易受到攻击和滥用。虽然有许多关于网络安全的指导和建议,但仍然有许多…...
亚马逊云科技HPC解决方案,帮助浙江大学实现成本和科研任务的双丰收
浙江大学土壤学科是朱祖祥院士等几代土壤科学家共同创建的A国家重点学科,整体实力雄厚,优势特色明显,总体水平居国内前列。在亚马逊云科技科研创新支持计划(Amazon Web Services Cloud Credits for Research)的多次支持…...
React Native 导航系统实战(React Navigation)
导航系统实战(React Navigation) React Navigation 是 React Native 应用中最常用的导航库之一,它提供了多种导航模式,如堆栈导航(Stack Navigator)、标签导航(Tab Navigator)和抽屉…...
如何在看板中有效管理突发紧急任务
在看板中有效管理突发紧急任务需要:设立专门的紧急任务通道、重新调整任务优先级、保持适度的WIP(Work-in-Progress)弹性、优化任务处理流程、提高团队应对突发情况的敏捷性。其中,设立专门的紧急任务通道尤为重要,这能…...
Python爬虫(二):爬虫完整流程
爬虫完整流程详解(7大核心步骤实战技巧) 一、爬虫完整工作流程 以下是爬虫开发的完整流程,我将结合具体技术点和实战经验展开说明: 1. 目标分析与前期准备 网站技术分析: 使用浏览器开发者工具(F12&…...
【配置 YOLOX 用于按目录分类的图片数据集】
现在的图标点选越来越多,如何一步解决,采用 YOLOX 目标检测模式则可以轻松解决 要在 YOLOX 中使用按目录分类的图片数据集(每个目录代表一个类别,目录下是该类别的所有图片),你需要进行以下配置步骤&#x…...
安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)
船舶制造装配管理现状:装配工作依赖人工经验,装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书,但在实际执行中,工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...
springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
Python实现简单音频数据压缩与解压算法
Python实现简单音频数据压缩与解压算法 引言 在音频数据处理中,压缩算法是降低存储成本和传输效率的关键技术。Python作为一门灵活且功能强大的编程语言,提供了丰富的库和工具来实现音频数据的压缩与解压。本文将通过一个简单的音频数据压缩与解压算法…...
全面解析数据库:从基础概念到前沿应用
在数字化时代,数据已成为企业和社会发展的核心资产,而数据库作为存储、管理和处理数据的关键工具,在各个领域发挥着举足轻重的作用。从电商平台的商品信息管理,到社交网络的用户数据存储,再到金融行业的交易记录处理&a…...
【安全篇】金刚不坏之身:整合 Spring Security + JWT 实现无状态认证与授权
摘要 本文是《Spring Boot 实战派》系列的第四篇。我们将直面所有 Web 应用都无法回避的核心问题:安全。文章将详细阐述认证(Authentication) 与授权(Authorization的核心概念,对比传统 Session-Cookie 与现代 JWT(JS…...
