Go 中的 Context实现原理以及正确使用方式

        在 Go 语言中,Context 包是一种非常常用的工具,它被用来管理 goroutine 之间的通信和取消。本文将深入探讨Context 包的基本原理,包括使用场景、原理和一些最佳实践。

1. 基本原理

1.1 Context 包的介绍

        在 Go 语言中,Context 包是用于传递请求范围数据、取消信号和截止时间的机制。它通常被用来处理 goroutine 之间的通信和取消。Context 包是 Go 语言内置的,它可以很方便地使用,而不需要额外的依赖。
Context 包是一个轻量级的工具,它提供了一个标准的接口,用于在 goroutine 之间传递请求范围的数据、取消信号和截止时间。Context 包实现了一种类似于树状结构的数据结构,其中每个节点都表示一个请求范围。每个节点都有一个唯一的 key-value 对,其中 key 是一个 interface{} 类型的值,而 value 则是任何类型的值。Context 包还提供了一个可选的超时机制,用于在一定时间后自动取消请求。
Context 包的核心是一个 Context 接口,它定义了一些方法,用于获取请求范围数据、取消请求和处理超时。

 type Context interface {
     Deadline() (deadline time.Time, ok bool)
     Done() <-chan struct{}
     Err() error
     Value(key interface{}) interface{}
 }
  • Deadline() 方法返回截止时间和一个布尔值,指示截止时间是否已经设置。
  • Done() 方法返回一个只读的 channel,当请求被取消或超时时,该 channel 将被关闭。
  • Err() 方法返回一个错误,指示为什么请求被取消。
  • Value() 方法返回与给定key相关联的值,如果没有值,则返回 nil。

        Context 包还提供了两个用于创建 Context 的函数:WithContext 和 Background。Background 函数返回一个空的 Context,而 WithContext 函数则根据给定的父 Context 创建一个新的 Context。
Context 包的基本原理是通过在 goroutine 之间传递 Context 来实现请求范围数据、取消信号和截止时间的管理。当一个 goroutine 创建了一个新的 goroutine 时,它将 Context 作为参数传递给新的 goroutine。新的goroutine 可以使用这个 Context 来访问请求范围数据、接收取消信号和处理超时。

1.2 Context 的创建

        在 Golang 中,Context 可以通过 WithCancel、WithDeadline、WithTimeout 和 WithValue 等函数来创建。下面分别介绍这些函数的用法和注意事项。

1.2.1 WithCancel

        WithCancel 函数可以用于创建一个 Context 对象,并返回一个可取消的上下文和一个取消函数。当调用取消函数时,会通知所有的 Context 对象和其子 Context 对象,使它们都取消执行。

 func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

下面是一个示例代码:

package main
 ​
 import (
     "context"
     "fmt"
     "time"
 )
 ​
 func main() {
     parent := context.Background()
     ctx, cancel := context.WithCancel(parent)
     go func() {
         select {
         case <-ctx.Done():
             fmt.Println(ctx.Err())
             return
         case <-time.After(5 * time.Second):
             fmt.Println("work done")
         }
     }()
     time.Sleep(10 * time.Second)
     cancel()
     time.Sleep(1 * time.Second)
 }

        在上面的代码中,我们首先使用 context.Background() 函数创建一个根 Context 对象 parent,然后使用 WithCancel 函数创建一个子 Context 对象 ctx,并返回一个可取消的上下文和一个取消函数 cancel。接下来,我们在一个 goroutine 中使用 select 语句监听 Context 对象的 Done 方法和 time.After 函数的返回值,如果 Done 方法返回一个非 nil 的 error,则说明 Context 已经被取消,否则说明 time.After 函数已经超时。在主函数中,我们调用 cancel 函数来通知 Context 对象和其子 Context 对象,使它们都取消执行。最后,我们使用 time.Sleep 函数让程序等待一段时间,以便观察 Context 的执行情况。

