Linux:Linux线程池

news2024/9/21 16:40:11

目录

线程池的概念

线程池的优点

线程池的应用场景

线程池的实现

线程池演示


线程池的概念

线程池是一种线程使用模式。

线程过多会带来调度开销,进而影响缓存局部和整体性能,而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。


线程池的优点

  • 线程池避免了在处理短时间任务时创建与销毁线程的代价。
  • 线程池不仅能够保证内核充分利用,还能防止过分调度。

注意: 线程池中可用线程的数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。


线程池的应用场景

线程池常见的应用场景如下:

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。

相关解释:

  • 像Web服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。
  • 对于长时间的任务,比如Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  • 突发性大量客户请求,在没有线程池的情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,但短时间内产生大量线程可能使内存到达极限,出现错误。

线程池的实现

线程池本质上就是一个生产者消费者模型,其中包括了一个任务队列与若干线程

  • 线程池中的多个线程负责从任务队列当中拿任务,并将拿到的任务进行处理。
  • 线程池对外提供一个Push接口,用于让外部线程能够将任务Push到任务队列当中。

线程池的代码如下:

#pragma once

#include <iostream>
#include <unistd.h>
#include <queue>
#include <pthread.h>

#define NUM 5

//线程池
template<class T>
class ThreadPool
{
private:
	bool IsEmpty()
	{
		return _task_queue.size() == 0;
	}
	void LockQueue()
	{
		pthread_mutex_lock(&_mutex);
	}
	void UnLockQueue()
	{
		pthread_mutex_unlock(&_mutex);
	}
	void Wait()
	{
		pthread_cond_wait(&_cond, &_mutex);
	}
	void WakeUp()
	{
		pthread_cond_signal(&_cond);
	}
public:
	ThreadPool(int num = NUM)
		: _thread_num(num)
	{
		pthread_mutex_init(&_mutex, nullptr);
		pthread_cond_init(&_cond, nullptr);
	}
	~ThreadPool()
	{
		pthread_mutex_destroy(&_mutex);
		pthread_cond_destroy(&_cond);
	}
	//线程池中线程的执行例程
	static void* Routine(void* arg)
	{
		pthread_detach(pthread_self());
		ThreadPool* self = (ThreadPool*)arg;
		//不断从任务队列获取任务进行处理
		while (true){
			self->LockQueue();
			while (self->IsEmpty()){
				self->Wait();
			}
			T task;
			self->Pop(task);
			self->UnLockQueue();
			
			task.Run(); //处理任务
		}
	}
	void ThreadPoolInit()
	{
		pthread_t tid;
		for (int i = 0; i < _thread_num; i++){
			pthread_create(&tid, nullptr, Routine, this); //注意参数传入this指针
		}
	}
	//往任务队列塞任务(主线程调用)
	void Push(const T& task)
	{
		LockQueue();
		_task_queue.push(task);
		UnLockQueue();
		WakeUp();
	}
	//从任务队列获取任务(线程池中的线程调用)
	void Pop(T& task)
	{
		task = _task_queue.front();
		_task_queue.pop();
	}
private:
	std::queue<T> _task_queue; //任务队列
	int _thread_num; //线程池中线程的数量
	pthread_mutex_t _mutex;
	pthread_cond_t _cond;
};

为什么线程池中需要有互斥锁和条件变量?

线程池中的任务队列是会被多个执行流同时访问的临界资源,因此我们需要引入互斥锁对任务队列进行保护。

线程池当中的线程要从任务队列里拿任务,前提条件是任务队列中必须要有任务,因此线程池当中的线程在拿任务之前,需要先判断任务队列当中是否有任务,若此时任务队列为空,那么该线程应该进行等待,直到任务队列中有任务时再将其唤醒,因此我们需要引入条件变量。

