GO语言学习(14)GO并发编程

news2025/4/9 4:30:44

目录

🌈前言

1.goroutine🌟 

2.GMP模型🌟 

2.1 GMP的由来☀️

        2.2 什么是GMP☀️

3.channel 🌟

3.1 通道声明与数据传输💥

3.2 通道关闭 💥

3.3 通道遍历 💥

3.4 Select语句 💥

4.同步并发工具🌟 

4.1 WaitGroup(协程等待组)💐

4.2 Mutex(互斥锁) 💐

4.3 RWMutex(读写锁) 💐


🌈前言

        并发是指程序同时执行多个任务的能力。Go 语言支持并发,通过 goroutines 和 channels 提供了一种简洁且高效的方式来实现并发。

1.goroutine🌟 

  •  定义:Goroutine是Go语言中实现并发的基本单位,是一种轻量级的线程,由Go运行时(runtime)负责调度。通过使用关键字go,我们可以启动一个新的goroutine
  • 特点

    • 轻量级:创建和切换的开销极小,启动和销毁的代价远低于传统的操作系统线程。

    • 高效调度:Go运行时使用M:N调度模型,将Goroutine调度到少量的操作系统线程上执行,提高了并发效率。

    • 简单的创建方式:只需在函数调用前加上go关键字即可启动一个新的Goroutine。

特性Goroutine (Go)操作系统进程 (Process)线程 (Thread)
定义Go 语言实现的轻量级用户态线程操作系统资源分配的基本单位操作系统调度的最小执行单元(属于进程)
调度方式Go 运行时调度(用户态协作+抢占)操作系统内核调度(抢占式)操作系统内核调度(抢占式)
创建开销极低(初始约 2KB 栈,动态扩展)高(独立内存、文件句柄、PCB 等)中(需内核分配资源,但共享进程内存)
切换开销极低(无内核参与,仅用户态切换)高(需内核态/用户态切换)中(需内核切换,但比进程轻量)
并发数量轻松支持数十万通常数百个(受系统资源限制)数千到数万(受内核限制)
内存占用共享堆,独立栈(可动态增长)独立地址空间(完全隔离)共享进程内存,独立栈
通信机制Channel(推荐)、共享内存(需同步)IPC(管道、信号、共享内存等)共享进程内存(需同步)
隔离性无(需自行管理共享数据)高(进程间完全隔离)低(同一进程内线程共享资源)
阻塞影响阻塞时自动切换其他 Goroutine阻塞整个进程阻塞所属进程的所有线程
并行能力支持(通过多线程绑定多核)支持(多进程在多核并行)支持(多线程在多核并行)
崩溃影响仅影响所在 Goroutine(除非未恢复)仅崩溃当前进程同一进程内所有线程终止
典型应用高并发 I/O 密集型(如 Web 服务器)计算密集型、需隔离的任务(如数据库)需要并发的计算密集型任务
跨平台性依赖 Go 运行时(抽象操作系统差异)所有操作系统通用所有操作系统通用(实现可能不同)

2.GMP模型🌟 

2.1 GMP的由来☀️

       我们知道软件都是跑在操作系统上,真正用来执行任务的是 CPU。早期的操作系统每个程序就是一个进程,直到一个程序运行完,才能进行下一个进程,一切的程序只能串行发生。这就是 “单进程时代”。

☄️不难看出上述操作系统,存在着两个主要的问题:

  1. 单一的执行流程,计算机只能一个任务一个任务处理。
  2. 进程阻塞所带来的 CPU 时间浪费。

        所以,之后便出现了 多进程 / 线程 时代 ,操作系统就具有了最早的并发能力:多进程并发,当一个进程阻塞的时候,切换到另外等待执行的进程,这样就能尽量把 CPU 利用起来,CPU 就不浪费了。

        在多进程 / 多线程的操作系统中,就解决了阻塞的问题,因为一个进程阻塞 cpu 可以立刻切换到其他进程中去执行,而且调度 cpu 的算法可以保证在运行的进程都可以被分配到 cpu 的运行时间片。这样从宏观来看,似乎多个进程是在同时被运行。

        但此时进程拥有太多的资源,进程的创建、切换、销毁,都会占用很长的时间,CPU 虽然利用起来了,但如果进程过多,CPU 有很大的一部分都被用来进行进程调度了。此时仍然存在以下问题:

  • 高内存占用
  • 调度的高消耗 CPU

        

        随后工程师们就发现,其实一个线程可以分为 内核态线程用户态线程。CPU并不知道“用户态”线程的存在,只知道执行“内核态”线程。此后,我们将内核线程叫 “线程 (thread)”,用户线程叫 “协程 (co-routine)”。

        协程跟线程是有区别的,线程由操作系统内核调度,并且线程切换需要保存和恢复上下文,线程的内存占用较大,其存在于内核空间,其并发能力有限,通常受操作系统支持的线程数量限制,线程编程需要处理复杂的同步和锁问题,容易出现竞态条件和死锁

        而协程存在于用户空间由用户态调度器管理,内存占用极小,调度开销较小。并且并发能力极高,协程之间可以通过共享内存或通道(channel)通信,通常不需要复杂的同步机制。而由GO语言创建的Goroutine与协程的主要不同在于Goroutine可能发生并行,是一种特殊的协程。

        在 Go 语言的 Goroutine 调度模型(GMP)中,M:N 模型 是指将多个 Goroutine(用户态协程)映射到少量的操作系统线程(M)上,从而实现高效的并发调度。

