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

go语言并发的最佳实践

Go 语言的并发模型是其最强大的特性之一,基于 CSP(Communicating Sequential Processes)理论,通过 goroutinechannel 实现轻量级并发.

一、并发核心概念

1. Goroutine

在 Go 语言中,Goroutine 是实现并发编程的核心特性之一,它本质上是一种轻量级线程。与传统的操作系统线程相比,Goroutine 有着显著的优势。

  • 轻量级线程,由 Go 运行时管理(非操作系统线程)。
  • 启动成本低(KB 级栈内存,动态扩缩容),可轻松创建数千个。

在 Go 语言中,通过 go 关键字可以非常方便地启动一个 Goroutine。以下是一个简单的示例:

   func main(){go func() {fmt.Println("Hello from goroutine!")}()// 这里需要注意,如果主函数直接结束,goroutine 可能来不及执行// 因为主函数结束会导致整个程序终止fmt.Println("Main function continues...")}

2. Channel

Channel 是 Go 语言中用于在不同 Goroutine 之间进行通信和同步的重要工具,它遵循 “通过通信共享内存” 的设计理念,避免了传统并发编程中使用共享内存带来的竞态问题。

  • 是一种类型安全的管道,它只能传输特定类型的数据。例如,一个 chan int 类型的 Channel 只能用于传输整数类型的数据
  • 避免共享内存的竞态问题,提倡“通过通信共享内存”。
  • 分两种类型:
    • 无缓冲通道(同步):发送和接收操作会阻塞,直到另一端准备好。
    • 有缓冲通道(异步):缓冲区未满或非空时不会阻塞。

二、原理:GMP 调度模型

1. GMP 调度模型

基本概念
在 Go 语言的并发体系中,GMP 调度模型是其核心机制,它主要由三个关键组件构成:

  • G(Goroutine):Goroutine 是 Go 语言中轻量级的执行单元,类似于传统操作系统中的线程,但它的开销要小得多。
  • M(Machine):Machine 代表操作系统线程,它是真正在操作系统内核层面上执行的线程。每个 M 都与一个底层的操作系统线程绑定,负责执行 Goroutine。M 是与操作系统交互的桥梁,它负责处理系统调用、线程上下文切换等底层操作。
  • P(Processor):Processor 是调度上下文,P 可以看作是 M 执行 Goroutine 的“许可证”,每个 M 要执行 Goroutine 必须先绑定一个 P。P 维护着一个本地的 Goroutine 队列,用于存储待执行的 Goroutine。同时,P 还负责调度和管理这些 Goroutine 的执行顺序。

工作流程

  • P 维护本地 Goroutine 队列:每个 P 都有一个自己的本地 Goroutine 队列,当创建一个新的 Goroutine 时,它会被放入某个 P 的本地队列中。这个队列是一个先进先出(FIFO)的队列,P 会按照队列中的顺序依次调度 Goroutine 执行。
  • M 绑定 P 后获取 G 执行:一个 M 要执行 Goroutine,必须先绑定一个空闲的 P。绑定成功后,M 会从 P 的本地队列中取出一个 Goroutine 并执行。当 M 执行完一个 Goroutine 后,会继续从 P 的队列中获取下一个 Goroutine,直到队列为空。
  • G 阻塞时 M 释放 P:当一个 Goroutine 在执行过程中发生阻塞(例如进行 IO 操作)时,M 会释放当前绑定的 P,以便其他 M 可以使用这个 P 继续执行其他 Goroutine。被释放的 P 会被放入全局 P 列表中,等待其他 M 来绑定。而阻塞的 Goroutine 会被挂起,当阻塞操作完成后,它会被重新放入某个 P 的本地队列或全局队列中,等待再次被调度执行。

2. 抢占式调度

Go 1.14+ 支持基于信号的抢占,避免长时间占用 CPU 的 Goroutine 导致调度延迟。

三、Goroutine 基础

1. 简单示例

下面是一个简单的 Goroutine 示例,展示了如何启动一个 Goroutine 并执行一个简单的任务:

package mainimport ("fmt""time"
)func main() {go sayHello() // 启动 goroutine// 这里使用 time.Sleep 是为了让主函数等待一段时间,以便 goroutine 有机会执行// 但这种方式并不是一个好的做法,实际开发中应该使用 sync.WaitGroup 进行同步time.Sleep(100 * time.Millisecond)
}func sayHello() {fmt.Println("Hello!")
}

2. 使用 sync.WaitGroup 并发组

在实际开发中,为了确保所有的 Goroutine 都执行完毕后再继续执行后续的代码,我们可以使用 sync.WaitGroup 来进行同步。sync.WaitGroup 提供了三个主要的方法:AddDoneWait

  • Add 方法用于设置需要等待的 Goroutine 的数量。
  • Done 方法用于标记一个 Goroutine 已经执行完毕,相当于将等待的数量减 1。
  • Wait 方法用于阻塞当前的 Goroutine,直到所有标记的 Goroutine 都执行完毕。
package mainimport ("fmt""sync"
)func main() {var wg sync.WaitGroupwg.Add(2) // 表示有两个任务需要等待go func() {defer wg.Done() // 在函数结束时调用 Done 方法,表示该任务完成fmt.Println("Goroutine 1")}()go func() {defer wg.Done()fmt.Println("Goroutine 2")}()wg.Wait() // 阻塞直到所有任务完成fmt.Println("All done!")
}

四、Channel 操作

1.无缓冲通道

创建和使用无缓冲通道的基本步骤如下:

package mainimport "fmt"func main() {ch := make(chan int) // 创建一个无缓冲的整数类型通道go func() {num := 42ch <- num // 向通道发送数据fmt.Println("Data sent to channel")}()value := <-ch // 从通道接收数据fmt.Println("Received value:", value)
}

2. 有缓冲通道

有缓冲通道的使用可以提高程序的并发性能,避免不必要的阻塞。以下是一个有缓冲通道的示例:

package mainimport "fmt"func main() {ch := make(chan string, 2) // 创建一个缓冲区容量为 2 的字符串类型通道ch <- "A"ch <- "B"fmt.Println("Data sent to channel")fmt.Println(<-ch) // 从通道接收第一个数据fmt.Println(<-ch) // 从通道接收第二个数据
}

3. 关闭通道

在使用通道时,有时需要通知接收方不再有数据发送,这时可以使用 close 函数关闭通道。关闭通道后,接收方仍然可以从通道中接收数据,直到通道中的数据被全部接收完,之后再接收数据会得到该类型的零值。以下是一个关闭通道的示例:

package mainimport "fmt"func main() {ch := make(chan int)go func() {for i := 0; i < 3; i++ {ch <- i}close(ch) // 关闭通道fmt.Println("Channel closed")}()for num := range ch { // 使用 for...range 循环从通道接收数据,直到通道关闭fmt.Println(num)}fmt.Println("All data received")
}

4. Select 多路复

select 语句用于在多个通道操作中进行选择,它类似于 switch 语句,但 select 语句专门用于处理通道操作。select 语句会随机选择一个可以执行的通道操作并执行,如果所有通道操作都无法执行,则会阻塞,直到有一个通道操作可以执行。如果指定了 default 分支,则在所有通道操作都无法执行时会执行 default 分支,不会阻塞。以下是一个使用 select 语句进行多路复用的示例:

package mainimport ("fmt""time"
)func main() {ch1 := make(chan string)ch2 := make(chan string)go func() {time.Sleep(200 * time.Millisecond)ch1 <- "one"}()go func() {time.Sleep(100 * time.Millisecond)ch2 <- "two"}()select {case msg1 := <-ch1:fmt.Println(msg1)case msg2 := <-ch2:fmt.Println(msg2)case <-time.After(1 * time.Second): // 超时控制fmt.Println("timeout")}
}

五、同步原语

1. 互斥锁 sync.Mutex

在并发编程中,多个 Goroutine 同时访问和修改共享资源可能会导致数据不一致的问题,这时可以使用互斥锁 sync.Mutex 来保证同一时间只有一个 Goroutine 可以访问共享资源。互斥锁提供了两个主要的方法:LockUnlock

  • Lock 方法用于获取锁,如果锁已经被其他 Goroutine 持有,则当前 Goroutine 会阻塞,直到锁被释放。
  • Unlock 方法用于释放锁,允许其他 Goroutine 获取锁。

以下是一个使用互斥锁的示例:

package mainimport ("fmt""sync"
)var counter int
var mu sync.Mutexfunc increment() {mu.Lock()         // 获取锁defer mu.Unlock() // 确保在函数结束时释放锁counter++
}func main() {var wg sync.WaitGroupfor i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()increment()}()}wg.Wait() // 等待所有 Goroutine 执行完毕fmt.Println(counter) // 输出 1000
}

2. 读写锁 sync.RWMutex

读写锁 sync.RWMutex 是一种特殊的锁,它允许多个 Goroutine 同时进行读操作,但在进行写操作时会独占锁,不允许其他 Goroutine 进行读或写操作。读写锁适用于读多写少的场景,可以提高程序的并发性能。读写锁提供了四个主要的方法:RLockRUnlockLockUnlock

  • RLock 方法用于获取读锁,允许多个 Goroutine 同时持有读锁。
  • RUnlock 方法用于释放读锁。
  • Lock 方法用于获取写锁,在持有写锁期间,不允许其他 Goroutine 进行读或写操作。
  • Unlock 方法用于释放写锁。

以下是一个使用读写锁的示例:

package mainimport ("fmt""sync"
)var cache = make(map[string]string)
var rwMu sync.RWMutexfunc read(key string) string {rwMu.RLock()         // 获取读锁defer rwMu.RUnlock() // 确保在函数结束时释放读锁return cache[key]
}func write(key, value string) {rwMu.Lock()          // 获取写锁defer rwMu.Unlock()  // 确保在函数结束时释放写锁cache[key] = value
}func main() {var wg sync.WaitGroup// 启动多个读操作for i := 0; i < 5; i++ {wg.Add(1)go func(index int) {defer wg.Done()key := fmt.Sprintf("key%d", index)value := read(key)fmt.Printf("Read key %s: %s\n", key, value)}(i)}// 启动一个写操作wg.Add(1)go func() {defer wg.Done()key := "key1"value := "value1"write(key, value)fmt.Printf("Written key %s: %s\n", key, value)}()wg.Wait() // 等待所有 Goroutine 执行完毕
}

六、并发模式

1. Worker Pool(工作池)

工作池模式是一种常见的并发模式,它可以帮助我们管理一组工作线程(在 Go 中就是 Goroutine),并将任务分配给这些工作线程进行处理。这种模式在处理大量独立任务时非常有用,可以避免创建过多的 Goroutine 导致系统资源耗尽。
收起


package mainimport ("fmt"
)// worker 函数表示一个工作线程,它从 jobs 通道接收任务,处理后将结果发送到 results 通道
func worker(id int, jobs <-chan int, results chan<- int) {for job := range jobs {fmt.Printf("Worker %d processing job %d\n", id, job)// 模拟任务处理,这里简单地将任务值乘以 2results <- job * 2}
}func main() {// 创建两个有缓冲的通道,分别用于存储任务和结果jobs := make(chan int, 100)results := make(chan int, 100)// 启动 3 个工作线程for w := 1; w <= 3; w++ {go worker(w, jobs, results)}// 发送 9 个任务到 jobs 通道for j := 1; j <= 9; j++ {jobs <- j}// 关闭 jobs 通道,表示不再有新的任务发送close(jobs)// 收集结果for a := 1; a <= 9; a++ {<-results}// 关闭 results 通道close(results)
}

2. Fan-out/Fan-in(扇出/扇入)

扇出 / 扇入模式是一种用于并发处理数据的模式。扇出指的是将一个输入源的数据分发到多个 Goroutine 中进行处理,扇入则是将多个 Goroutine 的处理结果合并到一个通道中。

package mainimport ("fmt""sync"
)// merge 函数用于将多个输入通道的数据合并到一个输出通道中
func merge(chs ...<-chan int) <-chan int {var wg sync.WaitGroupout := make(chan int)// 从每个输入通道读取数据for _, ch := range chs {wg.Add(1)go func(c <-chan int) {defer wg.Done()for n := range c {out <- n}}(ch)}// 关闭输出通道go func() {wg.Wait()close(out)}()return out
}func main() {// 创建 3 个输入通道ch1 := make(chan int)ch2 := make(chan int)ch3 := make(chan int)// 启动 3 个 Goroutine 向输入通道发送数据go func() {ch1 <- 1ch1 <- 2close(ch1)}()go func() {ch2 <- 3ch2 <- 4close(ch2)}()go func() {ch3 <- 5ch3 <- 6close(ch3)}()// 合并 3 个输入通道的数据到一个输出通道resultCh := merge(ch1, ch2, ch3)// 从输出通道读取数据for num := range resultCh {fmt.Println(num)}
}

3. 管道模式(Pipeline)

管道模式是 Go 语言并发编程中一种非常强大且实用的模式,它通过串联多个 Goroutine 来处理数据流,形成一个处理链。这种模式特别适合那些需要分阶段处理任务的场景,例如数据清洗、数据转换、数据筛选等。在管道模式中,每个阶段的 Goroutine 负责完成一个特定的任务,将处理后的数据传递给下一个阶段,就像工厂里的流水线一样,每个工人负责一道工序,最终完成整个产品的生产。

package mainimport "fmt"// generate 函数用于生成一个整数数据流
// 它接收多个整数作为输入,将这些整数依次发送到一个通道中
// 发送完成后关闭通道
func generate(nums ...int) <-chan int {out := make(chan int)go func() {for _, n := range nums {out <- n}close(out)}()return out
}// square 函数用于对输入通道中的每个整数进行平方操作
// 它从输入通道中接收整数,计算其平方值,并将结果发送到一个新的通道中
// 当输入通道关闭且所有数据都处理完后,关闭输出通道
func square(in <-chan int) <-chan int {out := make(chan int)go func() {for n := range in {out <- n * n}close(out)}()return out
}// filterEven 函数用于过滤掉输入通道中的奇数,只保留偶数
// 它从输入通道中接收整数,判断是否为偶数,如果是则发送到输出通道
// 当输入通道关闭且所有数据都处理完后,关闭输出通道
func filterEven(in <-chan int) <-chan int {out := make(chan int)go func() {for n := range in {if n%2 == 0 {out <- n}}close(out)}()return out
}func main() {// 定义一个整数切片作为初始数据nums := []int{1, 2, 3, 4}// 管道串联:生成 → 平方 → 过滤 → 输出// 首先调用 generate 函数生成整数数据流// 然后将生成的数据流传递给 square 函数进行平方操作// 接着将平方后的数据流传递给 filterEven 函数进行偶数过滤// 最后使用 for...range 循环从最终的通道中接收数据并输出for n := range filterEven(square(generate(nums...))) {fmt.Println(n) // 输出结果:4, 16}
}

管道模式的优点 :

  • 可维护性高:每个阶段的任务都封装在独立的函数中,代码结构清晰,易于理解和维护。
  • 可扩展性强:可以方便地添加、删除或修改处理阶段,以适应不同的业务需求。
  • 并发性能好:每个阶段的处理可以并行进行,充分利用多核 CPU 的资源,提高程序的处理效率。

通过管道模式,我们可以将复杂的任务分解为多个简单的子任务,并通过通道将这些子任务连接起来,实现高效、灵活的数据流处理。

4. 使用 Context 控制生命周期

在 Go 中,context 包提供了一种机制来控制 Goroutine 的生命周期,例如取消任务、设置超时等。

package mainimport ("context""fmt""time"
)// longRunningTask 函数表示一个长时间运行的任务,它会监听 context 的取消信号
func longRunningTask(ctx context.Context) {for {select {case <-ctx.Done(): // 监听取消信号fmt.Println("Task canceled:", ctx.Err())returndefault:// 模拟工作time.Sleep(1 * time.Second)}}
}func main() {// 创建一个带有超时的 context,超时时间为 3 秒ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)defer cancel()// 启动一个 Goroutine 执行长时间运行的任务go longRunningTask(ctx)// 等待 4 秒,确保任务会被取消time.Sleep(4 * time.Second)
}

七、常见问题与调试

1. 竞态条件(Race Condition)

竞态条件是指多个 Goroutine 同时访问和修改共享资源,导致程序的行为变得不可预测。可以使用 Go 语言提供的 -race 标志来检测竞态条件。

package mainimport ("fmt""sync"
)var counter int
var wg sync.WaitGroupfunc increment() {counter++  //不用互斥锁
}func main() {for i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()increment()}()}wg.Wait()fmt.Println(counter)
}

要检测上述代码中的竞态条件,可以使用以下命令:

go run -race main.go

如果存在竞态条件,Go 编译器会输出详细的错误信息,帮助我们定位问题。

2. 死锁

死锁是指程序中的 Goroutine 相互等待对方释放资源,导致所有 Goroutine 都无法继续执行的情况。要避免死锁,需要确保通道操作成对出现,避免永久阻塞。

func main() {ch := make(chan int)ch <- 1 // 这里会导致死锁,因为没有接收方<-ch
}
//fatal error: all goroutines are asleep - deadlock!

在上面代码中,由于没有接收方,ch <- 1 会导致永久阻塞,从而引发死锁。要避免这种情况,需要确保在发送数据之前有接收方准备好接收数据。

3.在 Goroutine 中传递错误

func doSomething() error {return errors.New("something went wrong")
}func main() {errCh := make(chan error)go func() {errCh <- doSomething()}()if err := <-errCh; err != nil {fmt.Println("Error:", err)}
}

4. Goroutine 泄漏

Goroutine 泄漏是指 Goroutine 由于某种原因无法正常退出,导致系统资源不断被占用。要避免 Goroutine 泄漏,始终确保通道被关闭,或使用 context 控制退出。

检测方法

使用 runtime.NumGoroutine() 监控 goroutine 数量

runtime.NumGoroutine() 函数可以返回当前程序中正在运行的 Goroutine 的数量。通过定期检查这个数量,我们可以判断是否存在 Goroutine 泄漏。如果 Goroutine 的数量持续增加,而没有相应的减少,那么很可能存在泄漏。

以下是一个示例代码,展示了如何使用 runtime.NumGoroutine() 监控 Goroutine 数量:

package mainimport ("fmt""runtime""time"
)func worker(ch chan int) {for {select {case num := <-ch:fmt.Println("Received:", num)default:time.Sleep(100 * time.Millisecond)}}
}func main() {ch := make(chan int)go worker(ch)// 定期检查 Goroutine 数量ticker := time.NewTicker(1 * time.Second)defer ticker.Stop()for range ticker.C {numGoroutines := runtime.NumGoroutine()fmt.Printf("Number of goroutines: %d\n", numGoroutines)}
}

使用Go 的 pprof 分析 goroutine 堆栈。

pprof 是 Go 语言自带的一个性能分析工具,它可以帮助我们分析程序的 CPU 使用情况、内存分配情况以及 Goroutine 堆栈信息。通过 pprof,我们可以查看每个 Goroutine 的状态和调用栈,从而找出可能存在泄漏的 Goroutine。

以下是一个使用 pprof 分析 Goroutine 堆栈的示例代码:

package mainimport ("net/http"_ "net/http/pprof""time"
)func worker() {for {time.Sleep(1 * time.Second)}
}func main() {go worker()// 启动 pprof 服务go func() {http.ListenAndServe("localhost:6060", nil)}()// 让程序一直运行select {}
}

代码执行后,使用以下命令:

go tool pprof http://localhost:6060/debug/pprof/goroutine

上述命令后,会进入 pprof 的交互式界面。在这个界面中,可以使用各种命令来查看 Goroutine 的堆栈信息,例如 top 命令可以查看占用资源最多的 Goroutine,list 命令可以查看指定函数的调用栈等。通过分析这些信息,我们可以找出可能存在泄漏的 Goroutine,并对代码进行修复。

八、最佳实践

1.优先使用 channel 而非共享内存

在 Go 语言中,推荐使用 channel 来进行 Goroutine 之间的通信和同步,而不是直接使用共享内存。channel 可以避免竞态条件和死锁等问题,使代码更加简洁和安全。

2.避免在 goroutine 中使用全局变量

全局变量在多个 Goroutine 中共享时容易引发竞态条件。尽量将变量的作用域限制在需要使用的 Goroutine 内部,或者使用 channel 来传递数据。

3.使用 select 实现超时和取消

select 语句可以用于同时监听多个通道的操作,结合 time.Aftercontext 可以实现超时和取消功能,提高程序的健壮性。

4.小任务用 goroutine,大任务考虑线程池

对于小的、独立的任务,可以直接使用 Goroutine 来执行,因为 Goroutine 的创建和销毁成本较低。对于大的、复杂的任务,可以考虑使用工作池模式来管理一组 Goroutine,避免创建过多的 Goroutine 导致系统资源耗尽。
通过掌握这些概念和模式,你可以高效地编写并发 Go 程序,充分利用多核 CPU 资源。同时,遵循最佳实践和注意常见问题的处理,可以使你的代码更加健壮和可靠。

相关文章:

go语言并发的最佳实践

Go 语言的并发模型是其最强大的特性之一&#xff0c;基于 CSP&#xff08;Communicating Sequential Processes&#xff09;理论&#xff0c;通过 goroutine 和 channel 实现轻量级并发. 一、并发核心概念 1. Goroutine 在 Go 语言中&#xff0c;Goroutine 是实现并发编程的…...

俄罗斯方块游戏完整代码示例

以下是一个基于Cocos Creator引擎开发的俄罗斯方块游戏的完整代码示例。该游戏实现了俄罗斯方块的基本功能&#xff0c;并且代码整合在单个文件中&#xff0c;无需任何外部依赖&#xff0c;可以直接在浏览器中运行。 1. 创建Cocos Creator项目 首先&#xff0c;确保你已经安装了…...

Ubuntu22.04配置cuda/cudnn/pytorch

Ubuntu22.04配置cuda/cudnn/pytorch 安装cuda官网下载.run文件并且安装/etc/profile中配置cuda环境变量 cudnn安装官网找cuda版本对应的cudnn版本下载复制相应文件到系统文件中 安装pytorch官网找cuda对应版本的pytorchpython代码测试pytorch-GPU版本安装情况 安装cuda 官网下…...

【九】Golang 数组

&#x1f4a2;欢迎来到张胤尘的技术站 &#x1f4a5;技术如江河&#xff0c;汇聚众志成。代码似星辰&#xff0c;照亮行征程。开源精神长&#xff0c;传承永不忘。携手共前行&#xff0c;未来更辉煌&#x1f4a5; 文章目录 数组数组初始化默认初始化显式初始化省略长度初始化索…...

百达翡丽(Patek Philippe):瑞士制表的巅峰之作(中英双语)

百达翡丽&#xff08;Patek Philippe&#xff09;&#xff1a;瑞士制表的巅峰之作 在钟表界&#xff0c;百达翡丽&#xff08;Patek Philippe&#xff09; 一直被誉为“世界三大名表”之一&#xff0c;并且常被认为是其中的至高存在。一句“没人能真正拥有一枚百达翡丽&#x…...

【学习】软件测试中的分类树法介绍

分类树法是一种软件测试设计技术&#xff0c;它通过构建一个树状结构来组织和展示输入数据的多种组合。这种方法有助于系统地识别和分析可能的测试情况&#xff0c;从而确保对软件进行全面而详尽的测试。分类树法特别适用于具有多个选择或条件的复杂系统&#xff0c;它可以有效…...

打造智能语料库:通过Coco AI Server 实现 Notion 笔记 RAG 检索功能

本文将详细介绍如何将 Notion 作为语料库&#xff0c;部署 Coco Server 的 RAG&#xff08;Retrieval-Augmented Generation&#xff09;功能。我们将使用 Easysearch 作为语料库存储 Notion 素材&#xff0c;并通过 ollama 进行 LLM 推理。 1. 环境准备 1.1 启动 Easysearch…...

SP字体UI放大代码

代码&#xff1a; echo off set QT_SCALE_FACTOR放大倍数 start "" "你的SP.exe启动路径"...

spring boot知识点2

1.spring boot 要开启一些特性&#xff0c;可通过什么方式开启 a.通过Enable注解&#xff0c;可启动定时服务 b.通过application.properties可设置端口号等地址信息 2.什么是热部署&#xff0c;以及spring boot通过什么方式进行热部署 热部署这个概念&#xff0c;我知道。就…...

动手学Agent——Day2

文章目录 一、用 Llama-index 创建 Agent1. 测试模型2. 自定义一个接口类3. 使用 ReActAgent & FunctionTool 构建 Agent 二、数据库对话 Agent1. SQLite 数据库1.1 创建数据库 & 连接1.2 创建、插入、查询、更新、删除数据1.3 关闭连接建立数据库 2. ollama3. 配置对话…...

qt实习总结

创建一个滑动条 QSlider *slider new QSlider(Qt::Vertical); //创建一个垂直方向的 进度条 带有上下箭头的输入框 QSpinBox 提供了一个带有上下箭头的输入框 垂直 水平怎么说 horizontal vetical 布局知识 BtnLayout->addWidget(AmendBtn); BtnLayout->addWidg…...

SpringBoot3.x整合WebSocket

SpringBoot3.x整合WebSocket 本文主要介绍最新springboot3.x下如何整合WebSocket. WebSocket简述 WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议&#xff0c;它允许在浏览器和服务器之间进行实时的、双向的通信。相对于传统的基于请求和响应的 HTTP 协议&#xff…...

vLLM专题(二):安装-CPU

vLLM 是一个 Python 库,支持以下 CPU 变体。选择您的 CPU 类型以查看供应商特定的说明: Intel/AMD x86 vLLM 最初支持在 x86 CPU 平台上进行基本模型推理和服务,支持的数据类型包括 FP32、FP16 和 BF16。 注意 此设备没有预构建的 wheel 包或镜像,因此您必须从源代码构建 v…...

「软件设计模式」适配器模式(Adapter)

软件设计模式深度解析&#xff1a;适配器模式&#xff08;Adapter&#xff09;&#xff08;C实现&#xff09; 一、模式概述 适配器模式&#xff08;Adapter Pattern&#xff09;是结构型设计模式中的"接口转换器"&#xff0c;它像现实世界中的电源适配器一样&#…...

Dify平台搭建面试机器人

无代码搭建面试机器人 什么是Dify 什么是Dify Dify 是一款开源的大语言模型(LLM) 应用开发平台。它融合了后端即服务&#xff08;Backend as Service&#xff09;和 LLMOps 的理念&#xff0c;使开发者可以快速搭建生产级的生成式 AI 应用。即使你是非技术人员&#xff0c;也能…...

Vue 3 中可读可写的计算属性(Computed Properties)的使用场景

在 Vue 3 中&#xff0c;计算属性&#xff08;Computed Properties&#xff09;是一种基于响应式依赖进行缓存的属性。它们通常用于处理复杂的逻辑&#xff0c;并且只有当依赖的响应式数据发生变化时&#xff0c;才会重新计算。计算属性非常适合用于处理模板中的复杂表达式&…...

如何通过AI轻松制作PPT?让PPT一键生成变得简单又高效

如何通过AI轻松制作PPT&#xff1f;让PPT一键生成变得简单又高效&#xff01;在这个信息化飞速发展的时代&#xff0c;PPT已经成为我们日常工作、学习和生活中不可或缺的一部分。无论是公司会议、学术报告&#xff0c;还是个人展示&#xff0c;PPT的作用都不容忽视。很多人对于…...

从零开始部署DeepSeek:基于Ollama+Flask的本地化AI对话系统

从零开始部署DeepSeek&#xff1a;基于OllamaFlask的本地化AI对话系统 一、部署背景与工具选型 在AI大模型遍地开花的2025年&#xff0c;DeepSeek R1凭借其出色的推理能力和开源特性成为开发者首选。本文将以零基础视角&#xff0c;通过以下工具链实现本地化部署&#xff1a; …...

STM32 CubeMx配置串口收发使用DMA并调用Idle模式(二)

本篇主要结合代码落实&#xff0c;之前串口已经配置好的DMA方式。 一、首先我们把串口看成一个对象&#xff0c;它有属性、私有数据和方法&#xff1b; 每个串口都有名字属性&#xff1b;有初始化、发送、接收方法&#xff1b;还有一个私有数据&#xff08;这个私有数据是每个…...

使用Redis实现分布式锁,基于原本单体系统进行业务改造

一、单体系统下&#xff0c;使用锁机制实现秒杀功能&#xff0c;并限制一人一单功能 1.流程图&#xff1a; 2.代码实现&#xff1a; Service public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderSe…...

数据结构中的邻接表

一、概念 邻接表&#xff08;Adjacency List&#xff09;是一种用于表示图&#xff08;Graph&#xff09;数据结构的常用方法。它特别适用于稀疏图&#xff0c;即边的数量远小于顶点数量平方的图。邻接表通过为每个顶点维护一个列表来存储与该顶点相邻的顶点&#xff0c;从而高…...

js第九题

题九&#xff1a;放大镜效果 要求&#xff1a; 1.鼠标移至图片上方&#xff0c;鼠标周围出现黄色的的正方形框&#xff0c;黄色矩形 框会随着鼠标的移动而移动&#xff1b; 2.将黄色正方形框里的内容的长和宽均放大2.4倍&#xff0c;并在图片右边进 行显示。 html <div …...

基于单片机ht7038 demo

单片机与ht7038 demo&#xff0c;三相电能表&#xff0c;电量数据包括电流电压功能&#xff0c;采用免校准方法 列表 ht7038模块/CORE/core_cm3.c , 17273 ht7038模块/CORE/core_cm3.h , 85714 ht7038模块/CORE/startup_stm32f10x_hd.s , 15503 ht7038模块/CORE/startup_stm32…...

轮播图html

题十二&#xff1a;轮播图 要求&#xff1a; 1.鼠标不在图片上方时&#xff0c;进行自动轮播&#xff0c;并且左右箭头不会显示&#xff1b;当鼠标放在图片上方时&#xff0c;停止轮播&#xff0c;并且左右箭头会显示&#xff1b; 2.图片切换之后&#xff0c;图片中下方的小圆…...

Nginx内存池源代码剖析----ngx_create_pool函数

ngx_create_pool 是 Nginx 内存池 的初始化函数&#xff0c;负责创建并初始化一个内存池对象。它的作用是 为后续的内存分配操作提供统一的管理入口&#xff0c;通过预分配一块较大的内存区域&#xff0c;并基于此区域实现高效的内存分配、对齐管理和资源回收。 源代码定义&…...

DeepSeek 开放平台无法充值 改用其他平台API调用DeepSeek-chat模型方法

近几天DeepSeek开放平台无法充值目前已经关闭状态&#xff0c;大家都是忙着接入DeepSeek模型 &#xff0c;很多人想使用DeepSeek怎么办&#xff1f; 当然还有改用其他平台API调用方法&#xff0c;本文以本站的提供chatgpt系统为例&#xff0c;如何修改DeepSeek-chat模型API接口…...

QT基础一、学会建一个项目

注&#xff1a;因为CSDN有很多付费才能吃到的史&#xff0c;本人对此深恶痛绝&#xff0c;所以我打算出一期免费的QT基础入门专栏&#xff0c;这是QT基础知识的第一期&#xff0c;学会建一个项目&#xff0c;本专栏是适用于c / c基础不错的朋友的一个免费专栏&#xff0c;接下来…...

科技引领未来,中建海龙C-MiC 2.0技术树立模块化建筑新标杆

在建筑行业追求高效与品质的征程中&#xff0c;中建海龙科技有限公司&#xff08;简称“中建海龙”&#xff09;以其卓越的创新能力和强大的技术实力&#xff0c;不断书写着装配式建筑领域的新篇章。1 月 10 日&#xff0c;由深圳安居集团规划&#xff0c;中建海龙与中海建筑共…...

解锁养生秘籍,拥抱健康生活

在这个快节奏的时代&#xff0c;人们行色匆匆&#xff0c;常常在忙碌中忽略了健康。其实&#xff0c;养生并非遥不可及&#xff0c;它就藏在生活的细微之处&#xff0c;等待我们去发现和实践。 规律作息是健康的基础。日出而作&#xff0c;日落而息&#xff0c;顺应自然规律&am…...

STM32 如何使用DMA和获取ADC

目录 背景 ‌摇杆的原理 程序 端口配置 ADC 配置 DMA配置 背景 DMA是一种计算机技术&#xff0c;允许某些硬件子系统直接访问系统内存&#xff0c;而不需要中央处理器&#xff08;CPU&#xff09;的介入&#xff0c;从而减轻CPU的负担。我们可以通过DMA来从外设&#xf…...