当前位置: 首页 > news >正文

【golang】调度系列之m

调度系列
调度系列之goroutine

上一篇中介绍了goroutine,最本质的一句话就是goroutine是用户态的任务。我们通常说的goroutine运行其实严格来说并不准确,因为任务只能执行。那么goroutine是被谁执行呢?是被m执行。

在GMP的架构中,m代表的是主动执行的能力,一个m对应的是一个线程。注意的是m只是对应操作系统的线程,因为线程是由操作系统来管理的,但是在用户态中我们可以通过一些同步机制来实现一定程度的操纵。

同样类比一个任务系统的话,goroutine对应task,m对应的就是worker。任务系统中创建一定数量的worker,worker获取task并执行,循环往复。通常在简单的任务系统中,只有worker和task两个对象完全可以胜任,所有task出于全局的队列(或者其他数据结构中)。golang的调度系统最开始也确实是GM架构。但是golang的调度体系显然不属于简单的任务系统,所以go在G和M中增加了一个中间层P。P对应的是执行的权限、执行的资源,这个会在下篇介绍。

文章目录

  • m的状态图
  • m的操作
    • newm
    • mstart
    • mexit
    • startm
    • stopm
  • m的对象

m的状态图

在介绍具体的细节前,同样先来一个整体的状态图。
需要说明的是,m不同于g,g有明确的status字段来记录状态,m没有记录状态的字段。虽然m没有status字段以及可枚举的状态值,但仍然可以抽象出相应的状态,来做状态的流转。
先介绍下几个状态值的含义。

  • running。
    表示m在运行中。处于running状态的m在执行某个goroutine或者在调用findrunnable寻找可执行的goroutine。需要注意的是,m处于running状态时,其g可能会处于running状态或者syscall状态。
  • spinning。
    表示m处于自旋状态,m有spinning字段表示是否处于自旋状态。此时系统中没有goroutine可执行时,但是m不会立即挂起,而是尝试寻找可执行的任务。spinning的设计是为了减少线程的切换,因为线程切换的损耗是比较高的。
  • idle。
    表示m处于空闲状态。此时m位于全局的队列(schedt.midle)中,对应的线程阻塞在condition上,等待唤醒。通常来说,m会在尝试spinning后再切换为idle。但是go中对最大的spinning的数量做了限制,如果正在spining的数量过多,则会直接转换为idle。

m开始创建时会处于running或者spinning状态(哪些情况下会处于spinning状态还不确定)。

当running状态的m找不到可执行的goroutine时,会切换为spinning状态,spinning一段时间后会转变为idle;另一个种情况时,当m从系统调用中返回时,获取不到p,则会转换为spinning状态。

当然我们上面也说过,处于spining状态的m的数量是有限制的,当达到这个限制,running会直接转变为idle。当需要新的m时,会先尝试从schedt.midle这个队列中获取m,如果没有再通过newm进行创建。

m流转的大概情况如此,下面我们来介绍细节。

m的操作

m的操作中,主要涉及到newm、mstart、mexit、startm等几个方法,下面逐一进行介绍。

newm

newm是创建m的入口(应该也是唯一的入口)。newm创建m对象,并将其同os线程关联起来运行,fn为传入的运行的函数。在某些情况下(这里暂时不深究),不能直接创建os线程,通过newmHandoff来操作,代码块中略过。

// src/proc.go 2096
func newm(fn func(), _p_ *p, id int64) {// allocm adds a new M to allm, but they do not start until created by// the OS in newm1 or the template thread.//// doAllThreadsSyscall requires that every M in allm will eventually// start and be signal-able, even with a STW.//// Disable preemption here until we start the thread to ensure that// newm is not preempted between allocm and starting the new thread,// ensuring that anything added to allm is guaranteed to eventually// start.acquirem()mp := allocm(_p_, fn, id)mp.nextp.set(_p_)mp.sigmask = initSigmaskif gp := getg(); gp != nil && gp.m != nil && (gp.m.lockedExt != 0 || gp.m.incgo) && GOOS != "plan9" {...}newm1(mp)releasem(getg().m)
}

