Як чекати, коли всі goututines закінчать, не використовуючи час. Сплячий?


108

Цей код вибирає всі файли xml у тій самій папці, оскільки викликаний виконуваний файл і асинхронно застосовує обробку до кожного результату методом зворотного виклику (у прикладі нижче виводиться лише ім'я файлу).

Як я можу не використовувати метод сну, щоб уникнути виходу основного методу? У мене проблеми з обгортанням голови навколо каналів (я вважаю, що це потрібно для синхронізації результатів), тому будь-яка допомога вдячна!

package main

import (
    "fmt"
    "io/ioutil"
    "path"
    "path/filepath"
    "os"
    "runtime"
    "time"
)

func eachFile(extension string, callback func(file string)) {
    exeDir := filepath.Dir(os.Args[0])
    files, _ := ioutil.ReadDir(exeDir)
    for _, f := range files {
            fileName := f.Name()
            if extension == path.Ext(fileName) {
                go callback(fileName)
            }
    }
}


func main() {
    maxProcs := runtime.NumCPU()
    runtime.GOMAXPROCS(maxProcs)

    eachFile(".xml", func(fileName string) {
                // Custom logic goes in here
                fmt.Println(fileName)
            })

    // This is what i want to get rid of
    time.Sleep(100 * time.Millisecond)
}

Відповіді:


173

Ви можете використовувати sync.WaitGroup . Цитуючи зв'язаний приклад:

package main

import (
        "net/http"
        "sync"
)

func main() {
        var wg sync.WaitGroup
        var urls = []string{
                "http://www.golang.org/",
                "http://www.google.com/",
                "http://www.somestupidname.com/",
        }
        for _, url := range urls {
                // Increment the WaitGroup counter.
                wg.Add(1)
                // Launch a goroutine to fetch the URL.
                go func(url string) {
                        // Decrement the counter when the goroutine completes.
                        defer wg.Done()
                        // Fetch the URL.
                        http.Get(url)
                }(url)
        }
        // Wait for all HTTP fetches to complete.
        wg.Wait()
}

11
Будь-яка причина, яку вам доведеться робити wg.Add (1) поза рутиною руху? Чи можемо ми це зробити всередині відкладеного wg.Done ()?
сів

18
сб, так, є причина, це описано в sync.WaitGroup.Add документи: Note that calls with positive delta must happen before the call to Wait, or else Wait may wait for too small a group. Typically this means the calls to Add should execute before the statement creating the goroutine or other event to be waited for. See the WaitGroup example.
wobmene

15
Адаптація цього коду викликала у мене тривалий налагоджувальний сеанс, оскільки моя програма була названою функцією, і передача в WaitGroup як значення скопіює її та зробить wg.Done () неефективною. Хоча це може бути виправлено шляхом передачі покажчика & wg, кращий спосіб запобігти таким помилкам - оголосити змінну WaitGroup в якості вказівника в першу чергу: wg := new(sync.WaitGroup)замість var wg sync.WaitGroup.
Роберт Джек Уілл

Я думаю, що правильно писати wg.Add(len(urls))трохи вище рядка for _, url := range urls, я вважаю, що це краще, оскільки ви використовуєте додавання лише один раз.
Віктор

@RobertJackWill: Примітка! До речі, це описано в документах : "Група WaitGroup не повинна бути скопійована після першого використання. Шкода, що Go Go не має способу примусово виконувати це . Насправді, однак, go vetвиявляє цей випадок і попереджає" функція передає замок за значенням : sync.WaitGroup містить sync.noCopy ".
Brent Bradburn,

56

WaitGroups - це, безумовно, канонічний спосіб зробити це. Однак задля повноти, ось ось рішення, яке зазвичай використовувалося до введення WaitGroups. Основна ідея полягає у використанні каналу, щоб сказати "Я закінчую", а головна програма повинна чекати, поки кожен породжений розпорядок не повідомить про його завершення.

func main() {
    c := make(chan struct{}) // We don't need any data to be passed, so use an empty struct
    for i := 0; i < 100; i++ {
        go func() {
            doSomething()
            c <- struct{}{} // signal that the routine has completed
        }()
    }

    // Since we spawned 100 routines, receive 100 messages.
    for i := 0; i < 100; i++ {
        <- c
    }
}

9
Приємно бачити рішення простими каналами. Додатковий бонус: якщо ви doSomething()повернете деякий результат, ви можете поставити це на канал, і ви можете збирати та обробляти результати у другому циклі (як тільки вони будуть готові)
andras

4
Це працює лише в тому випадку, якщо ви вже знаєте кількість горітин, яку ви хотіли б почати. Що робити, якщо ви пишете який-небудь html-сканер і починаєте горутини рекурсивно для кожного посилання на сторінці?
shinydev

Вам потрібно буде якось відстежувати це незалежно. З WaitGroups це трохи простіше, тому що кожного разу, коли ви створюєте нову goroutter, ви можете спочатку зробити це, wg.Add(1)і, таким чином, він буде відслідковувати їх. З каналами було б дещо складніше.
joshlf

c буде заблоковано, оскільки всі підпрограми переходу спробують отримати доступ до нього, і він нерозкритий
Edwin Ikechukwu Okonkwo

Якщо під "блокуванням" ви маєте на увазі, що програма зайде в тупик, це неправда. Ви можете спробувати запустити його самостійно. Причина полягає в тому, що єдині goututines, які записують c, відрізняються від головної програми, з якої читається c. Таким чином, головна програма завжди доступна для зчитування значення каналу, що відбудеться, коли одна з функцій буде доступна для запису значення на канал. Ви маєте рацію, що якби цей код не породив гороутини, а замість цього запустив усе в одній програмі, він зайшов у тупик.
joshlf

8

sync.WaitGroup може допомогти вам тут.

package main

import (
    "fmt"
    "sync"
    "time"
)


func wait(seconds int, wg * sync.WaitGroup) {
    defer wg.Done()

    time.Sleep(time.Duration(seconds) * time.Second)
    fmt.Println("Slept ", seconds, " seconds ..")
}


func main() {
    var wg sync.WaitGroup

    for i := 0; i <= 5; i++ {
        wg.Add(1)   
        go wait(i, &wg)
    }
    wg.Wait()
}

1

Хоча sync.waitGroup(wg) - це канонічний шлях вперед, він вимагає, щоб ви виконали принаймні деякі ваші wg.Addдзвінки перед вами, wg.Waitщоб усі завершили. Це може бути нездійсненним для таких простих речей, як веб-сканер, де ви не знаєте попередньо кількості рекурсивних дзвінків, і для отримання даних, що керують wg.Addдзвінками, потрібен певний час . Зрештою, вам потрібно завантажити та проаналізувати першу сторінку, перш ніж дізнатися розмір першої партії дочірніх сторінок.

Я написав рішення за допомогою каналів, уникаючи waitGroupв своєму рішенні вправ на веб-сканерах Tour of Go . Кожен раз, коли запускається одна або декілька рутинних процедур, ви надсилаєте номер на childrenканал. Кожен раз , коли йти рутина збирається завершена, ви відправити 1на doneканал. Коли сума дітей дорівнює сумі зробленого, ми готові.

Єдине, що мені залишається, - це жорсткий розмір resultsканалу, але це обмеження (поточне).


// recursionController is a data structure with three channels to control our Crawl recursion.
// Tried to use sync.waitGroup in a previous version, but I was unhappy with the mandatory sleep.
// The idea is to have three channels, counting the outstanding calls (children), completed calls 
// (done) and results (results).  Once outstanding calls == completed calls we are done (if you are
// sufficiently careful to signal any new children before closing your current one, as you may be the last one).
//
type recursionController struct {
    results  chan string
    children chan int
    done     chan int
}

// instead of instantiating one instance, as we did above, use a more idiomatic Go solution
func NewRecursionController() recursionController {
    // we buffer results to 1000, so we cannot crawl more pages than that.  
    return recursionController{make(chan string, 1000), make(chan int), make(chan int)}
}

// recursionController.Add: convenience function to add children to controller (similar to waitGroup)
func (rc recursionController) Add(children int) {
    rc.children <- children
}

// recursionController.Done: convenience function to remove a child from controller (similar to waitGroup)
func (rc recursionController) Done() {
    rc.done <- 1
}

// recursionController.Wait will wait until all children are done
func (rc recursionController) Wait() {
    fmt.Println("Controller waiting...")
    var children, done int
    for {
        select {
        case childrenDelta := <-rc.children:
            children += childrenDelta
            // fmt.Printf("children found %v total %v\n", childrenDelta, children)
        case <-rc.done:
            done += 1
            // fmt.Println("done found", done)
        default:
            if done > 0 && children == done {
                fmt.Printf("Controller exiting, done = %v, children =  %v\n", done, children)
                close(rc.results)
                return
            }
        }
    }
}

Повний вихідний код рішення


1

Ось рішення, в якому використовується WaitGroup.

Спочатку визначте два корисні методи:

package util

import (
    "sync"
)

var allNodesWaitGroup sync.WaitGroup

func GoNode(f func()) {
    allNodesWaitGroup.Add(1)
    go func() {
        defer allNodesWaitGroup.Done()
        f()
    }()
}

func WaitForAllNodes() {
    allNodesWaitGroup.Wait()
}

Потім замініть виклик callback:

go callback(fileName)

Зателефонувавши до вашої функції утиліти:

util.GoNode(func() { callback(fileName) })

Останній крок, додайте цей рядок в кінці свого main, замість вашого sleep. Це дозволить переконатися, що основний потік очікує завершення всіх процедур, перш ніж програма може зупинитися.

func main() {
  // ...
  util.WaitForAllNodes()
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.