1.2.2 WithDeadline

        WithDeadline 函数可以用于创建一个 Context 对象,并返回一个截止时间和一个取消函数。当超过截止时间时,会自动通知所有的 Context 对象和其子 Context 对象,使它们都取消执行。

func WithDeadline(parent Context, deadline time.Time) (ctx Context, cancel CancelFunc)

下面是一个示例代码:

package main
 ​
 import (
     "context"
     "fmt"
     "time"
 )
 ​
 func main() {
     parent := context.Background()
     ctx, cancel := context.WithDeadline(parent, time.Now().Add(5*time.Second))
     go func() {
         select {
         case <-ctx.Done():
             fmt.Println(ctx.Err())
             return
         case <-time.After(10 * time.Second):
             fmt.Println("work done")
         }
     }()
     time.Sleep(20 * time.Second)
     cancel()
     time.Sleep(1 * time.Second)
 }

        在上面的代码中,我们首先使用 context.Background() 函数创建一个根 Context 对象 parent,然后使用 WithDeadline 函数创建一个子 Context 对象 ctx,并返回一个截止时间和一个取消函数 cancel。接下来,我们在一个 goroutine 中使用 select 语句监听 Context 对象的 Done 方法和 time.After 函数的返回值,如果 Done 方法返回一个非 nil 的 error,则说明 Context 已经被取消,否则说明 time.After 函数已经超时。在主函数中,我们调用 cancel 函数来通知 Context 对象和其子 Context 对象,使它们都取消执行。最后,我们使用 time.Sleep 函数让程序等待一段时间,以便观察 Context 的执行情况。

1.2.3 WithTimeout

        WithTimeout 函数可以用于创建一个 Context 对象,并返回一个超时时间和一个取消函数。当超过超时时间时,会自动通知所有的 Context 对象和其子 Context 对象,使它们都取消执行。

func WithTimeout(parent Context, timeout time.Duration) (ctx Context, cancel CancelFunc)

下面是一个示例代码:

package main
 ​
 import (
     "context"
     "fmt"
     "time"
 )
 ​
 func main() {
     parent := context.Background()
     ctx, cancel := context.WithTimeout(parent, 5*time.Second)
     go func() {
         select {
         case <-ctx.Done():
             fmt.Println(ctx.Err())
             return
         case <-time.After(10 * time.Second):
             fmt.Println("work done")
         }
     }()
     time.Sleep(20 * time.Second)
     cancel()
     time.Sleep(1 * time.Second)
 }

        在上面的代码中,我们首先使用 context.Background() 函数创建一个根 Context 对象 parent,然后使用 WithTimeout 函数创建一个子 Context 对象 ctx,并返回一个超时时间和一个取消函数 cancel。接下来,我们在一个 goroutine 中使用 select 语句监听 Context 对象的 Done 方法和 time.After 函数的返回值,如果 Done 方法返回一个非 nil 的 error,则说明 Context 已经被取消,否则说明 time.After 函数已经超时。在主函数中,我们调用 cancel 函数来通知 Context 对象和其子 Context 对象,使它们都取消执行。最后,我们使用 time.Sleep 函数让程序等待一段时间,以便观察 Context 的执行情况。

1.2.4 WithValue

        WithValue 函数可以用于创建一个 Context 对象,并返回一个包含指定值的 Context 对象。这个值可以是任意类型的数据,可以是基本类型、结构体或者指针等。需要注意的是,这个值只在当前 Context 对象及其子 Context 对象中有效,对于其他 Context 对象来说是不可见的。

func WithValue(parent Context, key interface{}, val interface{}) Context

下面是一个示例代码:

