Почему горутины с сетевым вводом-выводом блокируются?

Я использую go 1.1 devel на Ubuntu 13.04.

go version devel +ebe8bca920ad Wed May 15 15:34:47 2013 +1000 linux/386

Согласно http://golang.org/doc/faq#goroutines

Когда сопрограмма блокируется, например, при вызове блокирующего системного вызова, среда выполнения автоматически перемещает другие сопрограммы в том же потоке операционной системы в другой исполняемый поток, чтобы они не были заблокированы.

Я пытаюсь написать загрузчик, который может загружать большие файлы по частям, используя горутины, и это лучшая горутина, которую я придумал:

func download(uri string, chunks chan int, offset int, file *os.file) {
    for current := range chunks {

        fmt.println("downloading range: ", current, "-", current+offset)

        client := &http.client{}
        req, _ := http.newrequest("get", uri, nil)
        req.header.set("range: ", fmt.sprintf("bytes=%d-%d", current, current+offset))
        resp, err := client.do(req)
        if err != nil {
            panic(err)
        }
        defer resp.body.close()
        body, err := ioutil.readall(resp.body)
        if err != nil {
            panic(err)
        }
        file.write(body)
    }
}

Полный сценарий доступен по адресу https://github.com/tuxcanfly/godown/blob/master/godown.go

Несмотря на то, что файлы загружаются и сохраняются правильно, я вижу, что второй фрагмент начинается только тогда, когда заканчивается первый.

Разве загрузки по частям не должны выполняться параллельно, или я что-то делаю не так?


person tuxcanfly    schedule 20.05.2013    source источник


Ответы (1)


У вас есть только одна горутина, загружающая куски.

Строка 64:

go download(*download_url, chunks, offset, file)

То, что вы, вероятно, хотите, это:

for i := 0; i < *threads; i++ {
    go download(*download_url, chunks, offset, file)
}

Это загрузит *threads фрагментов одновременно.


После того, как у вас заработает параллелизм, вы, вероятно, заметите, что строка 29 работает не так, как вы предполагали. Если фрагмент 1 заканчивается раньше фрагмента 2, части будут записаны не по порядку. Вместо этого вы можете использовать http://golang.org/pkg/os/#File.WriteAt .


У вас также есть две проблемы с заголовком Range.

  1. Вы не загружаете остаток. Если размер файла 3002 и у вас 3 потока, он запросит 0-1000, 1000-2000, 2000-3000 и последние 2 байта никогда не будут загружены.
  2. Диапазоны байтов включительно. это означает, что вы (как вы можете видеть в предыдущем примере) загружаете некоторые байты дважды. Байты 1000 и 2000 запрашиваются дважды. Конечно, пока вы пишете в правильные места, у вас не должно быть особых проблем.

Номер два достаточно легко исправить, изменив строку 19 с

req.Header.Set("Range: ", fmt.Sprintf("bytes=%d-%d", current, current+offset))

к этому

req.Header.Set("Range: ", fmt.Sprintf("bytes=%d-%d", current, current+offset-1))

Для получения дополнительной информации о заголовке Range я предлагаю прочитать раздел 14.35 в RFC2616.

person Stephen Weinberg    schedule 20.05.2013
comment
Выше и выше. Отличный ответ. - person sje397; 20.05.2013
comment
Большое спасибо! У меня были некоторые неправильные представления о том, как вызываются горутины, и я все еще учусь... еще раз спасибо! - person tuxcanfly; 20.05.2013