2.2 什么是GMP☀️

GMP实际上是三个单词的缩写:

  • 🌷Goroutine (G): Goroutine是Go语言的并发执行单元。它们非常轻量,初始栈空间小,可以动态伸缩。Goroutine的调度由Go运行时管理,而不是操作系统内核
  • 🌻Machine (M): Machine是操作系统的线程。在Go的并发模型中,M是执行Goroutine代码的实体。每个M都会被分配一个P,并从P的本地运行队列中获取G来执行
  • 🌼Processor (P): Processor是G和M之间的调度中介。每个P都有一个本地的Goroutine队列,负责维护和调度这些Goroutines到M上执行。P的数量通常等于机器的逻辑CPU数量

 下面通过图例具体说明:

 先介绍各个图标表示的含义,如下:

GMP模型架构图:

  • 全局队列(Global Queue):存放等待运行的 G。
  • P 的本地队列:同全局队列类似,存放的也是等待运行的 G,存的数量有限,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
  • P 列表:所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
  • M:线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
  • work stealing 机制:当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程
  • hand off 机制:当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行

 下面以一段示例代码进行说明:

package main

import (
	"fmt"
	"time"
)

func sayHello() {
	for i := 0; i < 5; i++ {
		fmt.Println("Hello")
		time.Sleep(100 * time.Millisecond)
	}
}

func main() {
	go sayHello() // 启动 Goroutine
	for i := 0; i < 5; i++ {
		fmt.Println("Main")
		time.Sleep(100 * time.Millisecond)
	}
}

上述例子并没有固定的输出,可能输出如下:

Main
Hello
Main
Hello
Main
Hello
...

🍓解释

1.初始阶段🥕:

  • Go 程序启动时,会创建一个主 Goroutine(对应 main() 函数)。

  • 默认创建一个 P(逻辑处理器)和绑定一个 M(系统线程)来执行主 Goroutine。

2.创建新的Goroutine🥕:

  • 执行  go sayHello() 将会创建一个新的协程,同样的会被放入当前P的本地队列当中,如果本地队列满了,则会放入全局队列当中。
  • 如果 Go 程序设置了 GOMAXPROCS > 1(默认等于 CPU 核心数),可能会有多个 M 同时运行不同的 G,实现并行。此时 go sayHello()创建的协程不一定会在原先主Goroutine的P的本地队列中,有可能会为其创建一个新的P的M。(本次假设GOMAXPROCS=1,如下图所示,之后图示也为此情况

⛅️GOMAXPROCS > 1时,情况可能如下,由于线程是并发进行,没有固定的调度顺序,所以程序的第一个输出的不一定是Main,也可能是 Hello。 

3.Goroutine 切换🥕:

  •  主 Goroutine(main())和 sayHello() Goroutine 交替执行。
  • 当主 Goroutine 调用 time.Sleep() 时,会主动让出 CPU,触发调度器切换到其他可运行的 G(如 sayHello()),此时退出的协程将进入休眠状态Gwaiting),并将其从当前 P 的 运行队列 中移出。
  • 此时,调度器会立即选择另一个 可运行的 G(从当前 P 的本地队列、全局队列或其他 P 偷取)继续执行。
  • 当休眠时间到期后,Go 运行时会将 G 重新标记为 可运行状态Grunnable),并放回 原来 P 的本地队列(或其他 P 的队列,取决于调度器的负载均衡策略)