package main
 ​
 import (
     "context"
     "fmt"
 )
 ​
 type userKey struct{}
 ​
 func main() {
     parent := context.Background()
     ctx := context.WithValue(parent, userKey{}, "admin")
     go func() {
         if user, ok := ctx.Value(userKey{}).(string); ok {
             fmt.Printf("user is %s\n", user)
         } else {
             fmt.Println("user is not found")
         }
     }()
     select {}
 }

        在上面的代码中,我们首先使用 context.Background() 函数创建一个根 Context 对象 parent,然后使用 WithValue 函数创建一个子 Context 对象 ctx,并返回一个包含指定值的 Context 对象。接下来,我们在一个 goroutine 中使用 ctx.Value 函数获取 Context 对象中的值,并判断其类型是否为字符串类型。如果是,则输出其值,否则输出 “user is not found”。在主函数中,我们使用select语句使程序一直运行,以便观察 Context 的执行情况。

2. Context 的使用场景

2.1 并发控制

        一个很典型的使用场景是,当我们需要同时启动多个 goroutine 进行任务处理时,我们可以使用 Context 来控制这些 goroutine 的执行。在每个 goroutine 中,我们都可以检测 Context 对象是否被取消,如果是,则退出 goroutine 的执行,否则继续执行。
下面是一个示例代码:

package main
 ​
 import (
     "context"
     "fmt"
     "sync"
 )
 ​
 func worker(ctx context.Context, wg *sync.WaitGroup) {
     defer wg.Done()
     for {
         select {
         default:
             fmt.Println("work")
         case <-ctx.Done():
             return
         }
     }
 }
 ​
 func main() {
     parent := context.Background()
     ctx, cancel := context.WithCancel(parent)
     var wg sync.WaitGroup
     for i := 0; i < 3; i++ {
         wg.Add(1)
         go worker(ctx, &wg)
     }
     cancel()
     wg.Wait()
 }

        在上面的代码中,我们首先使用 context.Background() 函数创建一个根 Context 对象 parent,然后使用 WithCancel 函数创建一个子 Context 对象 ctx,并返回一个取消函数 cancel。接下来,我们使用 sync.WaitGroup 来等待所有的 goroutine 执行完成。在主函数中,我们启动了三个 goroutine 来执行任务,同时使用 cancel 函数来通知这些 goroutine 取消执行。最后,我们使用Wait方法等待所有的 goroutine 执行完成。

2.2 超时控制

        另一个典型的使用场景是,当我们需要对一个操作设置一个超时时间时,我们可以使用 Context 来控制这个操作的执行时间。在操作执行超时时,我们可以通知 Context 对象和其子 Context 对象取消执行。
下面是一个示例代码:

package main
 ​
 import (
     "context"
     "fmt"
     "time"
 )
 ​
 func work(ctx context.Context) {
     for {
         select {
         default:
             fmt.Println("work")
         case <-ctx.Done():
             fmt.Println("work done")
             return
         }
     }
 }

 func main() {
     parent := context.Background()
     ctx, cancel := context.WithTimeout(parent, time.Second*5)
     defer cancel()
     work(ctx)
 }

        在上面的代码中,我们首先使用 context.Background() 函数创建一个根 Context 对象 parent,然后使用 WithTimeout 函数创建一个子 Context 对象 ctx,并返回一个取消函数 cancel。在 work 函数中,我们启动一个无限循环,不断输出 “work”。同时,我们使用 select 语句来等待 Context 对象被取消。在主函数中,我们使用 defer 语句调用 cancel 函数,以确保 Context 对象被取消。由于我们在 WithTimeout 函数中设置了一个 5 秒的超时时间,因此当程序运行超过 5 秒时,work 函数就会停止执行。

2.3 数据库连接

        在使用数据库连接时,我们通常需要保证连接池中的连接数量不会超过一定的阈值。如果连接池中的连接数量超过了阈值,则需要等待连接释放后再进行操作。在这种情况下,我们可以使用 Context 来控制连接的生命周期。
下面是一个示例代码:

package main
 ​
 import (
     "context"
     "database/sql"
     "fmt"
     "sync"
     "time"
 ​
     _ "github.com/go-sql-driver/mysql"
 )
 ​
 const maxConn = 5
 ​
 func main() {
     db, err := sql.Open("mysql", "root:root@tcp(127.0.0.1:3306)/test")
     if err != nil {
         panic(err)
     }
     defer db.Close()
 ​
     ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
     defer cancel()
 ​
     connCh := make(chan *sql.Conn, maxConn)
     var wg sync.WaitGroup
     for i := 0; i < maxConn; i++ {
         wg.Add(1)
         go func() {
             defer wg.Done()
             for {
                 select {
                 case <-ctx.Done():
                     return
                 default:
                     if len(connCh) < maxConn {
                         conn, err := db.Conn(ctx)
                         if err != nil {
                             fmt.Println(err)
                             return
                         }
                         connCh <- conn
                     }
                 }
             }
         }()
     }
     wg.Wait()
 }

        在上面的代码中,我们首先使用 sql.Open 函数打开一个 MySQL 数据库的连接,并返回一个 DB 对象 db。接下来,我们使用 WithTimeout 函数创建一个 Context 对象 ctx,并设置一个超时时间为 5 秒。同时,我们创建一个容量为 maxConn 的 channel 对象 connCh,用于存储数据库连接。在g oroutine 中,我们使用 select 语句等待 Context 对象被取消。在每次循环中,我们检查连接池中连接的数量是否超过了阈值,如果没有,则使用 db.Conn 函数从连接池中获取一个新的连接,并将其存储到 connCh 中。最后,我们使用 sync.WaitGroup 等待所有的 goroutine 执行完成。

####2.4 HTTP 请求

        在使用 HTTP 请求时,我们通常需要设置一个超时时间,以确保请求能够在规定的时间内得到响应。在这种情况下,我们可以使用 Context 来控制HTTP请求的执行时间。
下面是一个示例代码:

package main
 ​
 import (
     "context"
     "fmt"
     "io/ioutil"
     "net/http"
     "time"
 )
 ​
 func main() {
     client := http.DefaultClient
     ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
     defer cancel()
     req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://www.example.com", nil)
     if err != nil {
         fmt.Println(err)
         return
     }
 ​
     resp, err := client.Do(req)
     if err != nil {
         fmt.Println(err)
         return
     }
     defer resp.Body.Close()
 ​
     body, err := ioutil.ReadAll(resp.Body)
     if err != nil {
         fmt.Println(err)
         return
     }
 ​
     fmt.Println(string(body))
 }

        在上面的代码中,我们首先使用 http.DefaultClient 创建一个 HTTP 客户端对象 client。接下来,我们使用 WithTimeout 函数创建一个 Context 对象 ctx,并设置一个超时时间为 5 秒。同时,我们使用 http.NewRequestWithContext 函数创建一个 HTTP 请求对象 req,并将 Context 对象 ctx 作为参数传递给该函数。在 Do 函数中,我们会自动将 Context 对象 ctx 传递给 HTTP 请求,并在超时时间到达后自动取消该请求。

####2.5 gRPC 请求

        在使用 gRPC 请求时,我们通常需要设置一个超时时间,以确保请求能够在规定的时间内得到响应。在这种情况下,我们可以使用 Context 来控制 gRPC 请求的执行时间。
下面是一个示例代码:

