[Linux] 最基础简单的线程池 及其 单例模式的实现

|cover


本篇文章主要用到线程相关内容, 下面是博主关于线程相关内容的文章:

[Linux] 线程同步分析:什么是条件变量?生产者消费者模型是什么?POSIX信号量怎么用?阻塞队列和环形队列模拟生产者消费者模型

[Linux] 线程互斥分析: 多线程的问题、互斥锁、C++封装使用互斥锁、线程安全分析、死锁分析…

[Linux] 如何理解线程ID?什么是线程局部存储?

[Linux] 多线程控制分析:获取线程ID、线程退出分析、自动回收线程、线程分离…

[Linux] 多线程概念相关分析: 什么是线程、再次理解进程、线程的创建与查看、线程异常、线程与进程的对比…


线程池

什么是线程池?

线程池一种线程使用模式. 我们知道, 线程的创建、调度、销毁都是需要消耗资源的. 也就是说 线程过多会带来调度开销, 进而影响缓存局部性和整体性能.

而线程池维护着多个线程, 这些线程等待着被分配可并发执行的任务. 这避免了在处理短时间任务时创建与销毁线程的代价.

说简单点, 就是 线程池维护着多个线程, 这些线程都可以随时被调度、随时被派发任务, 不用在任务需要派发时再创建线程, 而是在需要派发任务时 可以直接调度线程池内的线程, 执行任务

线程池的使用场景, 一般是 任务量巨大, 但是任务内容小的、任务时间短 的时候. 这样可以避免发生过多线程的创建与销毁. 或者 需要快速响应的任务, 因为不用再创建线程.

简单的固定线程数线程池

下面, 封装一个 简单的 拥有固定线程数量的线程池.

线程池维护着多个线程, 并不是说创建几个线程就可以了. 线程池还要管理这些线程的调度和执行, 整个实现类似一个变化的生产者消费者模型.

所以我们可以通过阻塞队列, 来实现对接收任务和同步调度线程.

threadPool.hpp:

#pragma once#include <iostream>
#include <queue>
#include <cassert>
#include <pthread.h>
#include <unistd.h>#define THREADNUM 5template <class T>
class threadPool {
public:threadPool(size_t threadNum = THREADNUM): _threadNum(threadNum), _isStart(false) {assert(_threadNum > 0);pthread_mutex_init(&_mutex, nullptr); // 初始化 锁pthread_cond_init(&_cond, nullptr);   // 初始化 条件变量}// 线程回调函数// static 修饰, 是因为需要让函数参数 取消this指针, 只留一个void*// 但是由于 需要访问类内成员, 所以 传参需要传入this指针static void* threadRoutine(void* args) {// 线程执行回调函数// 先分离, 自动回收pthread_detach(pthread_self());// 获取this指针threadPool<T>* tP = static_cast<threadPool<T>*>(args);while (true) {// 即将通过任务队列给线程分配任务, 即 多线程访问临界资源, 需要上锁tP->lockQueue();while (!tP->haveTask()) {// 任务队列中没有任务, 就让线程通过条件变量等待tP->waitForTask();}// 走到这里 说明条件队列中有任务// 线程已经可以获取到任务T task = tP->popTask();// 获取到任务之后 临界资源的访问就结束了, 可以释放锁了.// 尽量避免拿着锁 执行任务tP->unlockQueue();// 为任务类提供一个运行的接口, 这样获取到任务之后 直接 task.run();task.run();}}// 开启线程池void start() {try {// _isStart 为true 则说明线程池已经开启if (_isStart)throw "Error: thread pool already exists";}catch (const char* e) {std::cout << e << std::endl;return;}for (int i = 0; i < _threadNum; i++) {pthread_t temp;pthread_create(&temp, nullptr, threadRoutine, this); // 回调函数的参数传入this指针, 用于类访问内成员}// 开启线程池之后, 要把 _isStart 属性设置为 true_isStart = true;}// 给任务队列添加任务 并分配任务void pushTask(const T& in) {// 上锁lockQueue();_taskQueue.push(in);// 任务队列中已经存在任务, 线程就不用再等待了, 就可以唤醒线程choiceThreadForHandler();// 释放锁unlockQueue();}~threadPool() {pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}private:// 线程调度 即为从任务队列中给各线程分配任务// 所以 任务队列是临界资源需要上锁void lockQueue() {pthread_mutex_lock(&_mutex);}void unlockQueue() {pthread_mutex_unlock(&_mutex);}// 条件变量 使用条件, 判断是否任务队列是否存在任务bool haveTask() {return !_taskQueue.empty();}// 线程通过条件变量等待任务void waitForTask() {pthread_cond_wait(&_cond, &_mutex);}// 从任务队列中获取任务, 并返回T popTask() {T task = _taskQueue.front();_taskQueue.pop();return task;}// 唤醒在条件变量前等待的线程// 由于唤醒之后就是线程调度的过程// 所以函数名 是线程调度相关void choiceThreadForHandler() {pthread_cond_signal(&_cond);}private:size_t _threadNum;        	// 线程池内线程数量bool _isStart;            	// 判断线程池是否已经开启std::queue<T> _taskQueue;	// 任务队列pthread_mutex_t _mutex;		// 锁 给临界资源使用 即任务队列 保证线程调度互斥pthread_cond_t _cond; 		// 条件变量 保证线程调度同步
};

这部分代码就是一个再简单不过的线程池.

这个最简单的线程池的功能就包括:

  1. 单次开启线程池, 创建多线程, 并让线程等待调度
  2. 可以获取任务并存储任务
  3. 由于通过多线程访问临界资源分配任务, 所以 要做到同步互斥地给线程分配任务
  4. 得到任务之后, 线程执行任务, 执行完成继续等待调度

所以, 成员变量至少要用到的成员变量有:

  1. size_t _threadNum, 用来设置线程池中线程的数量
  2. bool _isStart, 用来设置线程池开启状态
  3. std::queue<T> _taskQueue, 任务队列, 临界资源. 用来接收主线程发来的任务. 存储任务, 向线程分配
  4. pthread_mutex_t _mutex, 锁, 为保证多线程访问任务队列互斥, 且实现同步向线程分配任务
  5. pthread_cond_t _cond, 条件变量, 为实现无任务时 线程等待调度, 且实现同步向线程分配任务

整个线程池中, 最重要的就是多线程所执行的回调函数的实现.

此函数中, 包括线程等待, 分配任务, 执行任务的功能, 并且参数的传递也很重要:

// 线程回调函数
// static 修饰, 是因为需要让函数参数 取消this指针, 只留一个void*
// 但是由于 需要访问类内成员, 所以 传参需要传入this指针
static void* threadRoutine(void* args) {// 线程执行回调函数// 先分离, 自动回收pthread_detach(pthread_self());// 获取this指针threadPool<T>* tP = static_cast<threadPool<T>*>(args);while (true) {// 即将通过任务队列给线程分配任务, 即 多线程访问临界资源, 需要上锁tP->lockQueue();while (!tP->haveTask()) {// 任务队列中没有任务, 就让线程通过条件变量等待tP->waitForTask();}// 走到这里 说明条件队列中有任务, 线程已经可以获取任务T task = tP->popTask();// 获取到任务之后 临界资源的访问就结束了, 可以释放锁了.// 尽量避免拿着锁 执行任务tP->unlockQueue();// 为任务类提供一个运行的接口, 这样获取到任务之后 直接 task.run();task.run();}
}// 类内创建线程时的操作
pthread_create(&temp, nullptr, threadRoutine, this);

我们知道, 线程需要执行的回调函数格式是这样的void* 函数名(void*)

但是, 类内的所有成员函数第一个参数是this指针. 所以我们需要将此函数用static修饰. 然而修饰之后, 此函数就不属于类内成员函数了, 所以无法直接调用访问类内成员. 所以, 参数需要传入类的this指针, 通过this指针访问对象成员.

所以, 此函数的首要的功能 除了分离线程之外, 就是要通过参数获取到调用对象的this指针

然后就要实现线程主要需要执行的功能:

首先, 线程需要 在没有任务时, 通过条件变量陷入等待. 而且, 线程在 执行完任务 时, 需要 重新在没有任务时, 通过条件变量陷入等待. 所以, 函数的主体功能是在 一个循环 内的.

|inline

进入循环后, 就应该 从任务队列中获取任务, 但是 如果任务队列中 没有任务, 线程就需要等待.

并且, 线程 无论是获取任务的过程 还是 判断是否有任务的过程, 访问的都是临界资源, 而 临界资源需要保证线程安全, 所以 在进入循环之后的 第一件事, 应该是 对临界资源上锁, 即 多线程争夺锁. 争夺到锁之后, 才能访问临界资源:

|inline

获取到任务之后, 就表示线程访问本次临界资源已经结束, 就可以释放锁, 然后执行任务了.

而, 除了线程执行的功能函数之外, 还有一个需要将任务放入任务队列的函数:

|inline

至此, 有关线程池的主要功能函数就是先完毕了.

我们可以再结合下面这几个代码文件 测试一下:

logMessage.hpp:

#pragma once#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <cstdlib>// 宏定义 四个日志等级
#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3const char* log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};// 实现一个 可以输出: 日志等级、日志时间、用户、以及相关日志内容的 日志消息打印接口
void logMessage(int level, const char* format, ...) {// 通过可变参数实现, 传入日志等级, 日志内容格式, 日志内容相关参数// 确保日志等级正确assert(level >= DEBUG);assert(level <= FATAL);// 获取当前用户名char* name = getenv("USER");// 简单的定义log缓冲区char logInfo[1024];// 定义一个指向可变参数列表的指针va_list ap;// 将 ap 指向可变参数列表中的第一个参数, 即 format 之后的第一个参数va_start(ap, format);// 此函数 会通过 ap 遍历可变参数列表, 然后根据 format 字符串指定的格式, 将ap当前指向的参数以字符串的形式 写入到logInfo缓冲区中vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);// ap 使用完之后, 再将 ap置空va_end(ap); // ap = NULL// 通过判断日志等级, 来选择是标准输出流还是标准错误流FILE* out = (level == FATAL) ? stderr : stdout;// 获取本地时间time_t tm = time(nullptr);struct tm* localTm = localtime(&tm);char* localTmStr = asctime(localTm);char* nC = strstr(localTmStr, "\n");if(nC) {*nC = '\0';}fprintf( out, "%s | %s | %s | %s\n", log_level[level],localTmStr,name == nullptr ? "unknow" : name, logInfo );
}

intArithmeticTask.hpp:

// 任务类
#pragma once#include <iostream>
#include <map>
#include <string>
#include <functional>
#include <pthread.h>
#include "logMessage.hpp"std::map<char, std::function<int(int, int)>> opFunctions{{'+', [](int elemOne, int elemTwo) { return elemOne + elemTwo; }},{'-', [](int elemOne, int elemTwo) { return elemOne - elemTwo; }},{'*', [](int elemOne, int elemTwo) { return elemOne * elemTwo; }},{'/', [](int elemOne, int elemTwo) {if (elemTwo == 0) {std::cout << "div zero, abort" << std::endl;return -1;}return elemOne / elemTwo;}},{'%', [](int elemOne, int elemTwo) {if (elemTwo == 0) {std::cout << "div zero, abort" << std::endl;return -1;}return elemOne % elemTwo;}}
};class Task {
public:Task(int one = 0, int two = 0, char op = '0'): _elemOne(one), _elemTwo(two), _operator(op) {}void operator()() {run();}void run() {int result = 0;if (opFunctions.find(_operator) != opFunctions.end()) {result = opFunctions[_operator](_elemOne, _elemTwo);if ((_elemTwo == 0 && _operator == '/') ||(_elemTwo == 0 && _operator == '%')) return;logMessage(NOTICE, "新线程[%lu] 完成算术任务: %d %c %d = %d", pthread_self(), _elemOne, _operator, _elemTwo, result);}else {std::cout << "非法操作: " << _operator << std::endl;}}void get(int* e1, int* e2, char* op) {*e1 = _elemOne;*e2 = _elemTwo;*op = _operator;}private:int _elemOne;int _elemTwo;char _operator;
};