当外部线程向任务队列中Push一个任务后,此时可能有线程正处于等待状态,因此在新增任务后需要唤醒在条件变量下等待的线程。

  • 当某线程被唤醒时,其可能是被异常或是伪唤醒,或者是一些广播类的唤醒线程操作而导致所有线程被唤醒,使得在被唤醒的若干线程中,只有个别线程能拿到任务。此时应该让被唤醒的线程再次判断是否满足被唤醒条件,所以在判断任务队列是否为空时,应该使用while进行判断,而不是if。
  • pthread_cond_broadcast函数的作用是唤醒条件变量下的所有线程,而外部可能只Push了一个任务,我们却把全部在等待的线程都唤醒了,此时这些线程就都会去任务队列获取任务,但最终只有一个线程能得到任务。一瞬间唤醒大量的线程可能会导致系统震荡,这叫做惊群效应。因此在唤醒线程时最好使用pthread_cond_signal函数唤醒正在等待的一个线程即可。
  • 当线程从任务队列中拿到任务后,该任务就已经属于当前线程了,与其他线程已经没有关系了,因此应该在解锁之后再进行处理任务,而不是在解锁之前进行。因为处理任务的过程可能会耗费一定的时间,所以我们不要将其放到临界区当中。
  • 如果将处理任务的过程放到临界区当中,那么当某一线程从任务队列拿到任务后,其他线程还需要等待该线程将任务处理完后,才有机会进入临界区。此时虽然是线程池,但最终我们可能并没有让多线程并行的执行起来。

为什么线程池中的线程执行例程需要设置为静态方法?

使用pthread_create函数创建线程时,需要为创建的线程传入一个Routine(执行例程),该Routine只有一个参数类型为void*的参数,以及返回类型为void*的返回值。

而此时Routine作为类的成员函数,该函数的第一个参数是隐藏的this指针,因此这里的Routine函数,虽然看起来只有一个参数,而实际上它有两个参数,此时直接将该Routine函数作为创建线程时的执行例程是不行的,无法通过编译。

静态成员函数属于类,而不属于某个对象,也就是说静态成员函数是没有隐藏的this指针的,因此我们需要将Routine设置为静态方法,此时Routine函数才真正只有一个参数类型为void*的参数。

但是在静态成员函数内部无法调用非静态成员函数,而我们需要在Routine函数当中调用该类的某些非静态成员函数,比如Pop。因此我们需要在创建线程时,向Routine函数传入的当前对象的this指针,此时我们就能够通过该this指针在Routine函数内部调用非静态成员函数了。

日志模块

完整的日志功能至少有日志等级、时间。最好是支持用户自定义(日志内容, 文件行,文件名等) 

#pragma once
#include <iostream>
#include <string>
#include <cstdio>
#include <cstdarg>
#include <ctime>
 
//日志级别
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4
const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};
 
void LogMessage(int level, const char *format, ...)
{
#ifndef DEBUG_SHOW
    if(level== DEBUG) return;
#endif
    //标准部分
    char stdBuffer[1024];
    const time_t timestamp = time(nullptr);
    struct tm* local_time = localtime(&timestamp);
    snprintf(stdBuffer, sizeof stdBuffer, "[%s] [%d-%d-%d-%d-%d-%d] ", gLevelMap[level], 
        local_time->tm_year + 1900, local_time->tm_mon + 1, local_time->tm_mday, local_time->tm_hour, local_time->tm_min, local_time->tm_sec);
 
    //自定义部分
    char logBuffer[1024]; 
    va_list args;
    va_start(args, format);
    vsnprintf(logBuffer, sizeof logBuffer, format, args);
    va_end(args);
    printf("%s%s\n", stdBuffer, logBuffer);
}

任务模块

线程池中存储的是一个个任务,下面将任务进行封装

无论该任务是什么类型的,在该任务类中都必须包含仿函数,当处理该类型的任务时只需调用operator()即可

#pragma once
#include <iostream>
#include <string>
#include <functional>
#include "Log.hpp"
 
typedef std::function<int(int, int)> fun_t;
class Task
{
public:
    Task(){}
    Task(int x, int y, fun_t func):_x(x), _y(y), _func(func) {}
    void operator ()(const std::string &name) {
        LogMessage(NORMAL, "%s处理完成: %d+%d=%d", name.c_str(), _x, _y, _func(_x, _y));
    }
public:
    int _x;
    int _y;
    fun_t _func;
};