👉 更一般的go func流程情况,可参考下图所示:

3.channel 🌟

3.1 通道声明与数据传输💥

        通道(Channel)是用于 Goroutine 之间的数据传递。通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。使用 make 函数创建一个 channel,使用 <- 操作符发送和接收数据。如果未指定方向,则为双向通道。并且chanel的数据结构本质为队列,数据满足先进先出(FIFO)的操作顺序

        而通道可分为带缓冲通道无缓冲通道,具体区别如下表所示,简单来说,无缓冲通道是"你发我必收",带缓冲通道是"你先发着,我慢慢收"。

特性无缓冲通道带缓冲通道
创建方式make(chan T)make(chan T, n),n为通道大小
容量0n > 0
发送阻塞条件没有接收者等待时缓冲区满时
接收阻塞条件没有数据时缓冲区空时
同步性强同步,发送接收必须同时就绪弱同步,允许短暂不同步
类比实时电话短信或邮件
  • 📞无缓冲通道:发送端发送数据,同时必须有接收端相应的接收数据。就好比两个人打电话,必须双方同时接通,通信才能进行。
  • 🔧带缓冲通道:允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。 

🌱示例代码:

package main
import "fmt"

func main() {
	// 无缓冲通道
	ch1 := make(chan int) // 容量为0
	go func() {
		ch1 <- 1 // 发送会阻塞,直到有人接收
	}()
	fmt.Println(<-ch1) // 接收

	// 带缓冲通道
	ch2 := make(chan int, 3) // 容量为3
	ch2 <- 1                 // 不会阻塞
	ch2 <- 2                 // 不会阻塞
	ch2 <- 3                 // 不会阻塞
	// ch2 <- 4  // 这会阻塞,因为缓冲区满了
	fmt.Println(<-ch2) // 取出1
	ch2 <- 4         // 不会阻塞,1已被取出
}

 🌷解释🌷:

3.2 通道关闭 💥

        通道可以通过close函数来关闭。一旦通道被关闭,就不能再向其发送数据,但仍然可以从通道中接收已有的数据,直到所有数据都被取出。 

close(ch)  // 关闭后无法发送,但仍可接收剩余数据
val, ok := <-ch  // ok=false表示通道已关闭

        但忽略通道关闭状态可能会导致接收方不知道何时停止读取,进而造成goroutine永远挂起。一般来说我们关闭通道应遵循如下要求:

  • 仅由负责发送数据的一方关闭通道。
  • 避免多次关闭同一个通道。
  • 在发送数据之前,确保通道尚未关闭。 

3.3 通道遍历 💥

         可以使用for range循环来遍历通道中的所有元素,直到通道被关闭。这种方式非常适合用于处理未知数量的数据流。

for value := range ch {
    fmt.Println(value)
}

3.4 Select语句 💥

        select 是 Go 中的一个控制结构,类似于 switch 语句。select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。

        语法格式如下:

select {
  case <- channel1:
    // 执行的代码
  case value := <- channel2:
    // 执行的代码
  case channel3 <- value:
    // 执行的代码

    // 你可以定义任意数量的 case

  default:
    // 所有通道都没有准备好,执行的代码
}
  • 每个 case 都必须是一个通道
  • 所有 channel 表达式都会被求值
  • 所有被发送的表达式都会被求值
  • 如果任意某个通道可以进行,它就执行,其他被忽略。
  • 如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。
    否则:
    1. 如果有 default 子句,则执行该语句。
    2. 如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。

🌺示例代码:

package main
import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