threadPool.cc:

// 开启线程池, 任务派发主函数
#include <iostream>
#include <memory>
#include <ctime>
#include <cstdlib>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include "logMessage.hpp"
#include "threadPool.hpp"
#include "intArithmeticTask.hpp"const std::string operators = {"+-*/\%"};int main() {std::unique_ptr<threadPool<Task>> tP(new threadPool<Task>);// 开启线程池tP->start();srand((unsigned int)time(nullptr) ^ getpid() ^ pthread_self());while (true) {int elemOne = rand()%20;int elemTwo = rand()%10;char oper = operators[rand()%operators.size()];logMessage(NOTICE, "主线程[%lu] 派发算术任务: %d %c %d = ?", pthread_self(), elemOne, oper, elemTwo);Task taskTmp(elemOne, elemTwo, oper);tP->pushTask(taskTmp);// 设置为1s添加 分配一个任务sleep(1);}return 0;
}

然后, 编译运行:

可以看到运行的结果就是 我们期望的结果, 主线程每秒添加并分配一个, 5个线程同步获取到任务并执行.

我们还可以将任务的处理速度设置慢一些, 任务的添加分配速度快一些, 更明显的看到多线程的并发

将 处理速度设置为1s, 添加分配速度设置为0.1s:

请添加图片描述