线程模块

由于系统调用接口过于复杂,线程模块完成的便是线程的封装,降低接口的调用复杂度,提高代码的阅读性并提高代码的复用性

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cstdio>
 
class ThreadDate
{
public:
    void* _args;
    std::string _name;
};
 
typedef void*(*func_t)(void*);
class Thread
{
public:
    Thread(size_t num,func_t callback,void* args): _function(callback)
    {
        char nameBuffer[64];
        snprintf(nameBuffer, sizeof nameBuffer, "Thread-%d", num);
        _threadDate._name = nameBuffer;
        _threadDate._args = args;
    }
    ~Thread() {}
    void Start() { pthread_create(&_tid, nullptr, _function, (void*)&_threadDate); }
    void Join() { pthread_join(_tid, nullptr); }
    std::string Name() { return _name; }
private:
    std::string _name;
    func_t _function;
    ThreadDate _threadDate;
    pthread_t _tid;
};

懒汉单例模式

整个工程中线程池应该只存在一个实例,可以使用单例模式进行实现。这里采用懒汉单例模式,其最核心的思想是"延时加载",从而能够优化服务器的启动速度

//示意代码
template <typename T>
class Singleton 
{
    static T* inst;
public:
    static T* GetInstance() {
        if (inst == NULL) {
            inst = new T();
        }     
        return inst;
    }
};

只有调用GetInstance()后,才会实例化出唯一的线程池实例。但存在一个严重的问题, 线程不安全。第一次调用GetInstance()时, 若多个线程同时调用, 可能会创建出多份 T 对象的实例

template <class T>
class Singleton 
{
    static T* inst;
    static std::mutex lock;
public:
    static T* GetInstance() {
        if (inst == NULL) { // 双重判定空指针, 降低锁冲突的概率, 提高性能.
            lock.lock(); // 使用互斥锁, 保证多线程情况下也只调用一次 new.
            if (inst == NULL) {
                inst = new T();
            } 
            lock.unlock();
        } 
        return inst;
    }
};

可能会有些疑惑,为什么需要判定两次是否为空指针?为什么不写成下面这样呢?

template <class T>
class Singleton 
{
    static T* inst;
    static std::mutex lock;
public:
    static T* GetInstance() {
        lock.lock();
        if (inst == NULL) {
            inst = new T();
        } 
        lock.unlock();
        return inst;
    }
};

因为第一个线程创建出唯一线程池实例后,后续可能依然会有该函数的调用(不是为了创建出唯一实例,而是为了获得唯一实例的地址),此时多线程就会涉及到竞争锁资源以及不断的加锁与解锁,造成时间与资源的浪费。使用双重if判断则可以避免某些情况下的锁竞争(已经存在唯一实例),从而提高性能。

锁防护装置模块

RAII风格,可避免在加锁区域抛异常而导致未解锁,出作用域自动解锁

#pragma once
#include <iostream>
#include <pthread.h>
 
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):_mutex(mtx) { _mutex.Lock(); }
    ~LockGuard() { _mutex.UnLock(); }
private:
    Mutex _mutex;
};

线程池实现

#pragma once
#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <unistd.h>
#include "Thread.hpp"
#include "LockGuard.hpp"
#include "Log.hpp"
 
//懒汉模式
const int g_threadNum = 3;
template<class T>
class ThreadPool
{
public://为routine()静态函数提供
    pthread_mutex_t *GetMutex() { return &_mutex; }
    bool isEmpty() { return _taskQueue.empty(); }
    void WaitCond() { pthread_cond_wait(&_cond, &_mutex); }
    T GetTask() {
        T task = _taskQueue.front();
        _taskQueue.pop();
        return task;
    }
 
public:
    //需考虑多线程申请单例的情况
    static ThreadPool<T>* GetThreadPool(int num = g_threadNum)
    {
        if(nullptr == pool_ptr) {
            {
                LockGuard lockguard(&_init_mutex);
                if(nullptr == pool_ptr) {
                    pool_ptr = new ThreadPool<T>(num);
                }
            }
        }
        return pool_ptr;
    }
 