func main() {
    c := make(chan int)
    quit := make(chan int)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

        上述代码中fibonacci goroutine 在 channel c 上发送斐波那契数列,当接收到 quit channel 的信号时退出。最终输出结果如下:

0
1
1
2
3
5
8
13
21
34

4.同步并发工具🌟 

        在Go语言并发编程中,同步控制工具是协调多个Goroutine执行的关键。

4.1 WaitGroup(协程等待组)💐

        WaitGroup 是 Go 语言标准库中 sync 包提供的一种同步原语,本质是一个计数器,用于等待一组协程(goroutine)完成任务后再继续执行。它的主要作用是让主协程或某个协程能够等待其他协程完成后再继续执行。

主要方法:

  • Add(delta int):增加计数器的值。通常在启动协程前调用,表示需要等待的协程数量。

  • Done():减少计数器的值。通常在协程任务完成后调用,表示该协程已完成。

  • Wait():阻塞当前协程,直到计数器归零。

🔥示例代码:

package main

import (
        "fmt"
        "sync"
)

func worker(id int, wg *sync.WaitGroup) {
        defer wg.Done() // Goroutine 完成时调用 Done()
        fmt.Printf("Worker %d started\n", id)
        fmt.Printf("Worker %d finished\n", id)
}

func main() {
        var wg sync.WaitGroup

        for i := 1; i <= 3; i++ {
                wg.Add(1) // 增加计数器
                go worker(i, &wg)
        }

        wg.Wait() // 等待所有 Goroutine 完成
        fmt.Println("All workers done")
}

 上述代码,最终输出结果如下:

Worker 1 started
Worker 1 finished
Worker 2 started
Worker 2 finished
Worker 3 started
Worker 3 finished
All workers done

4.2 Mutex(互斥锁) 💐

        Mutex用于确保在并发环境下对共享资源的独占访问,其通过互斥锁机制防止多个goroutine同时访问共享资源,从而避免数据竞争(data race)问题。

主要方法:

  • Lock():获取锁。如果锁已被其他协程持有,则调用此方法的协程会阻塞,直到锁被释放。

  • Unlock():释放锁。释放锁后,其他等待的协程可以获取锁。

🔥实例代码(银行账户余额更新):

package main

import (
    "fmt"
    "sync"
    "time"
)

// 定义一个银行账户结构体
type Account struct {
    balance int
    mu      sync.Mutex // 互斥锁
}

// 存款操作
func (a *Account) Deposit(amount int) {
    a.mu.Lock()
    defer a.mu.Unlock()

    a.balance += amount
    fmt.Printf("存入 %d,当前余额: %d\n", amount, a.balance)
}

// 取款操作
func (a *Account) Withdraw(amount int) {
    a.mu.Lock()
    defer a.mu.Unlock()

    if a.balance >= amount {
        a.balance -= amount
        fmt.Printf("取出 %d,当前余额: %d\n", amount, a.balance)
    } else {
        fmt.Println("余额不足")
    }
}

func main() {
    account := &Account{balance: 1000}

    var wg sync.WaitGroup

    // 启动多个协程进行存款和取款操作
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(i int) {
            defer wg.Done()
            account.Deposit(i * 100)
            time.Sleep(time.Millisecond * 100)
            account.Withdraw(i * 50)
        }(i)
    }

    wg.Wait()
    fmt.Printf("最终余额: %d\n", account.balance)
}

 结构体定义:

  • Account 结构体包含一个 balance 字段,表示账户余额。
  • mu 是一个 sync.Mutex 类型的互斥锁,用于保护对 balance 的访问。

存款操作:

  • Deposit 方法使用 Lock() 方法获取锁,确保在修改 balance 时没有其他协程在访问。
  • 使用 defer a.mu.Unlock() 确保锁在方法结束时被释放,即使发生错误也不会导致死锁。

取款操作:

  • Withdraw 方法同样使用 Lock() 和 Unlock() 来保护对 balance 的访问。
  • 在取款前检查余额是否足够,避免透支。

并发执行:

  • 在 main 函数中,创建了一个 Account 实例,并启动了 10 个协程来模拟存款和取款操作。
  • 使用 sync.WaitGroup 等待所有协程完成,确保在打印最终余额之前所有操作都已完成。

4.3 RWMutex(读写锁) 💐

        RWMutex(读写锁)是 Go 语言标准库 sync 包中提供的一种同步原语,用于在并发环境中保护共享资源的访问。它允许多个读操作同时进行,但写操作必须独占访问。这种锁特别适合读多写少的场景,能够显著提高并发性能。  RWMutex 采用写优先(write-preferring)设计,即当有写操作等待时,新的读操作会被阻塞,直到写操作完成。

🔒读锁:

  • 允许多个协程同时读取共享资源。
  • 使用 RLock() 方法获取读锁。
  • 使用 RUnlock() 方法释放读锁。

🔒写锁:

  • 确保同一时间只有一个协程可以写入共享资源,且写操作会阻塞所有读操作。
  • 使用 Lock() 方法获取写锁。
  • 使用 Unlock() 方法释放写锁。

🔥示例代码:

var cache = make(map[string]string)
var rwMu sync.RWMutex

