将工作分配到切片但限制工人数量

时间:2017-11-21 19:32:34

标签: go

我正在尝试提高应用的性能。 其代码的一部分将文件以块的形式上传到服务器。

原始版本只是在顺序循环中执行此操作。但是,它很慢并且在序列期间它还需要在上传每个块之前与另一个服务器通信。

上传块可以简单地放在goroutine中。它可以工作,但不是一个好的解决方案,因为如果源文件非常大,它最终会占用大量内存。

因此,我尝试使用缓冲通道来限制活动goroutine的数量。这是一些显示我的尝试的代码。我已将其剥离以显示概念,您可以运行它来测试自己。

package main

import (
    "fmt"
    "io"
    "os"
    "time"
)

const defaultChunkSize = 1 * 1024 * 1024

// Lets have 4 workers
var c = make(chan int, 4)

func UploadFile(f *os.File) error {
    fi, err := f.Stat()
    if err != nil {
        return fmt.Errorf("err: %s", err)
    }
    size := fi.Size()

    total := (int)(size/defaultChunkSize + 1)
    // Upload parts
    buf := make([]byte, defaultChunkSize)
    for partno := 1; partno <= total; partno++ {
        readChunk := func(offset int, buf []byte) (int, error) {
            fmt.Println("readChunk", partno, offset)
            n, err := f.ReadAt(buf, int64(offset))
            if err != nil {
                return n, err
            }

            return n, nil
        }

        // This will block if there are not enough worker slots available
        c <- partno

        // The actual worker.
        go func() {
            offset := (partno - 1) * defaultChunkSize
            n, err := readChunk(offset, buf)
            if err != nil && err != io.EOF {
                return
            }

            err = uploadPart(partno, buf[:n])
            if err != nil {
                fmt.Println("Uploadpart failed:", err)
            }
            <-c
        }()
    }

    return nil
}

func uploadPart(partno int, buf []byte) error {
    fmt.Printf("Uploading partno: %d, buflen=%d\n", partno, len(buf))
    // Actually upload the part.  Lets test it by instead writing each
    // buffer to another file.  We can then use diff to compare the 
    // source and dest files.

    // Open file.  Seek to (partno - 1) * defaultChunkSize, write buffer
    f, err := os.OpenFile("/home/matthewh/Downloads/out.tar.gz", os.O_CREATE|os.O_WRONLY, 0755)
    if err != nil {
        fmt.Printf("err: %s\n", err)
    }

    n, err := f.WriteAt(buf, int64((partno-1)*defaultChunkSize))
    if err != nil {
        fmt.Printf("err=%s\n", err)
    }
    fmt.Printf("%d bytes written\n", n)
    defer f.Close()
    return nil
}

func main() {
    filename := "/home/matthewh/Downloads/largefile.tar.gz"
    fmt.Printf("Opening file: %s\n", filename)

    f, err := os.Open(filename)
    if err != nil {
        panic(err)
    }

    UploadFile(f)
}

它几乎可以工作。但是有几个问题。 1)最终部分22发生3次。正确的长度实际为612545,因为文件长度不是1MB的倍数。

// Sample output
...
readChunk 21 20971520
readChunk 22 22020096
Uploading partno: 22, buflen=1048576
Uploading partno: 22, buflen=612545
Uploading partno: 22, buflen=1048576

另一个问题,上传可能会失败,我不熟悉go以及如何最好地解决goroutine的失败。

最后,我想通常在uploadPart成功时返回一些数据。具体来说,它将是一个字符串(HTTP ETag标头值)。这些etag值需要由主函数收集。

在这个实例中构建此代码的更好方法是什么?我还没有找到一个好的golang设计模式,正确满足了我的需求。

3 个答案:

答案 0 :(得分:0)

暂时跳过如何更好地构造此代码的问题,我看到代码中的错误可能会导致您遇到的问题。由于您在goroutine中运行的函数使用变量partno,该变量随着循环的每次迭代而变化,因此在调用goroutine时,goroutine不一定会看到partno的值。解决此问题的常用方法是在循环内创建该变量的本地副本:

for partno := 1; partno <= total; partno++ {
    partno := partno
    // ...
}

答案 1 :(得分:0)

数据竞赛#1

多个goroutines同时使用相同的缓冲区。请注意,一个gorouting可能正在用新块填充它,而另一个goroutine仍在从中读取旧块。相反,每个partno应该拥有它自己的缓冲区。

数据竞赛#2

正如Andy Schweig指出的那样,goroutine中的值在循环中创建的partno有机会读取之前由循环更新。这就是最终partno 22多次出现的原因。要修复它,您可以将goroutine作为参数传递给匿名函数。这将确保每个{{1}}拥有自己的部件号。

此外,您可以使用频道传递工作人员的结果。也许是带有零件号和错误的结构类型。这样,您将能够观察进度并重试失败的上传。

有关良好模式的示例,请从GOPL书中查看此example

答案 2 :(得分:0)

建议的更改

正如dev.bmax buf所指出的那样,正如Andy Schweig partno所指出的那样是一个常规功能的参数,自WaitGroup退出以来也添加了UploadFile在上传完成之前。另外defer f.Close()档案,好习惯。

package main

import (
    "fmt"
    "io"
    "os"
    "sync"
    "time"
)

const defaultChunkSize = 1 * 1024 * 1024

// wg for uploads to complete
var wg sync.WaitGroup

// Lets have 4 workers
var c = make(chan int, 4)

func UploadFile(f *os.File) error {
    // wait for all the uploads to complete before function exit
    defer wg.Wait()

    fi, err := f.Stat()
    if err != nil {
        return fmt.Errorf("err: %s", err)
    }
    size := fi.Size()
    fmt.Printf("file size: %v\n", size)

    total := int(size/defaultChunkSize + 1)
    // Upload parts
    for partno := 1; partno <= total; partno++ {

        readChunk := func(offset int, buf []byte, partno int) (int, error) {
            fmt.Println("readChunk", partno, offset)
            n, err := f.ReadAt(buf, int64(offset))
            if err != nil {
                return n, err
            }

            return n, nil
        }

        // This will block if there are not enough worker slots available
        c <- partno

        // The actual worker.
        go func(partno int) {
            // wait for me to be done
            wg.Add(1)
            defer wg.Done()

            buf := make([]byte, defaultChunkSize)

            offset := (partno - 1) * defaultChunkSize
            n, err := readChunk(offset, buf, partno)
            if err != nil && err != io.EOF {
                return
            }

            err = uploadPart(partno, buf[:n])
            if err != nil {
                fmt.Println("Uploadpart failed:", err)
            }
            <-c
        }(partno)
    }

    return nil
}

func uploadPart(partno int, buf []byte) error {
    fmt.Printf("Uploading partno: %d, buflen=%d\n", partno, len(buf))

    // Actually do the upload.  Simulate long running task with a sleep
    time.Sleep(time.Second)
    return nil
}

func main() {
    filename := "/home/matthewh/Downloads/largefile.tar.gz"
    fmt.Printf("Opening file: %s\n", filename)

    f, err := os.Open(filename)
    if err != nil {
        panic(err)
    }
    defer f.Close()

    UploadFile(f)
}

我确信你可以对buf情况稍微聪明一些。我只是放手去处理垃圾。由于您将工作人员限制为特定数字4,因此您只需要4 x defaultChunkSize个缓冲区。如果你想出一些简单而又简单的东西,请分享。

玩得开心!