newm函数开始时,首先调用acquirem来防止发生抢占,并在结束时调用releasem来解锁。acquirem和releasem是通过对m的locks字段进行操作来达成目的的。

//go:nosplit
func acquirem() *m {_g_ := getg()_g_.m.locks++return _g_.m
}//go:nosplit
func releasem(mp *m) {_g_ := getg()mp.locks--if mp.locks == 0 && _g_.preempt {// restore the preemption request in case we've cleared it in newstack_g_.stackguard0 = stackPreempt}
}

之后调用allocm创建m对象,并做一些初始化的操作,主要是为g0和gsignal分配内存。 g0在上一篇介绍g的时候提到过,这是和每个m绑定的,主要执行系统任务,协程调度等任务都是在g0中执行的。gsignal是为信号处理分配的栈。然后会将m加入全局的队列(allm)中。allocm的代码这里就不贴了,感兴趣可以自己查看。

allocm创建的m调用newm1函数运行。忽略cgo的部分。newm1中调用了newosproc方法来运行m。

func newm1(mp *m) {if iscgo {...}execLock.rlock() // Prevent process clone.newosproc(mp)execLock.runlock()
}

newosproc调用了一些真正的底层方法,在准备工作(略过)之后调用pthread_create创建了os线程。os线程执行的入口为mstart_stub,其会指向mstart,创建的m作为参数传入。通过这里就讲os线程同m关联起来了。

// glue code to call mstart from pthread_create.
func mstart_stub()
// May run with m.p==nil, so write barriers are not allowed.
//
//go:nowritebarrierrec
func newosproc(mp *m) {// 忽略准备工作....// Finally, create the thread. It starts at mstart_stub, which does some low-level// setup and then calls mstart.var oset sigsetsigprocmask(_SIG_SETMASK, &sigset_all, &oset)err = pthread_create(&attr, abi.FuncPCABI0(mstart_stub), unsafe.Pointer(mp))sigprocmask(_SIG_SETMASK, &oset, nil)if err != 0 {write(2, unsafe.Pointer(&failthreadcreate[0]), int32(len(failthreadcreate)))exit(1)}
}

mstart

newm是创建m的入口,mstart是m执行的入口。mstart是汇编实现,调用了mstart0。

// mstart is the entry-point for new Ms.
// It is written in assembly, uses ABI0, is marked TOPFRAME, and calls mstart0.
func mstart()

mstart0初始化了栈相关的字段,是我们在goroutine中提到的stackguard0字段。这里getg()得到的应该是对应m的g0。然后调用mstart1。最后调用mexit。需要注意的是mstart1是不会返回的(这点下面详细介绍),所以不用担心mexit一下就执行了。

func mstart0() {_g_ := getg()osStack := _g_.stack.lo == 0if osStack {...}// Initialize stack guard so that we can start calling regular// Go code._g_.stackguard0 = _g_.stack.lo + _StackGuard// This is the g0, so we can also call go:systemstack// functions, which check stackguard1._g_.stackguard1 = _g_.stackguard0mstart1()// Exit this thread.if mStackIsSystemAllocated() {// Windows, Solaris, illumos, Darwin, AIX and Plan 9 always system-allocate// the stack, but put it in _g_.stack before mstart,// so the logic above hasn't set osStack yet.osStack = true}mexit(osStack)
}

mstart1保证是非内联的,这是为了保证能够记录mstart调用mstart1时的执行状态(pc和sp),将其保存在g0.sched中。这样调用gogo(&g0.sched)能够回到mstart该节点继续执行,后面的就会执行mexit。保证m的退出能够执行mexit。

mstart1中会先调用fn,然后调用schedule。g的介绍中提到过schedule方法是不会返回的,也是前面提到mstart1不会返回的原因。此时,m真正进入不断寻找就绪的g并执行的过程中,也进入了状态图中running、spinning、idle之间不断状态流转的过程中。

// The go:noinline is to guarantee the getcallerpc/getcallersp below are safe,
// so that we can set up g0.sched to return to the call of mstart1 above.
//
//go:noinline
func mstart1() {_g_ := getg()if _g_ != _g_.m.g0 {throw("bad runtime·mstart")}// Set up m.g0.sched as a label returning to just// after the mstart1 call in mstart0 above, for use by goexit0 and mcall.// We're never coming back to mstart1 after we call schedule,// so other calls can reuse the current frame.// And goexit0 does a gogo that needs to return from mstart1// and let mstart0 exit the thread._g_.sched.g = guintptr(unsafe.Pointer(_g_))_g_.sched.pc = getcallerpc()_g_.sched.sp = getcallersp()asminit()minit()// Install signal handlers; after minit so that minit can// prepare the thread to be able to handle the signals.if _g_.m == &m0 {mstartm0()}if fn := _g_.m.mstartfn; fn != nil {fn()}if _g_.m != &m0 {acquirep(_g_.m.nextp.ptr())_g_.m.nextp = 0}schedule()
}

mexit

mexit主要是做一些释放资源的操作,包括:将分配的栈内存释放、从全局的队列中移除m、将持有的p释放移交,然后退出os线程。这里就不做过多的详细的介绍。代码也不贴了,位于 src/go/proc.go 1471

startm

newm是创建m的唯一入口,但实际上大多数时候需要m的时候都是调用了startm。startm和newm的唯一区别时,其会先去全局的空闲队列里寻找,如果找不到再去调用newm进行创建。如果找到了,则获取idle的m,并唤醒该m。

//go:nowritebarrierrec
func startm(_p_ *p, spinning bool) {mp := acquirem()lock(&sched.lock)if _p_ == nil {_p_, _ = pidleget(0)if _p_ == nil {unlock(&sched.lock)if spinning {// The caller incremented nmspinning, but there are no idle Ps,// so it's okay to just undo the increment and give up.if int32(atomic.Xadd(&sched.nmspinning, -1)) < 0 {throw("startm: negative nmspinning")}}releasem(mp)return}}nmp := mget()if nmp == nil {// No M is available, we must drop sched.lock and call newm.// However, we already own a P to assign to the M.//// Once sched.lock is released, another G (e.g., in a syscall),// could find no idle P while checkdead finds a runnable G but// no running M's because this new M hasn't started yet, thus// throwing in an apparent deadlock.//// Avoid this situation by pre-allocating the ID for the new M,// thus marking it as 'running' before we drop sched.lock. This// new M will eventually run the scheduler to execute any// queued G's.id := mReserveID()unlock(&sched.lock)var fn func()if spinning {// The caller incremented nmspinning, so set m.spinning in the new M.fn = mspinning}newm(fn, _p_, id)// Ownership transfer of _p_ committed by start in newm.// Preemption is now safe.releasem(mp)return}unlock(&sched.lock)if nmp.spinning {throw("startm: m is spinning")}if nmp.nextp != 0 {throw("startm: m has p")}if spinning && !runqempty(_p_) {throw("startm: p has runnable gs")}// The caller incremented nmspinning, so set m.spinning in the new M.nmp.spinning = spinningnmp.nextp.set(_p_)notewakeup(&nmp.park)// Ownership transfer of _p_ committed by wakeup. Preemption is now// safe.releasem(mp)
}

stopm

stopm是用来挂起m,其内容也比较简单。将m放置到全局的空闲队列中,然后调用mPark。mPark是一个阻塞的操作,其会阻塞在信号(m.park)上,等待唤醒,然后获取P继续执行。

// Stops execution of the current m until new work is available.
// Returns with acquired P.
func stopm() {_g_ := getg()if _g_.m.locks != 0 {throw("stopm holding locks")}if _g_.m.p != 0 {throw("stopm holding p")}if _g_.m.spinning {throw("stopm spinning")}lock(&sched.lock)mput(_g_.m)unlock(&sched.lock)mPark()acquirep(_g_.m.nextp.ptr())_g_.m.nextp = 0
}
// mPark causes a thread to park itself, returning once woken.
//
//go:nosplit
func mPark() {gp := getg()notesleep(&gp.m.park)noteclear(&gp.m.park)
}

m的对象

m对应结构体的具体的代码就不贴了,这里就挑一些字段进行介绍。有后面涉及到的字段再来补充。


写在最后
本篇呢,依旧是只聚焦于m本身。同样的道理,抛开G和P,很难讲到面面俱到。但是同样的,读完本篇,相信对m也会有一个本质的理解。m就是一个worker,其同一个os线程关联。我们会将活跃的m的数量控制在一定的范围,以避免过多的切换造成不必要的损耗。m在不同条件下会在running、spinning、idle之间进行状态的转换。我们通过不同的队列以及一些同步机制在用户态来管理m。下面可能还会有一篇来补充些M相关的内容,然后再开始P的介绍。

相关文章:

【golang】调度系列之m

调度系列 调度系列之goroutine 上一篇中介绍了goroutine&#xff0c;最本质的一句话就是goroutine是用户态的任务。我们通常说的goroutine运行其实严格来说并不准确&#xff0c;因为任务只能被执行。那么goroutine是被谁执行呢&#xff1f;是被m执行。 在GMP的架构中&#xff…...

可持久化线段树

可持久化线段树 模板 在某一指定版本的单点查&#xff0c;单点修。 开 m m m 棵线段树&#xff0c;每次修改复制后单点修。时间复杂度 O ( m ( n log ⁡ n ) ) O(m(n\log n)) O(m(nlogn))&#xff0c;空间复杂度 O ( n m ) O(nm) O(nm)&#xff0c;不如暴力。 每次修改…...

运行 Node.js 与浏览器 JavaScript

浏览器和 Node.js 都使用 JavaScript 软件语言 - 但字面上的运行时环境是不同的。 Node.js(又名服务器端 JavaScript)与客户端 JavaScript 有许多相似之处。它也有很多差异。 尽管两者都使用 JavaScript 作为软件语言,但我们可以重点关注一些关键差异,这些差异使两者之间…...

File类操作

1. 练习一 在当前模块下的 text 文件夹中创建一个 io.txt 文件 import java.io.File; import java.io.IOException;public class Practice1 {public static void main(String[] args) {File file new File("D:\\kaifamiao");File file1 new File(file, "tex…...

C# 实现电子签名

本项目基于Emgu.CV&#xff08;C#下OpenCv的封装&#xff09;开发的&#xff0c;编译器最新版Vs2022&#xff0c;编译环境x86 直接看效果图 1.主页面 2.我们先看手写的方式&#xff1a; 点击确认就到主界面&#xff0c;如下 &#xff1a; 点击自动适配-&#xff0c;再点击生成…...

小米6/6X/米8/米9手机刷入鸿蒙HarmonyOS.4.0系统-刷机包下载-遥遥领先

小米手机除了解锁root权限&#xff0c;刷GSI和第三方ROM也是米粉的一大爱好&#xff0c;这不&#xff0c;在华为发布了HarmonyOS.4.0系统后不久&#xff0c;我们小米用户也成功将自己的手机干山了HarmonyOS.4.0系统。虽然干上去HarmonyOS.4.0系统目前BUG非常多&#xff0c;根本…...

集合框架和泛型二

一、Set接口 1. Set接口概述 java.util.Set 不包含重复元素的集合、不能保证存储的顺序、只允许有一个 null。 public interface Set<E> extends Collection<E>抽象方法&#xff0c;都是继承自 java.util.Collection 接口。 Set 集合的实现类有很多&#xff0c;…...

thinkphp6 入门教程合集(更新中)

thinkphp6 入门&#xff08;1&#xff09;--安装、路由规则、多应用模式 thinkphp6 入门&#xff08;1&#xff09;--安装、路由规则、多应用模式_软件工程小施同学的博客-CSDN博客 thinkphp6 入门&#xff08;2&#xff09;--视图、渲染html页面、赋值 thinkphp6 入门&#…...

openGauss学习笔记-65 openGauss 数据库管理-创建和管理数据库

文章目录 openGauss学习笔记-65 openGauss 数据库管理-创建和管理数据库65.1 前提条件65.2 背景信息65.3 注意事项65.4 操作步骤65.4.1 创建数据库65.4.2 查看数据库65.4.3 修改数据库65.4.4 删除数据库 openGauss学习笔记-65 openGauss 数据库管理-创建和管理数据库 65.1 前提…...

mysql、MHA高可用配置即故障切换

MHA概述 一套优秀的MySQL高可用环境下故障切换和主从复制的软件 MHA的出现就是解决MySQL 单点的问题 MySQL故障过程中&#xff0c;MHA能做到0-30秒内自动完成故障切换 MHA能在故障切换的过程中最大程度上保证数据的一致性以达到真正意义上的高可用 MHA的组成&#xff08;核…...

使用“vue init mpvue/mpvue-quickstart“初始化mpvue项目时出现的错误及解决办法

当使用"vue init mpvue/mpvue-quickstart"初始化 mpvue 项目时出现 "vue-cli Failed to download repo mpvue/mpvue-quickstart: connect ETIMEDOUT IP地址"原因是 github 的 IP 解析失败&#xff0c;连接超时 解决办法&#xff1a;更改最新的 github 的 …...

Linux-Shell整理集合

Shell变量 参考文章&#xff1a; Shell脚本中变量的使用 shell语法之 , ‘ ‘ , {},, ,‘‘,(),$(())四种语法含义 参考文章&#xff1a; shell语法之 , ‘ ‘ , {},, ,‘‘,(),$(())四种语法含义 grep常用用法 Shell awk命令详解 grep 跟awk连着用&#xff1a; 获取某程序的…...

windows环境下node安装教程(超详细)

安装node.js 1、下载node: 下载地址&#xff1a;下载 | Node.js 中文网 node.js的zip包安装时是直接解压缩后就可以了, node.js的msi包是傻瓜式一路next就可以了 选择一中方式就可以 2、解压后的目录,或者mis安装后的目录如下: 3、安装完后&#xff0c;可以在命令行中输入…...

《TCP/IP网络编程》阅读笔记--并发多进程服务端的使用

目录 1--并发服务器端 2--进程 2-1--进程的相关概念 2-2--fork()创建进程 2-3--僵尸进程 2-4--wait()和waitpid()销毁僵尸进程 3--信号处理 3-1--signal()函数 3-2--sigaction()函数 3--3--利用信号处理技术消灭僵尸进程 4--基于多任务的并发服务器 5--分割 TCP 的…...

【C++】day2学习成果:引用、结构体等等。。。

1.封装一个结构体&#xff0c;结构体中包含一个私有数组&#xff0c;用来存放学生的成绩&#xff0c;包含一个私有变量&#xff0c;用来记录学生个数&#xff0c; 提供一个公有成员函数&#xff0c;void setNum(int num)用于设置学生个数 提供一个公有成员函数&#xff1a;void…...

QT 第五天 TCP通信与数据库

一、数据库增删改查 QT core gui sqlgreaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c11# The following define makes your compiler emit warnings if you use # any Qt feature that has been marked deprecated (the exact warnings # depend on your comp…...

Java程序中常用的设计模式有哪些和该种设计模式解决的痛点

设计模式是大量程序员智慧的结晶&#xff0c;是优秀的代码范式&#xff0c;是以前那些大佬程序员的编程经验总结&#xff0c;非常值得学习。 在软件开发中&#xff0c;有许多常用的设计模式&#xff0c;每种模式都解决了特定类型的问题。以下是一些常见的设计模式及其简要介绍&…...

Android12之解析/proc/pid进程参数(一百六十四)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…...

正儿八经的雅思口语盘丝洞大法学习总结(长期修改更新)针对23.9月考生

目录 开篇语 李仙童口语大法 具体体系内容 说道科技产品或者说非传统物品 part2回答八大准则 【part2回答八大准则】&#xff08;一&#xff09; 【part2回答八大准则】&#xff08;二&#xff09; 【part3回答七大准则】&#xff08;一&#xff09; Part 1 核心体系 …...

算法竞赛入门【码蹄集新手村600题】(MT1260-1280)C语言

算法竞赛入门【码蹄集新手村600题】(MT1260-1280&#xff09;C语言 目录MT1260 袋鼠躲猫猫MT1261 留下来的才是幸运数MT1262 约数MT1263 最大的三位约数MT1264 完数MT1265 区间完数MT1266 完数与因子MT1267 亏数MT1268 因数的因数MT1269 区间素数MT1270 素数计算MT1271 三生质数…...

qt连接tcp通信和连接数据库

通过数据库实现学生管理系统 widget.cpp #include "widget.h" #include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget) {ui->setupUi(this);//判断数据库对象是否包含了自己使用的数据库 Studemt.dbif(!db.co…...

MySQL Oracle区别

由于SQL Server不常用&#xff0c;所以这里只针对MySQL数据库和Oracle数据库的区别 (1) 对事务的提交 MySQL默认是自动提交&#xff0c;而Oracle默认不自动提交&#xff0c;需要用户手动提交&#xff0c;需要在写commit;指令或者点击commit按钮 (2) 分页查询 MySQL是直接在SQL语…...

Figma实用插件速收藏!精选19个干货插件大公开!

Figma 如今有着大量的插件&#xff0c;在 UI/UX 设计领域&#xff0c;很多工作已经不用真的从零开始做了。用好 Figma 插件&#xff0c;往往能让设计工作事半功倍。不过其中的插件素质差别很大&#xff0c;需要仔细筛选。不过如果你选择了对的插件&#xff0c;合理的设置&#…...

【STM32】FSMC—扩展外部 SRAM 初步使用 1

基于野火指南者《零死角玩转 STM32F103—指南者》的学习 STM32F103系列 FSMC Flexible Static Memory Controller简介 1.详细功能参看《STM32F10x参考手册》&#xff0c;这边是概述 是一个外设&#xff0c;挂载在AHB总线下。 可以用于驱动包括 SRAM、NOR FLASH 以及 NAND FL…...

保姆级教程 --redis启动命令

1、在redis目录 打开命令 windowr 输入cmd 2、输入 redis-server.exe redis.windows.conf 启动redis命令&#xff0c;看是否成功 3、可能会启动失败&#xff0c;报28 Nov 09:30:50.919 # Creating Server TCP listening socket 127.0.0.1:6379: bind: No error 4、报错后&am…...

【C++】构造函数分类 ① ( 构造函数分类简介 | 无参构造函数 | 有参构造函数 | 拷贝构造函数 | 代码示例 - 三种类型构造函数定义与调用 )

文章目录 一、构造函数分类1、构造函数分类简介2、构造函数分类代码分析无参构造函数有参构造函数拷贝构造函数 二、代码示例 - 三种类型构造函数定义与调用 一、构造函数分类 1、构造函数分类简介 C 构造函数可以分为以下几类 : 无参构造函数 : 最简单也是默认的构造函数 , 函…...

胡焕庸线,我国东西地级市分布密度分界线

背景 黑河—腾冲线&#xff0c;又名胡焕庸线&#xff0c;是我国人口密度分布的东西近似分界线。今天把地级市坐标分布密度做成热力图&#xff0c;并与胡焕庸线一起展示时&#xff0c;惊奇的发现&#xff0c;胡焕庸线貌似也是我车东西地级市分布密度的分界线。 生成folium地图…...

里氏替换原则在继承关系中子类对父类方法的重写(覆盖)或重载时应遵循的规则

什么是里氏替换原则&#xff1a;只要父类能出现的地方子类就可以出现&#xff0c;而且 替换为子类也不会产生任何错误或异常&#xff0c;使用者可能根本就不需要知道是父类还是子类。但是&#xff0c;反过来就不行了&#xff0c;有子类出现的地方&#xff0c;父类未必就能适应。…...

【脑机接口开源数据处理包】brainflowBrainFlow是一个库,旨在获取,解析和分析脑电图,肌电图,心电图和其他类型的数据从生物传感器。

BrainFlow是一个库&#xff0c;旨在获取&#xff0c;解析和分析脑电图&#xff0c;肌电图&#xff0c;心电图和其他类型的数据从生物传感器。 brainflow开源库官网地址 [https://brainflow.readthedocs.io/en/stable/](https://brainflow.readthedocs.io/en/stable/) 它提供了…...

#452. 序列操作

序列操作 - 题目 - Daimayuan Online Judge 问题描述&#xff1a; 思路&#xff1a;首先想的是第二次操作的y可以将前面所以操作进行抵消&#xff0c;只需要第二次操作的最大值即可。但是发现&#xff0c;对于第一个操作&#xff0c;它是单点修改&#xff0c;每修改一次对于第…...

wordpress SORA 主题/营销自动化

最近做课程作业&#xff0c;需求解TSP问题&#xff08;旅行商问题&#xff09;&#xff0c;数据集格式均是.tsp格式的&#xff0c;下面就用pandas来进行数据的加载&#xff0c;并转换成列表形式。 具体步骤 1、查看源数据 在pycharm中可以打开tsp文件&#xff0c;可以发现&a…...

活动 wordpress主题/网络软文范文

前言在前端项目的规模和复杂性不断提升的情况下&#xff0c;各类构建思想和相应工具层出不穷。本文竭己所能对比了当下13个构建工具&#xff0c;包括 Browserify、 Webpack、 Rollup、 Grunt、 Gulp和 Yeoman6个广为流行的工具&#xff0c; FIS、 Athena、 WeFlow和 Cooking等4…...

深圳网站建设与推广/怎么推广app

事务代码&#xff1a;S_ABA_72000164 支持BDT导航和分析。...

pc网站是什么/百度2022最新版本

简介&#xff1a; 平台简介 前端采用Vue、Element UI。 后端采用Spring Boot、Spring Security、Redis & Jwt。 权限认证使用Jwt&#xff0c;支持多终端认证系统。 支持加载动态权限菜单&#xff0c;多方式轻松权限控制。 高效率开发&#xff0c;使用代码生成器可以一键生…...

延吉网站建设公司/萧山seo

前言&#xff1a; 本文是前一片文章《深入浅出之正则表达式&#xff08;一&#xff09;》的续篇&#xff0c;在本文中讲述了正则表达式中的组与向后引用&#xff0c;先前向后查看&#xff0c;条件测试&#xff0c;单词边界&#xff0c;选择符等表达式及例子&#xff0c;并…...

电脑做网站空间/windows优化大师是系统软件吗

对于python列表里元素的操作主要分为以下几个方面&#xff1a; 1、向列表里面加元素&#xff1b; 向python列表里面添加元素主要有三种方法&#xff1a; &#xff08;1&#xff09;append&#xff08;&#xff09; append()对于列表的操作主要实现的是在特定的列表最后添加一个…...