// 写操作使用写锁
func Set(key, value string) {
    rwMu.Lock()
    defer rwMu.Unlock()
    cache[key] = value
}

// 读操作使用读锁
func Get(key string) string {
    rwMu.RLock()
    defer rwMu.RUnlock()
    return cache[key]
}

 注意事项:

  • 避免死锁:不要在持有读锁的情况下尝试获取写锁,否则会导致死锁。
  • 性能优化:合理安排读写操作的顺序,避免写操作过于频繁,从而影响读操作的性能。
  • 锁的粒度:尽量减少锁的持有时间,以提高并发性能

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

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

相关文章

【Audio开发二】Android原生音量曲线调整说明

一&#xff0c;客制化需求 客户方对于音量加减键从静音到最大音量十五个档位区域的音量变化趋势有定制化需求。 二&#xff0c;音量曲线调试流程 Android根据不同的音频流类型定义不同的曲线&#xff0c;曲线文件存放在/vendor/etc/audio_policy_volumes.xml或者default_volu…

spring-security原理与应用系列:HttpSecurity.filters

目录 AnyRequestMatcher WebSecurityConfig HttpSecurity AbstractInterceptUrlConfigurer AbstractAuthenticationProcessingFilter 类图 在前面的文章《spring-security原理与应用系列&#xff1a;securityFilterChainBuilders》中&#xff0c;我们遗留了一个问题&…

JVM生产环境问题定位与解决实战(六):总结篇——问题定位思路与工具选择策略

本文已收录于《JVM生产环境问题定位与解决实战》专栏&#xff0c;完整系列见文末目录 引言 在前五篇文章中&#xff0c;我们深入探讨了JVM生产环境问题定位与解决的实战技巧&#xff0c;从基础的jps、jmap、jstat、jstack、jcmd等工具&#xff0c;到JConsole、VisualVM、MAT的…

并行治理机制对比:Polkadot、Ethereum 与 NEAR

治理是任何去中心化网络的基础。它塑造了社区如何发展、如何为创新提供资金、如何应对挑战以及如何随着时间的推移建立信任。随着 Web3 的不断发展&#xff0c;决定这些生态系统如何做出决策的治理模型也在不断发展。 在最近的一集的【The Decentralized Mic】中, Polkadot 汇…

TDengine tar.gz和docker两种方式安装和卸载

下载地址 3.1.1.0 Linux版本 安装包 下载地址 3.1.1.0 docker 镜像 下载地址 3.1.1.0 Window客户端 1. 将文件上传至服务器后解压 tar -zxvf TDengine-server-3.1.1.0-Linux-x64.tar.gz 2. tar.gz安装 解压文件后&#xff0c;进入相应子目录&#xff0c;执行其中的 install.…

【STM32设计】基于STM32的智能门禁管理系统(指纹+密码+刷卡+蜂鸣器报警)(代码+资料+论文)

本课题为基于单片机的智能门禁系统&#xff0c;整个系统由AS608指纹识别模块&#xff0c;矩阵键盘&#xff0c;STM32F103单片机&#xff0c;OLED液晶&#xff0c;RFID识别模块&#xff0c;继电器&#xff0c;蜂鸣器等构成&#xff0c;在使用时&#xff0c;用户可以录入新的指纹…

java知识梳理(二)

一.lambda表达式 作用&#xff1a;Lambda 表达式在 Java 8 引入&#xff0c;主要用于简化匿名内部类的写法&#xff0c;特别是在函数式编程场景中&#xff0c;比如 函数式接口、流式 API&#xff08;Streams&#xff09;、并发编程等。它让 Java 代码更简洁、可读性更强&#x…

鸿蒙Flutter实战:20. Flutter集成高德地图,同层渲染

本文以同层渲染为例&#xff0c;介绍如何集成高德地图 完整代码见 Flutter 鸿蒙版 Demo 概述 Dart 侧 核心代码如下&#xff0c;通过 OhosView 来承载原生视图 OhosView(viewType: com.shaohushuo.app/customView,onPlatformViewCreated: _onPlatformViewCreated,creation…

AI辅助下基于ArcGIS Pro的SWAT模型全流程高效建模实践与深度进阶应用

目前&#xff0c;流域水资源和水生态问题逐渐成为制约社会经济和环境可持续发展的重要因素。SWAT模型是一种基于物理机制的分布式流域水文与生态模拟模型&#xff0c;能够对流域的水循环过程、污染物迁移等过程进行精细模拟和量化分析。SWAT模型目前广泛应用于流域水文过程研究…