package main
 ​
 import (
     "context"
     "fmt"
     "log"
     "time"
 ​
     pb "github.com/example/helloworld"
     "google.golang.org/grpc"
 )
 ​
 const (
     address     = "localhost:50051"
     defaultName = "world"
 )
 ​
 func main() {
     conn, err := grpc.Dial(address, grpc.WithInsecure())
     if err != nil {
         log.Fatalf("did not connect: %v", err)
     }
     defer conn.Close()
 ​
     c := pb.NewGreeterClient(conn)
 ​
     ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
     defer cancel()
 ​
     r, err := c.SayHello(ctx, &pb.HelloRequest{Name: defaultName})
     if err != nil {
         log.Fatalf("could not greet: %v", err)
     }
     log.Printf("Greeting: %s", r.GetMessage())
 }

        在上面的代码中,我们首先使用 grpc.Dial 函数创建一个 gRPC 客户端连接对象 conn。接下来,我们使用 pb.NewGreeterClient 函数创建一个 GreeterClient 对象 c。然后,我们使用 WithTimeout 函数创建一个 Context 对象 ctx,并设置一个超时时间为 5 秒。最后,我们使用 GreeterClient 对象 c 的 SayHello 函数发送一个 gRPC 请求,并将 Context 对象 ctx 作为参数传递给该函数。在 SayHello 函数中,我们会自动将 Context 对象 ctx 传递给 gRPC 请求,并在超时时间到达后自动取消该请求。

Go Context 到底放第一个参数传,还是放结构体里?

作为函数的第一个参数

优点:

  1. 明确性:将context作为第一个参数,清晰地表明了函数执行的上下文依赖,增强了代码的可读性和意图表达。这种方式符合Go的设计哲学,即显式优于隐式。

  2. 易于测试:测试时可以轻松创建并传递一个自定义的context.Context实例,便于控制测试中的超时和取消逻辑,无需修改函数签名或结构体定义。

  3. 标准一致性:Go的标准库广泛采用了这种模式,比如net/http包中的ServeHTTP方法。遵循这一标准使得代码风格统一,便于其他开发者理解和维护。

缺点:

  1. 参数列表增长:对于参数较多的函数,额外增加一个context.Context可能会让函数签名显得冗长,特别是当多个函数嵌套调用时,每一层都需要传递context

  2. 侵入性:虽然增加了灵活性,但也意味着每一个需要考虑取消或超时逻辑的函数都需要调整,对于既有代码库的改造成本较高。

嵌入到结构体中

优点:

  1. 减少函数签名复杂度:将context.Context作为一个字段嵌入到结构体中,可以减少函数参数的数量,使函数签名更加简洁。

  2. 封装性:对于内部逻辑复杂的服务,将context隐藏在结构体内部,可以对外提供更加抽象和友好的接口,提高代码的封装性。

缺点:

  1. 测试复杂度增加:如果结构体中的context字段不是公开的,测试时可能需要通过构造特定的结构体实例来传递特定的上下文信息,这可能使得测试代码变得复杂。

  2. 灵活性降低:一旦context作为结构体的一部分,函数调用时就失去了直接控制context的能力,比如无法在运行时轻易改变超时时间或取消策略。

实践建议

  • 常规操作:对于大多数情况,遵循Go标准库的推荐,将context作为函数的第一个参数传递是最佳选择。这样做既体现了Go的简洁和明了,也便于维护和测试。

  • 高度封装的服务:在设计高度封装的内部服务或复杂的API时,可以考虑将context嵌入到结构体中,尤其是当需要在整个服务生命周期内管理上下文时。但需权衡好封装性和测试便利性之间的关系。

  • 混合使用:在某些场景下,你可能会发现结合两者使用的效果更好。例如,在服务初始化阶段,将context作为结构体字段管理,而在服务的具体操作函数中,依然将context作为第一个参数传递,以保持操作的灵活性。

        总之,选择将context放在哪里,应基于项目的具体需求、代码的可读性和维护性综合考量。无论哪种方式,关键在于理解并充分利用context机制,以提升程序的健壮性和可维护性。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

深入理解对象池 sync.Pool

文章目录 前言应用使用源码走读数据结构Get获取对象Put归还对象poolDeque分析GC时 总结 前言 当多个 goroutine 都需要创建同⼀种对象的时候&#xff0c;如果 goroutine 数量过多&#xff0c;导致对象的创建剧增&#xff0c;进⽽导致 GC 压⼒增大。形成下面的恶性循环&#xf…

