From 0aa9d4402548c1bdda2675daef5796bd9fc0f0a0 Mon Sep 17 00:00:00 2001 From: Odboy Date: Wed, 28 May 2025 21:16:15 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E5=B9=B6=E5=8F=91worker=E6=95=B0?= =?UTF-8?q?=E5=8F=AF=E9=85=8D=E7=BD=AE(=E9=BB=98=E8=AE=A44)=E4=B8=8EMIME?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E5=B9=B6=E8=A1=8C=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application.properties | 2 ++ internal/config/config.go | 28 ++++++++++++++++++++++++++- internal/mover/mover.go | 14 +++++--------- internal/organize/organize.go | 12 ++++-------- internal/scanner/scanner.go | 36 ++++++++++++++++++++++++++++++----- 5 files changed, 69 insertions(+), 23 deletions(-) diff --git a/application.properties b/application.properties index 93ac435..159baa0 100644 --- a/application.properties +++ b/application.properties @@ -9,3 +9,5 @@ database.dsn=media-info.db ffmpeg.path= # 整理时是否自动转正带 EXIF 旋转标记的图片(true=是,默认;false=只按显示方向归类不转正) organize.rotateImages=true +# 探测/认类型/移动阶段的并发 worker 数(默认 4;0 表示用满全部 CPU 核数) +organize.workers=4 diff --git a/internal/config/config.go b/internal/config/config.go index 7670cc3..505c74e 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -5,6 +5,10 @@ package config import ( + "runtime" + "strconv" + "strings" + "github.com/magiconair/properties" ) @@ -14,12 +18,14 @@ import ( // - AutoOpen:启动后要不要自动打开浏览器 // - FFmpegPath:ffmpeg 程序路径(用于把带 EXIF 旋转标记的图片转正);留空自动在 PATH 里找 // - RotateImages:整理时是否自动转正带 EXIF 旋转标记的图片(默认 true) +// - Workers:探测/MIME/移动阶段的并发 worker 数(默认 4;设 0 表示用满全部 CPU 核数) type Config struct { Port int Dsn string AutoOpen bool FFmpegPath string RotateImages bool + Workers int } // appConfig 当前生效的配置(程序运行期间一直不变)。 @@ -33,7 +39,7 @@ func GetAppConfig() Config { // LoadAppConfig 从 application.properties 读配置;文件不存在就用默认值。 func LoadAppConfig() { // 先给一套默认值 - appConfig = Config{Port: 8080, Dsn: "media-info.db", AutoOpen: true, RotateImages: true} + appConfig = Config{Port: 8080, Dsn: "media-info.db", AutoOpen: true, RotateImages: true, Workers: 4} p, err := properties.LoadFile("application.properties", properties.UTF8) if err != nil { @@ -52,4 +58,24 @@ func LoadAppConfig() { appConfig.FFmpegPath = v } appConfig.RotateImages = p.GetBool("organize.rotateImages", true) + // 并发 worker 数:只认非负整数,填了别的就保持默认 + if v, ok := p.Get("organize.workers"); ok && strings.TrimSpace(v) != "" { + if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && n >= 0 { + appConfig.Workers = n + } + } +} + +// WorkerCount 返回探测/MIME/移动阶段使用的并发 worker 数: +// - 配置里 organize.workers > 0 时用配置值(默认 4); +// - 配置为 0 时用满全部 CPU 核数。 +func WorkerCount() int { + if appConfig.Workers > 0 { + return appConfig.Workers + } + n := runtime.NumCPU() + if n < 1 { + return 1 + } + return n } diff --git a/internal/mover/mover.go b/internal/mover/mover.go index 4dcbf98..17d5706 100644 --- a/internal/mover/mover.go +++ b/internal/mover/mover.go @@ -13,11 +13,12 @@ import ( "io" "os" "path/filepath" - "runtime" "strings" "sync" "sync/atomic" + "kenaito-media-picker/internal/config" + uuid "github.com/satori/go.uuid" ) @@ -30,15 +31,10 @@ type MoveJob struct { // Mover 文件移动器。目前不需要记录任何状态,所以是个空壳结构体。 type Mover struct{} -// workerCount 并行搬文件的工人数:直接用满 CPU 核心数。 -// 你的机器够强、平时又没别的活,那就让它全速跑,这就是空闲时的极限值。 -// (如果搬文件的同时机器还在干别的活、被拖慢了,再手动把这个数调低即可。) +// workerCount 并行搬文件的工人数:默认 4 个(配置 organize.workers 可调,0 表示用满 CPU)。 +// 搬文件时磁盘 IO 才是瓶颈,并发太大反而把 CPU/磁盘都拖垮,给个配置让用户自行权衡。 func workerCount() int { - n := runtime.NumCPU() - if n < 1 { - return 1 - } - return n + return config.WorkerCount() } // MoveAll 把一批"搬文件任务"全局并行搬完——不管任务分散在几个目录, diff --git a/internal/organize/organize.go b/internal/organize/organize.go index 6188679..25b8f85 100644 --- a/internal/organize/organize.go +++ b/internal/organize/organize.go @@ -14,12 +14,12 @@ import ( "fmt" "os" "path/filepath" - "runtime" "strings" "sync" "sync/atomic" "kenaito-media-picker/internal/classify" + "kenaito-media-picker/internal/config" "kenaito-media-picker/internal/digest" "kenaito-media-picker/internal/mover" "kenaito-media-picker/internal/probe" @@ -93,14 +93,10 @@ func (o *Organizer) report(phase string, done, total int) { // ============================ 并行小工具 ============================ -// workerCount 并行探测的工人数:直接用满 CPU 核心数(空闲时的极限值)。 -// 多核机器并行探测能明显提速;如果机器同时还在跑别的重活,再手动调低即可。 +// workerCount 并行探测的工人数:默认 4 个(配置 organize.workers 可调,0 表示用满 CPU)。 +// 多核机器并行探测能明显提速;但并发太大会吃满 CPU,给个配置让用户自行权衡。 func workerCount() int { - n := runtime.NumCPU() - if n < 1 { - return 1 - } - return n + return config.WorkerCount() } // runParallel 用固定数量的工人并行处理一批东西,结果按原来的顺序放回。 diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index b78f520..d9c5cf9 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -11,8 +11,11 @@ import ( "os" "path/filepath" "strings" + "sync" + "sync/atomic" "time" + "kenaito-media-picker/internal/config" "kenaito-media-picker/internal/probe" "github.com/gabriel-vasile/mimetype" @@ -106,12 +109,35 @@ func Scan(opts Options) (*Result, error) { if opts.OnProgress != nil { opts.OnProgress(total, total) } - for i := range res.Files { - res.Files[i].Mime = detectMime(res.Files[i].Path) - if opts.OnProgress != nil { - opts.OnProgress(i+1, total) - } + // 并行认类型:每个 worker 只写自己名下那个元素(res.Files[i]),并发安全; + // 认类型要读文件内容,数量多时串行太慢,并行能明显提速。 + workers := config.WorkerCount() + if workers > total { + workers = total } + if workers < 1 { + workers = 1 + } + var wg sync.WaitGroup + var done int32 + jobs := make(chan int) + for w := 0; w < workers; w++ { + wg.Add(1) + go func() { + defer wg.Done() + for i := range jobs { + res.Files[i].Mime = detectMime(res.Files[i].Path) + if opts.OnProgress != nil { + opts.OnProgress(int(atomic.AddInt32(&done, 1)), total) + } + } + }() + } + for i := range res.Files { + jobs <- i + } + close(jobs) + wg.Wait() return res, nil }