尚语翻译图册翻译|专业图册翻译|北京专业翻译公司推荐|专业文件翻译报价

内容概要 尚语翻译公司聚焦多语种产品图册翻译的竞价推广服务&#xff0c;通过行业垂直化运营构建差异化竞争力。其核心服务覆盖机械制造、医疗器械、电子元件三大领域&#xff0c;依托ISO 17100认证的翻译流程和Trados术语管理系统&#xff0c;实现技术文档的精准转化。为提升…

LeetCode 解题思路 30(Hot 100)

解题思路&#xff1a; 递归参数&#xff1a; 生成括号的对数 n、结果集 result、当前路径 path、左括号数 open、右括号数 close。递归过程&#xff1a; 当当前路径 path 的长度等于 n * 2 时&#xff0c;说明已经生成有效括号&#xff0c;加入结果集。若左括号数小于 n&…

Java EE(18)——网络原理——应用层HTTP协议

一.初识HTTP协议 HTTP(HyperText Transfer Protocol&#xff0c;超文本传输协议)是用于在客户端&#xff08;如浏览器&#xff09;和服务器之间传输超媒体文档&#xff08;如HTML&#xff09;的应用层协议。 HTTP协议发展至今发布了多个版本&#xff0c;其中1.0&#xff0c;1.…

强大而易用的JSON在线处理工具

强大而易用的JSON在线处理工具&#xff1a;程序员的得力助手 在当今的软件开发世界中&#xff0c;JSON&#xff08;JavaScript Object Notation&#xff09;已经成为了数据交换的通用语言。无论是前端还是后端开发&#xff0c;我们都经常需要处理、验证和转换JSON数据。今天&a…

Qt笔记----》不同环境程序打包

文章目录 概要1、windows环境下打包qt程序2、linux环境下打包qt程序2.1、程序目录2.2、创建一个空文件夹2.3、添加依赖脚本2.4、打包过程2.4.1、添加程序依赖库2.4.2、添加Qt相关依赖库 概要 qt不同运行环境下打包方式&#xff1a;windows/linux 1、windows环境下打包qt程序 …

企业服务器备份软件,企业服务器备份的方法有哪些?

企业服务器备份需综合考虑数据量、业务连续性要求&#xff08;RTO/RPO&#xff09;、合规性及成本等因素。以下是分场景的工具和方法指南&#xff1a; 一、备份软件推荐 1. 80KM备份软件 80KM备份软件可以进行很复杂的备份方式&#xff0c;也可以内网对内网备份、还能内网的…

html5炫酷图片悬停效果实现详解

html5炫酷图片悬停效果实现详解 这里写目录标题 html5炫酷图片悬停效果实现详解项目介绍技术栈核心功能实现1. 页面布局2. 图片容器样式3. 炫酷悬停效果缩放效果倾斜效果模糊效果旋转效果 4. 悬停文字效果5. 性能优化6. 响应式设计 项目亮点总结 项目介绍 本文将详细介绍如何使…

机器学习的一百个概念(5)数据增强

前言 本文隶属于专栏《机器学习的一百个概念》&#xff0c;该专栏为笔者原创&#xff0c;引用请注明来源&#xff0c;不足和错误之处请在评论区帮忙指出&#xff0c;谢谢&#xff01; 本专栏目录结构和参考文献请见[《机器学习的一百个概念》 ima 知识库 知识库广场搜索&…

在MCU工程中优化CPU工作效率的几种方法

在嵌入式系统开发中&#xff0c;优化 CPU 工作效率对于提升系统性能、降低功耗、提高实时性至关重要。Keil 作为主流的嵌入式开发工具&#xff0c;提供了多种优化策略&#xff0c;包括 关键字使用、内存管理、字节对齐、算法优化 等。本文将从多个方面介绍如何在 Keil 工程中优…

美团民宿 mtgsig 小程序 mtgsig1.2 分析

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 逆向分析 cp execjs.compile(open(民…

(done) MIT6.824 Lecture 02 - RPC and Threads

知乎专栏&#xff1a;https://zhuanlan.zhihu.com/p/641105196 原视频&#xff1a;https://www.bilibili.com/video/BV16f4y1z7kn?spm_id_from333.788.videopod.episodes&vd_source7a1a0bc74158c6993c7355c5490fc600&p2 看知乎专栏 一、Why we choose go&#xff1f…