当派发速度变快 处理速度变慢, 之间超过5倍差的时候:

|inline

懒汉单例模式线程池

单例模式, 是指 只能创建一个实例对象的类

懒汉式的单例模式, 是指 在使用时才实例化单例对象的单例模式.

我们可以将 这个线程池 修改为单例模式:

lock.hpp:

// 一个 RAII思想实现的锁
#pragma once#include <iostream>
#include <pthread.h>class Mutex {
public:Mutex() {pthread_mutex_init(&_lock, nullptr);}void lock() {pthread_mutex_lock(&_lock);}void unlock() {pthread_mutex_unlock(&_lock);}~Mutex() {pthread_mutex_destroy(&_lock);}private:pthread_mutex_t _lock;
};class LockGuard {
public:LockGuard(Mutex* mutex): _mutex(mutex) {_mutex->lock();std::cout << "加锁成功..." << std::endl;}~LockGuard() {_mutex->unlock();std::cout << "解锁成功...." << std::endl;}private:Mutex* _mutex;
};

threadPool.hpp:

// 单例模式的线程池
#pragma once#include <cstddef>
#include <iostream>
#include <ostream>
#include <queue>
#include <cassert>
#include <pthread.h>
#include <unistd.h>
#include "lock.hpp"#define THREADNUM 5template <class T>
class threadPool {
public:static threadPool<T>* getInstance() {// RAII锁static Mutex mutex;if (_instance == nullptr) {LockGuard lockG(&mutex);if (_instance == nullptr) {_instance = new threadPool<T>();}}return _instance;}// 线程回调函数// static 修饰, 是因为需要让函数参数 取消this指针, 只留一个void*// 但是由于 需要访问类内成员, 所以 传参需要传入this指针static void* threadRoutine(void* args) {// 线程执行回调函数// 先分离, 自动回收pthread_detach(pthread_self());// 获取this指针threadPool<T>* tP = static_cast<threadPool<T>*>(args);while (true) {// 即将通过任务队列给线程分配任务, 即 多线程访问临界资源, 需要上锁tP->lockQueue();while (!tP->haveTask()) {// 任务队列中没有任务, 就让线程通过条件变量等待tP->waitForTask();}// 走到这里 说明条件队列中有任务// 线程已经可以获取到任务T task = tP->popTask();// 获取到任务之后 临界资源的访问就结束了, 可以释放锁了.// 尽量避免拿着锁 执行任务tP->unlockQueue();// 为任务类提供一个运行的接口, 这样获取到任务之后 直接 task.run();task.run();}}// 开启线程池void start() {try {// _isStart 为true 则说明线程池已经开启if (_isStart)throw "Error: thread pool already exists";}catch (const char* e) {std::cout << e << std::endl;return;}for (int i = 0; i < _threadNum; i++) {pthread_t temp;pthread_create(&temp, nullptr, threadRoutine,this); // 回调函数的参数传入this指针, 用于类访问内成员}// 开启线程池之后, 要把 _isStart 属性设置为 true_isStart = true;}// 给任务队列添加任务 并分配任务void pushTask(const T& in) {// 上锁lockQueue();_taskQueue.push(in);// 任务队列中已经存在任务, 线程就不用再等待了, 就可以唤醒线程choiceThreadForHandler();// 释放锁unlockQueue();}~threadPool() {pthread_mutex_destroy(&_mutex);pthread_cond_destroy(&_cond);}threadPool(const threadPool<T>&) = delete;threadPool<T>& operator=(const threadPool<T>&) = delete;private:threadPool(size_t threadNum = THREADNUM): _threadNum(threadNum), _isStart(false) {assert(_threadNum > 0);pthread_mutex_init(&_mutex, nullptr); // 初始化 锁pthread_cond_init(&_cond, nullptr);   // 初始化 条件变量}// 线程调度 即为从任务队列中给各线程分配任务// 所以 任务队列是临界资源需要上锁void lockQueue() {pthread_mutex_lock(&_mutex);}void unlockQueue() {pthread_mutex_unlock(&_mutex);}// 条件变量 使用条件, 判断是否任务队列是否存在任务bool haveTask() {return !_taskQueue.empty();}// 线程通过条件变量等待任务void waitForTask() {pthread_cond_wait(&_cond, &_mutex);}// 从任务队列中获取任务, 并返回T popTask() {T task = _taskQueue.front();_taskQueue.pop();return task;}// 唤醒在条件变量前等待的线程// 由于唤醒之后就是线程调度的过程// 所以函数名 是线程调度相关void choiceThreadForHandler() {pthread_cond_signal(&_cond);}private:size_t _threadNum;        // 线程池内线程数量bool _isStart;            // 判断线程池是否已经开启std::queue<T> _taskQueue; // 任务队列pthread_mutex_t _mutex; // 锁 给临界资源使用 即任务队列 保证线程调度互斥pthread_cond_t _cond; // 条件变量 保证线程调度同步static threadPool<T>* _instance;
};template <class T>
threadPool<T>* threadPool<T>::_instance = nullptr;

运行结果:

请添加图片描述

执行效果是没有区别的.


感谢阅读~

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

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

相关文章

华为、阿里巴巴、字节跳动 100+ Python 面试问题总结(一)

系列文章目录 个人简介&#xff1a;机电专业在读研究生&#xff0c;CSDN内容合伙人&#xff0c;博主个人首页 Python面试专栏&#xff1a;《Python面试》此专栏面向准备面试的2024届毕业生。欢迎阅读&#xff0c;一起进步&#xff01;&#x1f31f;&#x1f31f;&#x1f31f; …

华为发布大模型时代AI存储新品

7月14日&#xff0c;华为发布大模型时代AI存储新品&#xff0c;为基础模型训练、行业模型训练&#xff0c;细分场景模型训练推理提供存储最优解&#xff0c;释放AI新动能。 企业在开发及实施大模型应用过程中&#xff0c;面临四大挑战&#xff1a; 首先&#xff0c;数据准备时…

剑指offer刷题笔记--Num41-50

1--数据流中的中位数&#xff08;41&#xff09; 主要思路&#xff1a; 维护两个优先队列&#xff0c;Q1大数优先&#xff0c;存储比中位数小的数&#xff1b;Q2小数优先&#xff0c;存储比中位数大的数&#xff1b; 当存储的数为偶数时&#xff0c;Q1.size() Q2.size(), 中位…

解决github无法拉取submodule子模块的问题

引言 当使用git clone --recursive url 拉取一个配置了子模块的仓库后&#xff0c;会卡住。 同时在使用git clone 拉去https的url时&#xff0c;同样可能会出现一直卡在cloning int reposity...本文提供一个简单的脚本来解决该问题。 前置准备 需要配置好git的相关配置&…

快速配置 Rust 开发环境并编写一个小应用

安装: curl --proto https --tlsv1.2 -sSf https://sh.rustup.rs | sh 更新: Rust 的升级非常频繁. 如果安装 Rustup 后已有一段时间,那么很可能 Rust 版本已经过时, 运行 rustup update 获取最新版本的 Rust rustc&#xff1a;编译Rust程序 rustc只适合简单的Rust程序&#xf…

qt和vue的交互

1、首先在vue项目中引入qwebchannel /******************************************************************************** Copyright (C) 2016 The Qt Company Ltd.** Copyright (C) 2016 Klarlvdalens Datakonsult AB, a KDAB Group company, infokdab.com, author Milian …

记录--再也不用手动改package.json的版本号

这里给大家分享我在网上总结出来的一些知识&#xff0c;希望对大家有所帮助 本文的起因是有在代码仓库发包后&#xff0c;同事问我“为什么package.json 里的版本还是原来的&#xff0c;有没有更新&#xff1f;”&#xff0c;这个时候我意识到&#xff0c;我们完全没有必要在每…

阿里云无影云电脑具体价格_云桌面不同配置1元报价

阿里云无影云电脑配置费用&#xff0c;4核8G企业办公型云电脑可以免费使用3个月&#xff0c;无影云电脑地域不同费用不同&#xff0c;无影云电脑是由云桌面配置、云盘、互联网访问带宽、AD Connector、桌面组共用桌面session等费用组成&#xff0c;阿里云百科分享阿里云无影云电…

什么是分布式软件系统

:什么是分布式软件系统&#xff1f;分布式软件系统是什么意思&#xff1f; 分布式软件系统(Distributed Software Systems)是支持分布式处理的软件系统,是在由通信网络互联的多处理机体系结构上执行任务的系统。它包括分布式操作系统、分布式程序设计语言及其编译(解释)系统、分…

Unity 2D骨骼动画+IK反向动力学

本文言简意赅的完成这个流程&#xff0c;废话不多说&#xff01;干&#xff01; 等等&#xff0c;先看看效果 第一步&#xff1a;导入2D Animation包 当前环境&#xff1a;Unity3D 2021(不需要完全一样也可以) 进入unity后点击Window->PackageManager打开如下界面 按Insta…

LangChain 联合创始人下场揭秘:如何用 LangChain 和向量数据库搞定语义搜索?

近期&#xff0c;关于 ChatGPT 的访问量有所下降的消息引发激烈讨论&#xff0c;不过这并不意味着开发者对于 AIGC 的热情有所减弱&#xff0c;例如素有【2023 最潮大语言模型 Web 开发框架】之称的大网红 LangChain 的热度就只增不减。 原因在于 LangChain 作为大模型能力“B2…

Vue3_简介、CompositionVPI、新的组件

文章目录 Vue3快速上手1.Vue3简介2.Vue3带来了什么1.性能的提升2.源码的升级3.拥抱TypeScript4.新的特性 一、创建Vue3.0工程1.使用 vue-cli 创建2.使用 vite 创建 二、常用 Composition API1.拉开序幕的setup2.ref函数3.reactive函数4.Vue3.0中的响应式原理vue2.x的响应式Vue3…

万字长文 | Hadoop 上云: 存算分离架构设计与迁移实践

一面数据原有的技术架构是在线下机房中使用 CDH 构建的大数据集群。自公司成立以来&#xff0c;每年都保持着高速增长&#xff0c;业务的增长带来了数据量的剧增。 在过去几年中&#xff0c;我们按照每 1 到 2 年的规划扩容硬件&#xff0c;但往往在半年之后就不得不再次扩容。…

《Redis 核心技术与实战》课程学习笔记(八)

String 类型为什么不好用了&#xff1f; String 类型可以保存二进制字节流&#xff0c;只要把数据转成二进制字节数组&#xff0c;就可以保存了。String 类型并不是适用于所有场合的&#xff0c;它有一个明显的短板&#xff0c;就是它保存数据时所消耗的内存空间较多。 为什么…

Unity Shader - SV_POSITION 和 TEXCOORD[N] 的varying 在 fragment shader 中输出的区别

起因 因另一个TA同学问了一个问题 我抱着怀疑的心态&#xff0c;测试了一下 发现 varying 中的 sv_position 和 texcoord 的值再 fragment shader 阶段还真的不一样 而且 sv_position 还不是简单的 clipPos/clipPos.w 的操作 因此我自己做了一个试验&#xff1a; 结果还是不一…

电脑应用程序发生异常怎么办?

有时候我们打开电脑上面的某个软件时&#xff0c;会打不开&#xff0c;并且会弹出如下的错误提示“应用程序发生异常 未知的软件异常&#xff08;&#xff58;&#xff58;&#xff58;&#xff09;&#xff0c;位置为&#xff58;&#xff58;”。相信大多数的人在使用电脑的时…

Pytorch基本使用—激活函数

✨1 介绍 ⛄ 1.1 概念 激活函数是神经网络中的一种数学函数&#xff0c;它被应用于神经元的输出&#xff0c;以决定神经元是否应该被激活并传递信号给下一层。常见的激活函数包括Sigmoid函数、ReLU函数、Tanh函数等。 &#x1f384; 1.2 性质 激活函数是神经网络中的一种重…

为什么单片机可以直接烧录程序的原因是什么?

单片机&#xff08;Microcontroller&#xff09;可以直接烧录程序的原因主要有以下几点&#xff1a; 集成性&#xff1a;单片机是一种高度集成的芯片&#xff0c;内部包含了处理器核心&#xff08;CPU&#xff09;、存储器&#xff08;如闪存、EEPROM、RAM等&#xff09;、输入…

校园wifi网页认证登录入口

很多校园wifi网页认证登录入口是1.1.1.1 连上校园网在浏览器写上http://1.1.1.1就进入了校园网 使 用 说 明 一、帐户余额 < 0.00元时&#xff0c;帐号被禁用&#xff0c;需追加网费。 二、在计算中心机房上机的用户&#xff0c;登录时请选择新建帐号时给您指定的NT域&…

windows 搭建ssh服务

1、官网下载安装包&#xff1a;mls-software.com 2、点击安装&#xff08;一直默认即可&#xff09; 3、配置 opensshServer 4、成功登录