Go并发编程实战:Gsync/jobsync库实现任务并行与结果同步

news2026/5/3 3:26:02
1. 项目概述与核心价值如果你在分布式系统、微服务或者大规模数据处理领域工作过大概率遇到过这样的场景一个任务需要拆分成多个子任务分发给不同的工作节点去执行然后等待所有结果返回再进行下一步的聚合或处理。这个看似简单的“分而治之”模式在实际编码中却常常伴随着一堆繁琐的细节——如何分发任务如何收集结果节点挂了怎么办任务超时了怎么处理结果数据太大内存放不下怎么办Gsync/jobsync这个项目就是为了优雅地解决上述所有痛点而生的。它不是一个重量级的任务调度框架而是一个轻量级、高可用的 Go 语言库核心目标就一个让“同步执行一批异步任务”变得像调用本地函数一样简单可靠。我最初是在一个需要实时聚合数十个微服务指标数据的项目里接触到类似需求的当时自己手搓了一套轮子过程堪称血泪史。后来发现了jobsync其设计之精巧让我有种“相见恨晚”的感觉。它非常适合那些需要并行处理独立任务、又要求最终同步等待所有结果的场景比如批量调用外部 API、并行执行数据库查询、分布式渲染、数据校验等。简单来说jobsync提供了一个高级抽象JobManager。你只需要定义好任务一个简单的函数把它提交给管理器管理器会负责在后台的 Goroutine 池中执行它们。而你作为调用者可以同步地等待所有任务完成并安全地获取它们的结果或错误。整个过程你无需手动创建 Goroutine、使用sync.WaitGroup、设计 channel 来收集结果更不用担心 Goroutine 泄露和 panic 导致的程序崩溃。jobsync把这些复杂性全部封装了起来暴露出一个干净、易用的接口。2. 核心设计思想与架构拆解Gsync/jobsync的设计哲学深深植根于 Go 语言的并发模型并在此基础上做了极致的封装和强化。理解其设计思想能帮助我们在更复杂的场景下灵活运用它。2.1 基于 Goroutine 池的负载管理与直接go func()创建海量 Goroutine 不同jobsync内部维护了一个可配置的 Goroutine 工作池。这是其第一个关键设计。直接无限制地创建 Goroutine 虽然轻量但在任务数量极大例如数十万时仍然会带来显著的调度开销和内存占用。通过工作池jobsync可以将任务排队由固定数量的“工人” Goroutine 依次处理实现了对并发度的精细控制。注意这里的“池”并非指传统的 I/O 阻塞任务池如数据库连接池而是用于限制并发 Goroutine 数量的计算池。对于 CPU 密集型任务合理设置池大小通常接近或等于 CPU 核心数可以避免过度切换带来的性能损耗。对于 I/O 密集型任务则可以适当调大。2.2 任务、结果与错误的统一封装在原生 Go 中处理多个 Goroutine 的结果通常需要自定义结构体并通过 channel 传递还要小心处理错误。jobsync引入了Job和Result的概念。Job 就是一个func() (interface{}, error)类型的函数。它代表一个可执行的工作单元。Result 是Job执行后的产物它内部封装了返回值interface{}和错误error。jobsync保证无论任务正常结束还是 panic都会生成一个Result对象。对于 panicjobsync会进行 recover并将其转换为一个包含相应错误的Result。这个设计将异步执行的不确定性封装成了同步访问的确定性对象这是整个库易用性的基石。2.3 同步等待与上下文传播jobsync的核心同步原语是JobManager的Wait方法。调用Wait会阻塞直到所有提交的任务都执行完毕无论成功或失败。这背后通常基于sync.WaitGroup实现。更重要的是jobsync支持 Go 标准库的context.Context。你可以在创建JobManager或提交任务时传入一个context。这个context被用于两处任务超时/取消如果context在任务开始执行前被取消如超时那么该任务将不会被执行其对应的Result会直接包含一个“任务已取消”的错误。任务间传递context会被传递给每一个任务函数。这意味着你的任务逻辑可以感知到外部的取消信号从而实现优雅的中断。例如当用户前端取消请求时你可以取消根context所有关联的并行任务都会收到信号并尝试终止。2.4 内存安全的流式结果处理对于海量任务将所有Result一次性保存在内存中等待Wait返回可能不现实。jobsync提供了另一种模式流式处理。你可以在提交任务后不立即调用Wait而是通过JobManager提供的通道Channel来逐个消费已完成任务的Result。这种方式允许你在任务还在执行的同时就开始处理已完成的结果极大地降低了内存峰值并可能缩短端到端的延迟。3. 从入门到精通核心 API 详解与实战理论说再多不如一行代码。让我们深入jobsync的核心 API并通过对比不同用法来掌握其精髓。3.1 基础用法快速同步一批任务假设我们需要从三个不同的数据源并行获取用户信息、订单信息和商品信息。package main import ( context fmt time github.com/gsync/jobsync // 假设这是正确的导入路径请以官方文档为准 ) func main() { // 1. 创建一个 JobManager默认使用无界队列和足够的 worker。 // 在实际生产中我们通常会指定 Worker 数量。 mgr : jobsync.NewJobManager(jobsync.WithWorkers(4)) // 2. 提交任务。每个任务是一个简单的函数。 job1 : mgr.Submit(func(ctx context.Context) (interface{}, error) { time.Sleep(100 * time.Millisecond) // 模拟网络IO return 用户数据 from API-A, nil }) job2 : mgr.Submit(func(ctx context.Context) (interface{}, error) { time.Sleep(200 * time.Millisecond) return 42, nil // 返回订单数量 }) job3 : mgr.Submit(func(ctx context.Context) (interface{}, error) { time.Sleep(50 * time.Millisecond) return []string{商品A, 商品B}, nil }) // 3. 同步等待所有任务完成 mgr.Wait() // 4. 获取结果 if res1, ok : job1.Result(); ok res1.Error() nil { fmt.Printf(用户数据: %v\n, res1.Value()) } if res2, ok : job2.Result(); ok res2.Error() nil { fmt.Printf(订单数量: %v\n, res2.Value()) } if res3, ok : job3.Result(); ok res3.Error() nil { fmt.Printf(商品列表: %v\n, res3.Value()) } }关键点解析Submit方法是非阻塞的它会立即返回一个Job句柄并将任务函数放入内部队列。Wait()会阻塞当前 Goroutine直到所有通过该mgr提交的任务都执行完毕。Job.Result()方法返回(Result, bool)。第二个bool值表示结果是否已就绪在Wait()后通常为true。我们必须先检查错误res.Error()再使用res.Value()。3.2 进阶用法带上下文与错误处理现在我们为任务加上超时控制并完善错误处理。func main() { ctx, cancel : context.WithTimeout(context.Background(), 150*time.Millisecond) defer cancel() // 良好习惯确保资源释放 // 创建 Manager 时传入 Context这个 Context 会作为所有任务的父 Context。 mgr : jobsync.NewJobManager(jobsync.WithContext(ctx), jobsync.WithWorkers(2)) // 任务1正常任务 job1 : mgr.Submit(func(ctx context.Context) (interface{}, error) { select { case -time.After(100 * time.Millisecond): return Data, nil case -ctx.Done(): // 监听取消信号 return nil, ctx.Err() // 返回上下文错误 } }) // 任务2会超时的任务 job2 : mgr.Submit(func(ctx context.Context) (interface{}, error) { time.Sleep(300 * time.Millisecond) // 这个睡眠时间超过了父 Context 的 150ms 超时 // 注意即使任务函数还在 sleep如果 Manager 的 Context 超时Wait() 会立即返回。 // 但这个任务函数本身可能仍在后台运行除非它像任务1一样主动检查 ctx.Done()。 // 更好的做法是 // select { // case -time.After(300 * time.Millisecond): // return Late Data, nil // case -ctx.Done(): // return nil, ctx.Err() // } return Too Late, nil }) // 任务3会 panic 的任务 job3 : mgr.Submit(func(ctx context.Context) (interface{}, error) { panic(something went terribly wrong inside the job!) }) mgr.Wait() // 这里会在 150ms 超时后返回因为父 Context 超时了。 // 收集并分析所有结果 jobs : []*jobsync.Job{job1, job2, job3} for i, job : range jobs { if res, ok : job.Result(); ok { if err : res.Error(); err ! nil { fmt.Printf(Job %d failed: %v\n, i1, err) // 错误可能是 context.DeadlineExceeded, context.Canceled, 或者是 panic 转换来的错误。 } else { fmt.Printf(Job %d succeeded: %v\n, i1, res.Value()) } } else { fmt.Printf(Job %d result not ready (should not happen after Wait)\n, i1) } } }实操心得Context 的传递是链式的传给JobManager的context是任务的“父上下文”。任务函数内部应该始终使用传入的这个ctx参数而不是自己新建一个context.Background()。这样才能实现全局的取消传播。Panic 安全这是jobsync的一大亮点。即使任务函数 panic也不会导致整个程序崩溃。panic 会被jobsync在内部 recover并转换为一个包含错误信息的Result。你可以在res.Error()中获取到它。这对于集成不可控的第三方库代码非常有用。超时控制的两面性Manager的Context超时会导致Wait()立即返回但已经提交且正在执行的任务 Goroutine 可能不会立即停止除非任务函数内部主动检查ctx.Done()。最佳实践是任务函数中任何可能阻塞的操作如网络请求、长时循环都应该与ctx.Done()通道进行select以实现主动中断。3.3 高级用法流式结果处理当处理成千上万个任务时我们可能等不起所有任务完成或者内存吃不消。这时就需要流式处理。func main() { mgr : jobsync.NewJobManager(jobsync.WithWorkers(5)) // 假设我们要处理 1000 个URL urlList : generateURLs(1000) // 提交所有任务 for _, url : range urlList { u : url // 重要闭包捕获循环变量需要创建副本 mgr.Submit(func(ctx context.Context) (interface{}, error) { return fetchURL(ctx, u) // fetchURL 是一个模拟的抓取函数 }) } // 关键我们不调用 mgr.Wait()而是获取结果通道 resultChan : mgr.Results() // 从通道中持续读取已完成的结果 for result : range resultChan { // 通道关闭意味着所有任务都执行完毕且结果都已发送 if err : result.Error(); err ! nil { log.Printf(Fetch failed: %v, err) // 可以实现重试逻辑将失败任务重新提交 } else { data : result.Value().([]byte) // 类型断言实际使用需谨慎 processData(data) } } // 循环结束后所有任务自然处理完毕无需额外调用 Wait() } // generateURLs, fetchURL, processData 为模拟函数注意事项Results()方法返回一个-chan Result只读通道。一旦调用Results()JobManager的内部状态会发生变化通常不能再提交新任务。你必须消费这个通道直到它被关闭。如果中途跳出循环而不读完可能会导致发送结果的 Goroutine 永远阻塞造成 Goroutine 泄露。流式模式下任务的执行顺序和结果返回的顺序是不确定的哪个任务先完成其结果就先进入通道。这种模式非常适合实现“生产者-消费者”模型你可以在一个 Goroutine 中提交任务在另一个 Goroutine 中消费结果。4. 性能调优与配置详解Gsync/jobsync提供了多种配置选项以适应不同的应用场景。理解这些选项是将其性能发挥到极致的关键。4.1 Worker 数量并发度的权衡WithWorkers(n int)是最重要的配置项。CPU 密集型任务建议n设置为runtime.NumCPU()或runtime.GOMAXPROCS(0)。这样可以让每个 CPU 核心满载运行一个任务最大化利用计算资源同时避免过多的 Goroutine 切换开销。I/O 密集型任务如网络请求、磁盘读写可以设置得更大比如NumCPU() * 2、NumCPU() * 5甚至更高。因为任务大部分时间在等待 I/OGoroutine 会主动让出 CPU更多的 Worker 可以让 CPU 在等待期间去处理其他就绪的任务。一个常见的经验公式是n (I/O等待时间 / CPU处理时间 1) * NumCPU()但这个比例很难精确估算需要通过压测来确定。默认值如果不设置jobsync可能会使用一个默认值如runtime.NumCPU()或者使用无限制的 Goroutine类似直接go func()。务必查阅最新官方文档或源码确认默认行为。4.2 队列容量应对突发流量WithQueueSize(size int)用于设置内部任务队列的长度。当所有 Worker 都忙碌时新提交的任务会被放入队列等待。如果队列已满Submit方法的行为取决于具体实现可能会阻塞也可能会返回一个错误。这需要仔细查看文档。设置策略无界队列size0或负数任务永远不会因为队列满而被拒绝。适用于任务必须被执行的场景但风险是内存可能被无限增长的任务队列耗尽“背压”问题。有界队列设置一个合理的容量如Workers * 2。当队列满时Submit的阻塞行为可以天然形成背压迫使上游调用者慢下来。这有助于维持系统的稳定性。你需要根据任务的平均处理速度和系统的容忍度来设定这个值。4.3 上下文与超时控制如前所述WithContext(ctx context.Context)至关重要。它不仅用于取消也用于设置截止时间。全局超时通过context.WithTimeout创建带超时的ctx并传给Manager可以为整个批量任务设置一个总时限。链路追踪你可以使用集成 OpenTelemetry 或 OpenTracing 的context这样每个子任务的执行 span 都能关联到父 span便于分布式追踪。4.4 内存与生命周期管理及时清理当一个JobManager的所有任务都执行完毕通过Wait返回或Results通道关闭后它占用的资源如 worker goroutines, internal channels应该会被垃圾回收。但最佳实践是让JobManager的变量离开作用域或者显式地不再持有其引用以帮助 GC。避免长期存活的 Manager对于一次性批量任务建议每次使用时创建新的JobManager用完后丢弃。避免创建一个全局的、长期存活的Manager来服务所有请求这可能导致不同请求的任务相互影响比如一个慢任务阻塞了队列影响其他请求。每个独立的请求或工作单元使用独立的Manager是更清晰的模式。5. 实战场景深度剖析让我们看几个更贴近真实生产的例子感受jobsync如何解决实际问题。5.1 场景一微服务聚合查询API Gateway模式在一个电商首页需要展示用户信息、推荐商品、促销活动、未读消息等。这些数据来自不同的后端微服务。func assembleHomePage(ctx context.Context, userID string) (*HomePageData, error) { // 为这个聚合请求创建一个独立的 JobManager继承请求的上下文包含超时、追踪ID等 mgr : jobsync.NewJobManager( jobsync.WithContext(ctx), jobsync.WithWorkers(4), // 并发调用4个下游服务 jobsync.WithQueueSize(10), ) var userJob, recJob, promoJob, msgJob *jobsync.Job // 并行提交所有数据获取任务 userJob mgr.Submit(func(ctx context.Context) (interface{}, error) { return userService.GetProfile(ctx, userID) }) recJob mgr.Submit(func(ctx context.Context) (interface{}, error) { return recommendationService.GetForUser(ctx, userID, 10) }) promoJob mgr.Submit(func(ctx context.Context) (interface{}, error) { return promotionService.GetActive(ctx) }) msgJob mgr.Submit(func(ctx context.Context) (interface{}, error) { return messageService.GetUnreadCount(ctx, userID) }) // 等待所有下游调用完成或超时 mgr.Wait() homeData : HomePageData{} var finalErr error // 聚合结果处理部分失败 if res, ok : userJob.Result(); ok { if err : res.Error(); err ! nil { log.Printf(Failed to get user profile: %v, err) // 可能设置一个默认用户信息或者将错误记录为最终错误的一部分 finalErr fmt.Errorf(partial failure: user profile) } else { homeData.User res.Value().(*UserProfile) } } // ... 类似地处理其他 job // 即使部分失败只要核心数据如用户信息拿到了仍然可以返回部分成功的页面 if homeData.User ! nil { return homeData, finalErr // 可能返回页面数据和聚合的错误信息 } else { return nil, errors.New(failed to load essential data) } }优势将串行调用假设每个服务耗时100ms总耗时400ms改为并行调用总耗时约100ms极大提升了接口响应速度。统一的错误处理和上下文传播让代码更健壮。5.2 场景二数据校验与清洗管道有一个数据导入管道需要对每一条记录并行执行多种校验规则格式校验、业务规则校验、重复性校验等。type Record struct { /* fields */ } type ValidationResult struct { Rule string Pass bool Msg string } func validateRecord(ctx context.Context, record Record) ([]ValidationResult, error) { validationRules : []func(context.Context, Record) ValidationResult{ validateFormat, validateBusinessRuleA, validateBusinessRuleB, checkDuplication, } mgr : jobsync.NewJobManager(jobsync.WithContext(ctx), jobsync.WithWorkers(len(validationRules))) // 为每条规则提交一个校验任务 var jobs []*jobsync.Job for _, ruleFunc : range validationRules { rule : ruleFunc // 闭包捕获 job : mgr.Submit(func(ctx context.Context) (interface{}, error) { // 规则函数内部应检查 ctx.Done()以便在超时时快速返回 select { case -ctx.Done(): return ValidationResult{Rule: getRuleName(rule), Pass: false, Msg: ctx.Err().Error()}, nil default: return rule(ctx, record), nil } }) jobs append(jobs, job) } mgr.Wait() results : make([]ValidationResult, 0, len(validationRules)) for _, job : range jobs { if res, ok : job.Result(); ok res.Error() nil { results append(results, res.Value().(ValidationResult)) } // 如果 job 本身执行出错如 panic我们可以选择忽略或记录这里简化处理 } // 判断是否有任何规则校验失败 for _, r : range results { if !r.Pass { return results, fmt.Errorf(validation failed for rule: %s, msg: %s, r.Rule, r.Msg) } } return results, nil }优势复杂的校验逻辑被拆分成独立的、可并行执行的函数代码结构清晰。利用jobsync的 panic 恢复机制即使某条校验规则有 bug 导致 panic也不会影响其他规则的执行和整个校验流程。5.3 场景三批量图片缩略图生成一个用户上传了100张图片需要为每张图片生成大、中、小三种尺寸的缩略图。func generateThumbnailsForBatch(ctx context.Context, imagePaths []string) error { // 使用流式结果处理避免在内存中保存所有图片数据 mgr : jobsync.NewJobManager( jobsync.WithContext(ctx), jobsync.WithWorkers(runtime.NumCPU()), // 图片处理是CPU密集型 jobsync.WithQueueSize(100), ) // 提交任务每张图片一个任务任务内串行生成三种尺寸 for _, path : range imagePaths { imgPath : path mgr.Submit(func(ctx context.Context) (interface{}, error) { img, err : loadImage(imgPath) if err ! nil { return nil, err } defer img.Close() // 生成三种尺寸这里可以进一步并行化但考虑到单个任务已较重串行亦可 sizes : []struct{ w, h int }{{1024, 768}, {320, 240}, {150, 150}} for _, size : range sizes { select { case -ctx.Done(): return nil, ctx.Err() // 支持取消 default: thumb, err : resizeImage(img, size.w, size.h) if err ! nil { return nil, fmt.Errorf(resize failed for %s: %w, imgPath, err) } if err : saveThumbnail(thumb, imgPath, size.w, size.h); err ! nil { return nil, err } } } return imgPath, nil // 返回处理成功的文件名 }) } // 流式消费结果实时记录进度和错误 successCount : 0 for result : range mgr.Results() { if err : result.Error(); err ! nil { log.Printf(Failed to process image: %v, err) // 可以在这里将失败任务加入重试队列 } else { successCount log.Printf(Successfully processed: %s, result.Value()) } } log.Printf(Batch processing finished. Success: %d/%d, successCount, len(imagePaths)) return nil }优势充分利用多核 CPU 并行处理计算密集型任务。流式结果处理允许我们在第一批图片处理完成时就开始上传到云存储或通知前端而不必等待全部100张处理完提升了用户体验。6. 常见陷阱、疑难排查与最佳实践即使使用了jobsync这样的优秀工具如果使用不当依然会掉进坑里。下面是我在实践中总结的一些经验和教训。6.1 闭包变量捕获陷阱这是 Go 并发编程的老问题在jobsync中尤其需要注意。错误示例for _, url : range urls { mgr.Submit(func(ctx context.Context) (interface{}, error) { return fetch(url) // 问题所有 Goroutine 捕获的是同一个 url 变量 }) }循环结束时所有任务函数里的url变量都指向urls切片的最后一个元素。正确做法for _, url : range urls { u : url // 在循环内创建局部变量副本 mgr.Submit(func(ctx context.Context) (interface{}, error) { return fetch(u) // 捕获局部变量 u }) }或者使用带参数的函数for _, url : range urls { mgr.Submit(fetchJob(url)) // fetchJob 返回一个闭包其中 url 作为参数传入 } func fetchJob(url string) func(context.Context) (interface{}, error) { return func(ctx context.Context) (interface{}, error) { return fetch(url) } }6.2 资源泄露未消费的结果通道在使用Results()流式模式时必须确保完整消费通道。// 危险 go func() { resultChan : mgr.Results() // 只读前10个结果就退出了 for i : 0; i 10; i { -resultChan } // 循环退出但 resultChan 未被读完发送方的 Goroutine 会永久阻塞。 }() // 安全做法确保循环直到通道关闭 go func() { for result : range mgr.Results() { process(result) } // 循环自然结束意味着所有结果已处理完毕 }()6.3 任务函数中的阻塞与上下文检查任务函数内部如果进行网络调用、等待 channel、执行长循环务必加入对ctx.Done()的检查以实现可取消性。mgr.Submit(func(ctx context.Context) (interface{}, error) { // 模拟一个可能很慢的 HTTP 请求 req, _ : http.NewRequestWithContext(ctx, GET, url, nil) // 关键使用带 ctx 的请求 resp, err : http.DefaultClient.Do(req) if err ! nil { return nil, err } defer resp.Body.Close() // ... 处理 resp }) // 或者对于非标准库的阻塞操作 mgr.Submit(func(ctx context.Context) (interface{}, error) { resultChan : make(chan interface{}) go doSomeLegacyBlockingWork(resultChan) select { case res : -resultChan: return res, nil case -ctx.Done(): // 尝试停止 legacy work (如果可能) stopLegacyWork() return nil, ctx.Err() } })6.4 错误处理策略jobsync提供了两种错误任务执行返回的错误和 panic 转换的错误。你需要制定统一的策略。快速失败任何一个任务失败就立即取消所有其他任务通过取消Manager的Context并返回错误。部分成功收集所有成功和失败的结果进行聚合。对于失败的任务可能需要进行重试、记录日志或返回给用户部分数据。重试逻辑可以在获取到失败Result后根据错误类型决定是否重新提交一个新的相同任务到Manager注意在Wait()之后不能再向同一个Manager提交任务。更常见的模式是使用一个外部的重试机制如github.com/cenkalti/backoff/v4。6.5 性能监控与调试度量 Worker 利用率你可以通过暴露的指标或自己包装Submit函数来监控任务队列长度和 Worker 忙碌状态。如果队列经常满可能需要增加 Worker 或优化任务处理速度。如果 Worker 经常空闲可能可以减少 Worker 数量以节省资源。追踪任务链路如前所述通过传递集成了追踪信息的context.Context你可以在 Jaeger、Zipkin 等工具中看到每个子任务的耗时和关系图这对于诊断性能瓶颈至关重要。记录慢任务在任务函数开头记录开始时间结尾记录结束时间可以帮你发现哪些任务执行异常缓慢。Gsync/jobsync将 Go 并发编程中最繁琐、最容易出错的部分进行了标准化和封装。它没有引入任何新的复杂概念只是将Goroutine,channel,sync.WaitGroup,context这些原生工具组合成了一个更趁手的“瑞士军刀”。在需要并行处理、同步等待的场景下它几乎总是比手动管理这些底层原语更安全、更清晰、更高效。下次当你面对需要“同时做几件事然后等它们都做完”的需求时不妨先想想是不是可以用jobsync来优雅地实现。

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

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

相关文章

SpringBoot-17-MyBatis动态SQL标签之常用标签

文章目录 1 代码1.1 实体User.java1.2 接口UserMapper.java1.3 映射UserMapper.xml1.3.1 标签if1.3.2 标签if和where1.3.3 标签choose和when和otherwise1.4 UserController.java2 常用动态SQL标签2.1 标签set2.1.1 UserMapper.java2.1.2 UserMapper.xml2.1.3 UserController.ja…

wordpress后台更新后 前端没变化的解决方法

使用siteground主机的wordpress网站,会出现更新了网站内容和修改了php模板文件、js文件、css文件、图片文件后,网站没有变化的情况。 不熟悉siteground主机的新手,遇到这个问题,就很抓狂,明明是哪都没操作错误&#x…

网络编程(Modbus进阶)

思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…

UE5 学习系列(二)用户操作界面及介绍

这篇博客是 UE5 学习系列博客的第二篇,在第一篇的基础上展开这篇内容。博客参考的 B 站视频资料和第一篇的链接如下: 【Note】:如果你已经完成安装等操作,可以只执行第一篇博客中 2. 新建一个空白游戏项目 章节操作,重…

IDEA运行Tomcat出现乱码问题解决汇总

最近正值期末周,有很多同学在写期末Java web作业时,运行tomcat出现乱码问题,经过多次解决与研究,我做了如下整理: 原因: IDEA本身编码与tomcat的编码与Windows编码不同导致,Windows 系统控制台…

利用最小二乘法找圆心和半径

#include <iostream> #include <vector> #include <cmath> #include <Eigen/Dense> // 需安装Eigen库用于矩阵运算 // 定义点结构 struct Point { double x, y; Point(double x_, double y_) : x(x_), y(y_) {} }; // 最小二乘法求圆心和半径 …

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式

一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明&#xff1a;假设每台服务器已…

XML Group端口详解

在XML数据映射过程中&#xff0c;经常需要对数据进行分组聚合操作。例如&#xff0c;当处理包含多个物料明细的XML文件时&#xff0c;可能需要将相同物料号的明细归为一组&#xff0c;或对相同物料号的数量进行求和计算。传统实现方式通常需要编写脚本代码&#xff0c;增加了开…

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器的上位机配置操作说明

LBE-LEX系列工业语音播放器|预警播报器|喇叭蜂鸣器专为工业环境精心打造&#xff0c;完美适配AGV和无人叉车。同时&#xff0c;集成以太网与语音合成技术&#xff0c;为各类高级系统&#xff08;如MES、调度系统、库位管理、立库等&#xff09;提供高效便捷的语音交互体验。 L…

(LeetCode 每日一题) 3442. 奇偶频次间的最大差值 I (哈希、字符串)

题目&#xff1a;3442. 奇偶频次间的最大差值 I 思路 &#xff1a;哈希&#xff0c;时间复杂度0(n)。 用哈希表来记录每个字符串中字符的分布情况&#xff0c;哈希表这里用数组即可实现。 C版本&#xff1a; class Solution { public:int maxDifference(string s) {int a[26]…

【大模型RAG】拍照搜题技术架构速览:三层管道、两级检索、兜底大模型

摘要 拍照搜题系统采用“三层管道&#xff08;多模态 OCR → 语义检索 → 答案渲染&#xff09;、两级检索&#xff08;倒排 BM25 向量 HNSW&#xff09;并以大语言模型兜底”的整体框架&#xff1a; 多模态 OCR 层 将题目图片经过超分、去噪、倾斜校正后&#xff0c;分别用…

【Axure高保真原型】引导弹窗

今天和大家中分享引导弹窗的原型模板&#xff0c;载入页面后&#xff0c;会显示引导弹窗&#xff0c;适用于引导用户使用页面&#xff0c;点击完成后&#xff0c;会显示下一个引导弹窗&#xff0c;直至最后一个引导弹窗完成后进入首页。具体效果可以点击下方视频观看或打开下方…

接口测试中缓存处理策略

在接口测试中&#xff0c;缓存处理策略是一个关键环节&#xff0c;直接影响测试结果的准确性和可靠性。合理的缓存处理策略能够确保测试环境的一致性&#xff0c;避免因缓存数据导致的测试偏差。以下是接口测试中常见的缓存处理策略及其详细说明&#xff1a; 一、缓存处理的核…

龙虎榜——20250610

上证指数放量收阴线&#xff0c;个股多数下跌&#xff0c;盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型&#xff0c;指数短线有调整的需求&#xff0c;大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的&#xff1a;御银股份、雄帝科技 驱动…

观成科技:隐蔽隧道工具Ligolo-ng加密流量分析

1.工具介绍 Ligolo-ng是一款由go编写的高效隧道工具&#xff0c;该工具基于TUN接口实现其功能&#xff0c;利用反向TCP/TLS连接建立一条隐蔽的通信信道&#xff0c;支持使用Let’s Encrypt自动生成证书。Ligolo-ng的通信隐蔽性体现在其支持多种连接方式&#xff0c;适应复杂网…

铭豹扩展坞 USB转网口 突然无法识别解决方法

当 USB 转网口扩展坞在一台笔记本上无法识别,但在其他电脑上正常工作时,问题通常出在笔记本自身或其与扩展坞的兼容性上。以下是系统化的定位思路和排查步骤,帮助你快速找到故障原因: 背景: 一个M-pard(铭豹)扩展坞的网卡突然无法识别了,扩展出来的三个USB接口正常。…

未来机器人的大脑:如何用神经网络模拟器实现更智能的决策?

编辑&#xff1a;陈萍萍的公主一点人工一点智能 未来机器人的大脑&#xff1a;如何用神经网络模拟器实现更智能的决策&#xff1f;RWM通过双自回归机制有效解决了复合误差、部分可观测性和随机动力学等关键挑战&#xff0c;在不依赖领域特定归纳偏见的条件下实现了卓越的预测准…

Linux应用开发之网络套接字编程(实例篇)

服务端与客户端单连接 服务端代码 #include <sys/socket.h> #include <sys/types.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #include <pthread.h> …

华为云AI开发平台ModelArts

华为云ModelArts&#xff1a;重塑AI开发流程的“智能引擎”与“创新加速器”&#xff01; 在人工智能浪潮席卷全球的2025年&#xff0c;企业拥抱AI的意愿空前高涨&#xff0c;但技术门槛高、流程复杂、资源投入巨大的现实&#xff0c;却让许多创新构想止步于实验室。数据科学家…

深度学习在微纳光子学中的应用

深度学习在微纳光子学中的主要应用方向 深度学习与微纳光子学的结合主要集中在以下几个方向&#xff1a; 逆向设计 通过神经网络快速预测微纳结构的光学响应&#xff0c;替代传统耗时的数值模拟方法。例如设计超表面、光子晶体等结构。 特征提取与优化 从复杂的光学数据中自…