    static void* Routine(void* args) {
        ThreadDate* thread_date = (ThreadDate*)args;
        ThreadPool<T>* thread_pool = (ThreadPool<T>*)thread_date->_args;
        while(true) {
            T task;
            {
                LockGuard lockguard(thread_pool->GetMutex());
                while(thread_pool->isEmpty()) thread_pool->WaitCond();
                task = thread_pool->GetTask();
            }
            task(thread_date->_name);//仿函数
        }
    }
    void PushTask(const T& task)
    {
        LockGuard lockguard(&_mutex);
        _taskQueue.push(task);
        pthread_cond_signal(&_cond);
    }
    void Run()
    {
        for(auto& iter : _threads) {
            iter->Start(); 
            LogMessage(DEBUG, "%s %s", iter->Name().c_str(), "启动成功");
        }
    }
    ~ThreadPool()
    {
        for (auto &iter : _threads) {
            iter->Join();
            delete iter;
        }
        pthread_mutex_destroy(&_mutex);
        pthread_cond_destroy(&_cond);
    }
 
private:
    ThreadPool(int threadNum):_num(threadNum) {
        pthread_mutex_init(&_mutex, nullptr);
        pthread_cond_init(&_cond, nullptr);
        for (int i = 1; i <= _num; i++) {
            _threads.push_back(new Thread(i, Routine, this));
        }
    }
    ThreadPool(const ThreadPool<T>& others) = delete;
    ThreadPool<T>& operator= (const ThreadPool<T>& others) = delete;
 
private:
    std::vector<Thread*> _threads;
    size_t _num;
    std::queue<T> _taskQueue;
private:
    pthread_mutex_t _mutex;
    pthread_cond_t _cond;
private:
    static ThreadPool<T>* pool_ptr;//避免编译器自动优化
    static pthread_mutex_t _init_mutex;
};
 
template<typename T>
ThreadPool<T>* ThreadPool<T>::pool_ptr = nullptr;
template<typename T>
pthread_mutex_t ThreadPool<T>::_init_mutex = PTHREAD_MUTEX_INITIALIZER;

线程池演示

主线程就负责不断向任务队列当中Push任务即可,此后线程池中的线程会从任务队列中获取任务并进行处理

#include "ThreadPool.hpp"
#include "Task.hpp"
 
int main()
{
    ThreadPool<Task>* threadPool = ThreadPool<Task>::GetThreadPool(5);
    threadPool->Run();
    while(true)
    {
        //生产的过程,制作任务的时候,要花时间
        int x = rand()%100 + 1;
        usleep(7721);
        int y = rand()%30 + 1;
        Task task(x, y, [](int x, int y)->int{
            return x + y;
        });
 
        LogMessage(NORMAL, "制作任务完成: %d+%d=?", x, y);
        //推送任务到线程池中
        threadPool->PushTask(task);
        sleep(1);
    }
    return 0;
}

运行代码后一瞬间就有六个线程,其中一个为主线程,另外五个是线程池内处理任务的线程

五个线程在处理时会呈现出一定的顺序性,因为主线程是每秒Push一个任务,五个线程中只会有一个线程获取到该任务,其他线程都会在等待队列中进行等待,当该线程处理完任务后就会因为任务队列为空而排到等待队列的最后,当主线程再次Push一个任务后会唤醒等待队列首部的一个线程,这个线程处理完任务后又会排到等待队列的最后,因此这五个线程在处理任务时会呈现出一定的顺序性(2-3-4-5-1)

注意:此后若想让线程池处理其他不同的任务请求时,只需要提供一个任务类,在该任务类当中提供对应的operator()方法即可。

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

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

相关文章

长视频生成研究的挑战、方法与前景

