Golang中使用go-exiftool处理大量文件时循环卡住的问题

Golang中使用go-exiftool处理大量文件时循环卡住的问题 大家好,

我正在使用 go-exiftool 循环处理大约一万个文件。我使用一个 go-exiftool 实例来获取所有所需文件的信息。以下代码在循环中被调用了一万次,每次处理的文件都不同。

fileInfos := et.ExtractMetadata(file)

在大约七千次循环后,程序会挂起。我调试了 go-exiftool,发现它在 https://github.com/barasher/go-exiftool/blob/master/exiftool.go#L121 的这行代码处挂起:

fmt.Fprintln(io.WriteCloser, "-execute")

如果我理解正确,io.WriteCloserexec.Command(binary, initArgs...).StdinPipe() 的一个实例。

我尝试在循环中处理同一个文件一万次,运行正常。这看起来像是内存耗尽,有可能吗?我在系统内存图表中没有发现问题。或者是标准输入溢出了。我不知道如何检查这一点。

另一种情况是:如果我忽略最后一个读取失败的文件,程序会在下一个文件处停止。

此外,在调试模式下它运行得非常好。

那么,问题是:

  1. exec.Command 是否有执行次数限制?
  2. 如果问题1的答案是否定的,那么其他可能的原因是什么?
  3. 这取决于文件大小吗?我尝试了另一个文件夹,它在处理了三万五千个文件后正常工作,然后才挂起。如何检查这一点?

更多关于Golang中使用go-exiftool处理大量文件时循环卡住的问题的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

我尝试在macOS上使用lsof的替代品。我不明白应该在那里看到什么。使用它之后,它很快就停止工作了。

更多关于Golang中使用go-exiftool处理大量文件时循环卡住的问题的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


问题似乎确实出在 exiftool 上。由于某种原因它卡住了。我会为此寻找变通方法。感谢提示!

这仅仅是将一个文件名写入 exiftool 的存根,然后它打开该文件,但似乎没有关闭它,你按照我的建议检查过 lsof 的输出吗?

如果我理解正确,这段代码是将文件写入写入器吗?

fmt.Fprintln(io.WriteCloser, filePath)

并且 io.WriteCloser 只有在循环处理完所有一万个文件后才会关闭。

是你读取文件,还是在你告诉另一个工具文件名后,由它来读取?

那个程序会关闭文件吗?

如果你在Linux系统上,可以使用 lsof 来检查程序何时挂起。

在列表中搜索你自己的二进制文件以及你正在使用的外部工具。

这是一个典型的流式处理中生产者-消费者同步问题。go-exiftool 的 ExtractMetadata 方法内部使用了带缓冲的通信,当大量文件快速调用时,如果消费者(exiftool 子进程)处理速度跟不上生产者(Go 主进程)的发送速度,会导致写入阻塞。

以下是问题分析和解决方案:

1. 问题根源分析

查看 go-exiftool 源码,ExtractMetadata 每次调用都会向 exiftool 子进程发送命令并等待响应。在循环中快速调用时:

// 问题代码示例
for _, file := range files {
    fileInfos := et.ExtractMetadata(file) // 每次都会同步等待
    // 处理 fileInfos...
}

问题出在:

  • 每次 ExtractMetadata 都是同步操作
  • exiftool 子进程可能还在处理前一个请求
  • Go 程序尝试写入新命令时,如果缓冲区已满就会阻塞

2. 解决方案:使用批处理模式

go-exiftool 支持批处理模式,这是处理大量文件的最佳方式:

package main

import (
    "fmt"
    "log"
    "github.com/barasher/go-exiftool"
)

func main() {
    // 1. 初始化 exiftool 实例
    et, err := exiftool.NewExiftool()
    if err != nil {
        log.Fatal(err)
    }
    defer et.Close()
    
    // 2. 准备文件列表
    files := []string{
        "file1.jpg",
        "file2.jpg",
        // ... 一万个文件
    }
    
    // 3. 批量提取元数据
    fileInfos := et.ExtractMetadata(files...)
    
    // 4. 处理结果
    for _, fileInfo := range fileInfos {
        if fileInfo.Err != nil {
            fmt.Printf("Error processing %s: %v\n", fileInfo.File, fileInfo.Err)
            continue
        }
        
        // 访问元数据
        for k, v := range fileInfo.Fields {
            fmt.Printf("%s: %s = %v\n", fileInfo.File, k, v)
        }
    }
}

3. 如果必须循环处理,添加并发控制

如果因为内存限制不能一次性处理所有文件,可以使用分批处理:

func processInBatches(et *exiftool.Exiftool, files []string, batchSize int) {
    for i := 0; i < len(files); i += batchSize {
        end := i + batchSize
        if end > len(files) {
            end = len(files)
        }
        
        batch := files[i:end]
        fileInfos := et.ExtractMetadata(batch...)
        
        // 处理当前批次的结果
        for _, info := range fileInfos {
            // 处理每个文件的元数据
            _ = info
        }
        
        // 可选:给 exiftool 一些喘息时间
        // time.Sleep(10 * time.Millisecond)
    }
}

4. 检查系统限制

虽然 exec.Command 本身没有执行次数限制,但系统资源有限制:

// 检查当前进程的文件描述符限制
import "syscall"

func checkLimits() {
    var rlimit syscall.Rlimit
    err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlimit)
    if err == nil {
        fmt.Printf("File descriptor limits: Current=%d, Max=%d\n", 
            rlimit.Cur, rlimit.Max)
    }
}

5. 监控内存使用

添加内存监控来排除内存泄漏:

import (
    "runtime"
    "time"
)

func printMemUsage() {
    var m runtime.MemStats
    runtime.ReadMemStats(&m)
    fmt.Printf("Alloc = %v MiB", m.Alloc/1024/1024)
    fmt.Printf("\tTotalAlloc = %v MiB", m.TotalAlloc/1024/1024)
    fmt.Printf("\tSys = %v MiB", m.Sys/1024/1024)
    fmt.Printf("\tNumGC = %v\n", m.NumGC)
}

// 在循环中定期调用
func processWithMonitoring(files []string) {
    et, _ := exiftool.NewExiftool()
    defer et.Close()
    
    for i, file := range files {
        if i%1000 == 0 {
            printMemUsage()
        }
        
        fileInfos := et.ExtractMetadata(file)
        // 处理结果...
    }
}

6. 使用带超时的处理

为每个文件处理添加超时控制:

import "context"
import "time"

func processWithTimeout(et *exiftool.Exiftool, file string) (*exiftool.FileMetadata, error) {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
    
    done := make(chan struct{})
    var result *exiftool.FileMetadata
    var err error
    
    go func() {
        infos := et.ExtractMetadata(file)
        if len(infos) > 0 {
            result = &infos[0]
        }
        close(done)
    }()
    
    select {
    case <-done:
        return result, err
    case <-ctx.Done():
        return nil, ctx.Err()
    }
}

总结建议

  1. 首选批处理模式:使用 ExtractMetadata 的批量接口,这是最高效的方式
  2. 分批处理:如果内存不足,将一万个文件分成多个批次(如每批1000个)
  3. 监控资源:添加内存和文件描述符监控
  4. 错误处理:确保正确处理每个文件的错误,避免错误累积

问题不是 exec.Command 的执行次数限制,而是同步调用导致的缓冲区阻塞。批处理模式能显著提升性能并避免阻塞问题。

回到顶部