项目管理(软设软考高频)

一、进度管理 1.Gantt图 2.PERT图 二、风险管理 三、沟通管理 四、成本管理

在Java中,实现数据库连接通常使用JDBC

学习总结 1、掌握 JAVA入门到进阶知识(持续写作中……&#xff09; 2、学会Oracle数据库入门到入土用法(创作中……&#xff09; 3、手把手教你开发炫酷的vbs脚本制作(完善中……&#xff09; 4、牛逼哄哄的 IDEA编程利器技巧(编写中……&#xff09; 5、面经吐血整理的 面试技…

gradle下载的jar包,源码出现Decompiled .class file, bytecode version

如下是问题截图 问题产生原因&#xff1a; gradle依赖下载只下载了jar包&#xff0c;这导致idea在读取jar包时&#xff0c;需要通过Fernflower技术对jar包进行反编译&#xff0c;而反编译过程中只会保留源码信息&#xff0c;因此注释等额外信息全部丢失 解决方案&#xff1a…

[357]基于springboot的中小型制造企业质量管理系统

摘 要 信息数据从传统到当代&#xff0c;是一直在变革当中&#xff0c;突如其来的互联网让传统的信息管理看到了革命性的曙光&#xff0c;因为传统信息管理从时效性&#xff0c;还是安全性&#xff0c;还是可操作性等各个方面来讲&#xff0c;遇到了互联网时代才发现能补上自古…

SAP(PP生产制造)拆解工单业务处理

1、BOM维护 要拆解的成品或半成品要和原成品、半成品BOM一致 2、创建拆解工单 CO01选择拆解工单的类型&#xff0c;以及填写拆解的物料和拆解工厂 维护工单组件 注意&#xff1a; 1、拆解入库组件的数量需要维护为负数 2、拆解工单投料组件数量维护为正数 3、拆解工单收发…

NavVis LX系列产品典型应用—现有住宅装修改造-沪敖3D

现有住宅装修改造项目的 数据捕捉和测量技术 当Jay Ure着手翻新和美化自己的新家时&#xff0c;他敏锐地发现这是现场测试NavVis VLX的绝佳机会。 为了全面评估&#xff0c;他聘请了一位工程师&#xff0c;采用传统的全站仪技术进行地形测绘。之后&#xff0c;他用移动扫描设…

【初阶数据结构篇】链式结构二叉树(续)

文章目录 须知 &#x1f4ac; 欢迎讨论&#xff1a;如果你在学习过程中有任何问题或想法&#xff0c;欢迎在评论区留言&#xff0c;我们一起交流学习。你的支持是我继续创作的动力&#xff01; &#x1f44d; 点赞、收藏与分享&#xff1a;觉得这篇文章对你有帮助吗&#xff1…

qt QTabWidget详解

1、概述 QTabWidget是Qt框架中的一个控件&#xff0c;它提供了一个标签页式的界面&#xff0c;允许用户在不同的页面&#xff08;或称为标签&#xff09;之间切换。每个页面都可以包含不同的内容&#xff0c;如文本、图像、按钮或其他小部件。QTabWidget非常适合用于创建具有多…

Linux系统基础-多线程超详细讲解(5)_单例模式与线程池

个人主页&#xff1a;C忠实粉丝 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 C忠实粉丝 原创 Linux系统基础-多线程超详细讲解(5)_单例模式与线程池 收录于专栏[Linux学习] 本专栏旨在分享学习Linux的一点学习笔记&#xff0c;欢迎大家在评论区交流讨论&a…

Spark中的宽窄依赖

一、什么是依赖关系 这里通过一张图来解释&#xff1a; result_rdd是由tuple_rdd使用reduceByKey算子得到的&#xff0c; 而tuple_rdd是由word_rdd使用map算子得到的&#xff0c;word_rdd又是由input_rdd使用flatMap算子得到的。它们之间的关系就称为依赖关系&#xff01; 二…

[每周一更]-(第121期):模拟面试|微服务架构面试思路解析

这一系列针对Go面试题整理,仅供参考 文章目录 00|综合服务治理方案:怎么保证微服务应用的高可用?1. **什么是微服务架构?**2. **怎么保证微服务架构的高可用?**3. **怎么判定服务是否已经健康?**4. **如果服务不健康该怎么办?**5. **怎么判定服务已经从不健康状态恢复过…

一体化运维监控管理平台详解:构建高效运维体系

在当今数字化转型的大潮中&#xff0c;IT系统的复杂性和规模不断扩大&#xff0c;运维工作的挑战也随之增加。为了应对这一挑战&#xff0c;我们推出了一体化运维监控管理平台&#xff0c;旨在通过全面、智能的监控手段&#xff0c;提升运维效率&#xff0c;保障业务连续性。本…

FBX福币交易所A股三大指数小幅低开 稀土永磁板块回调

查查配分析11月5日电 周二,A股三大指数小幅低开。沪指开盘跌0.10%报3306.81点,深证成指开盘跌0.09%报10653.20点,创业板指开盘跌0.05%报2184.90点。 FBX福币凭借用户友好的界面和对透明度的承诺,迅速在加密货币市场中崭露头角,成为广大用户信赖的平台。 来源:同花顺iFinD 盘面…

【数据分享】1981-2024年我国逐日平均气温栅格数据(免费获取)

气象数据一直是一个价值很高的数据&#xff0c;它被广泛用于各个领域的研究当中。这其中&#xff0c;又以平均气温数据最为常用&#xff01;之前我们分享过来源于美国国家海洋和大气管理局&#xff08;NOAA&#xff09;下设的国家环境信息中心(NCEI)发布的1929-2024年全球站点的…

云渲染与汽车CGI图像技术优势和劣势

在数字时代&#xff0c;云渲染技术以其独特的优势在汽车CGI图像制作中占据了重要地位。云渲染通过利用云计算的分布式处理能力&#xff0c;将渲染任务分配给云端的服务器集群进行计算&#xff0c;从而实现高效、高质量的渲染效果。 这种技术的优势主要体现在以下几个方面&#…

QT仿QQ聊天项目,第三节,实现主界面(好友列表)

目录 一&#xff0c;主界面示例 二&#xff0c;主界面控件组成 三&#xff0c;好友列表实现 1&#xff0c;好友列表的实现原理 2&#xff0c;实现示例代码 一&#xff0c;主界面示例 二&#xff0c;主界面控件组成 三&#xff0c;好友列表实现 1&#xff0c;好友列表的实现…

20241105编译荣品的Android13并给荣品PRO-RK3566开发板刷机

20241105编译荣品的Android13并给荣品PRO-RK3566开发板刷机 2024/11/5 19:10 荣品SDK版本呢&#xff1a;rk-android13-20240713.tgz cf9cea18d26ad7db31b000a7d13b09c2 rk-android13-20240713.tgz 精简步骤&#xff1a; rootrootrootroot-desktop:~$ cd Android13.0/rootrootr…

KVM虚拟机的冷热迁移

首先了解在KVM&#xff08;Kernel-based Virtual Machine&#xff09;环境中&#xff0c;冷热迁移是指将虚拟机从一台主机迁移到另一台主机的过程&#xff0c;根据虚拟机是否需要停机&#xff0c;迁移分为热迁移和冷迁移&#xff1a; 冷迁移&#xff08;Cold Migration&#x…

讲讲软件业务设计原则?

大家好&#xff0c;我是锋哥。今天分享关于【讲讲软件业务设计原则&#xff1f;】面试题。希望对大家有帮助&#xff1b; 讲讲软件业务设计原则&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 在软件开发过程中&#xff0c;如何设计一个既高效又可维护…