人工智能咨询培训老师叶梓 转载标明出处 长视频生成面临的主要挑战包括如何在有限的计算资源下生成长时间、高一致性、内容丰富且多样化的视频序列。另外现有研究中对于“长视频”的定义并不统一&#xff0c;这给研究的标准化和比较带来了困难。来自西安电子科技大学、上海交通…

Window 安装Gogs教程

1、下载 下载地址&#xff1a;https://gogs.io/docs/installation/install_from_binary.html(请自行科学上网 选择Windows amd64(64位)或者386(32位) 2、安装 2.1 将压缩文件放到目标文件夹 2.2 创建数据库 在本地数据库或者其他目标数据库新建查询执行下列SQL语句 找到go…

taskBus的设计局限和吞吐能力测试

在前文中&#xff0c;我们介绍了EPDR技术的起源&#xff0c;以及使用该技术驱动的业余软件无线电平台专栏。已有玩家通过踩坑证明&#xff0c;进程管道交换数据时间延迟大&#xff08;10ms&#xff09;&#xff0c;构造时间敏感系统难。除非采用传统的紧耦合设计及更大的颗粒度…

尚品汇-选中状态缓存变更、删除缓存购物车(三十八)

目录&#xff1a; &#xff08;1&#xff09;选中状态的变更 &#xff08;2&#xff09;删除购物车 &#xff08;3&#xff09;流程总结 &#xff08;1&#xff09;选中状态的变更 用户每次勾选购物车的多选框&#xff0c;都要把当前状态保存起来。由于可能会涉及更频繁的操…

基于AT89C51单片机的可手动定时控制的智能窗帘设计

点击链接获取Keil源码与Project Backups仿真图: https://download.csdn.net/download/qq_64505944/89469560?spm=1001.2014.3001.5503 C 源码+仿真图+毕业设计+实物制作步骤+11 摘要 I abstract II 第1章 绪论 1 1.1 背景及意义 1 1.2 国内外发展现状 1 1.3 设计思想及基…

ChatGPT等大模型高效调参大法——PEFT库的算法简介

随着ChatGPT等大模型&#xff08;Large Language Model&#xff09;的爆火&#xff0c;而且目前业界已经发现只有当模型的参数量达到100亿规模以上时&#xff0c;才能出现一些在小模型无法得到的涌现能力&#xff0c;比如 in_context learing 和 chain of thougt。深度学习似乎…

Excel如何快速的定位到某一列和快速知道当前列

