Як чекати, коли всі 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.