Excel如何快速的定位到某一列和快速知道当前列 背景快速找到某一列---660列快速知道当前列 背景 由于某一次做excel数据太大需要快速知道某一列是多少列和快速定位到某一列对此写了这个 快速找到某一列—660列 SUBSTITUTE(ADDRESS(1, 660, 4), "1", ""…

实现MySQL的主从复制基础

目录 1 MySQL实现主从复制的原理 1.1 实现主从复制的规则 1.2 如何实现主从复制 2 MySQL 实现主从复制实践 2.1 实验环境 2.2 my.cnf 配置添加 2.2.1 配置MSTER 端配置文件 2.2.2 配置SLAVE 端配置文件 2.2.3 三台MySQL服务器重启服务 2.3 创建用于复制的用户 2.4 保证三台主机…

Android实战:过root检测

在启动这个app时&#xff0c;我们会看到一个提示&#xff0c;表示设备处于root环境。如下图所示&#xff1a; 为了过掉到这个root检测&#xff0c;我们可以通过直接Hook Toast.show()方法&#xff0c;并打印调用堆栈信息来实现定位关键代码。以下是相关的Frida脚本代码&#…

esxi 安装 精简版win10

镜像来源&#xff1a;[【不忘初心】Windows10 22H2 (19045.4780) X64 无更新 纯净[深度精简版]1.27G](https://www.pc528.net/22h2s.html) 提供下载地址&#xff1a;https://www.123pan.cn/s/lYtRVv-Wmuf3?提取码:GaD4 先把下载esd 转成iso安装 把下载的esd 重命名为install…

如何使用ssm实现学生宿舍管理

TOC ssm094学生宿舍管理jsp 绪论 1.1 研究背景 当前社会各行业领域竞争压力非常大&#xff0c;随着当前时代的信息化&#xff0c;科学化发展&#xff0c;让社会各行业领域都争相使用新的信息技术&#xff0c;对行业内的各种相关数据进行科学化&#xff0c;规范化管理。这样…

YOLOv5改进 | 融合改进 | C3融合EffectiveSE-Convolutional【完整代码 + 小白必备】

秋招面试专栏推荐 &#xff1a;深度学习算法工程师面试问题总结【百面算法工程师】——点击即可跳转 &#x1f4a1;&#x1f4a1;&#x1f4a1;本专栏所有程序均经过测试&#xff0c;可成功执行&#x1f4a1;&#x1f4a1;&#x1f4a1; 专栏目录&#xff1a; 《YOLOv5入门 改…

如何用comate快速生成一个剩菜好帮手

想法 上班后不想吃饭店的饭菜&#xff0c;时长想自己做一些饭菜&#xff0c;买完菜后却经常放到冰箱中&#xff0c;剩下的菜有无法一下子处理&#xff0c;单纯扔掉有些可惜&#xff0c;但是基于冰箱中的剩菜如何能做出一顿像样的饭菜一致困扰着我&#xff0c;查市面上的程序有…

在不修改应用数据源的情况下,如何确保应用程序能够正常访问adg切换后的主库?

在不修改应用数据源的情况下&#xff0c;如何确保应用程序能够正常访问adg切换后的主库&#xff1f; oracle12c rac测试通过&#xff1a; 1.修改原主库的scanip为某个临时ip&#xff0c;新主库的scanip修改为原生产 2.修改新主库的service_names&#xff1a;dgorcl为原生产的…

学习2d直线拟合

直线拟合算法&#xff08;续&#xff1a;加权最小二乘&#xff09;_加权拟合直线法-CSDN博客 直线拟合算法_相位拟合直线-CSDN博客 特别感谢博主无私分享 博文中提到的参考资料《机器视觉算法与应用&#xff08;双语版&#xff09;》[德] 斯蒂格&#xff08;Steger C&#x…

GPT-4o语音功能潜在风险分析与技术挑战

引言 近年来&#xff0c;随着大语言模型&#xff08;LLM&#xff09;技术的飞速发展&#xff0c;人工智能的能力在语音处理领域也取得了显著进展。OpenAI推出的GPT系列模型正成为人工智能领域的标杆。然而&#xff0c;在最新的GPT-4o版本中&#xff0c;尽管语音功能具备广阔的…

vue3 多文件下载zip压缩包

vue3多文件下载zip文件包 效果图 代码块 在这里插入代码片 <template><div><el-button type"primary" click"downLoadClick">下载文件zip</el-button></div> </template><script setup lang"ts"> i…

Springsecurity 自定义AuthenticationManager

一、认证流程 1、当用户提交了一个他的凭证(用户名、密码) AbstractAuthenticationProcessingFilter 将会创建一个凭证信息&#xff0c;最终&#xff0c;该请求会被UsernamePasswordAuthenticationFilter 拦截将请求中用户名和密码&#xff0c;封装为 Authentication 对象&…

4个学生党必备好用 AI 学术论文写作工具

随着人工智能技术的不断进步&#xff0c;AI论文写作工具已成为研究人员和学生的得力助手。学姐今天将介绍4个市面上广受好评的免费AI论文写作工具&#xff0c;它们能帮助用户高效地完成从论文大纲到最终校对的各个阶段。 一、梅子AI论文 梅子AI提供快速论文撰写功能&#xff…

Datawhale X 李宏毅苹果书 AI夏令营 学习笔记(二)

自适应学习率 我们梯度下降在参数更新上&#xff0c;公式是 W t W t − 1 − η g t &#xff0c; η 是学习率&#xff0c; g t 是梯度 W_tW_{t-1}-\eta g_t&#xff0c;\eta是学习率&#xff0c;g_t是梯度 Wt​Wt−1​−ηgt​&#xff0c;η是学习率&#xff0c;gt​是梯度…