2012-09-27 9 views
6

Ich versuchte, die Go-Tour exercise #71golang: goroute mit select nicht aufhören, wenn ich einen fmt.Print hinzugefügt()

Wenn es wie go run 71_hang.go ok ausgeführt wird, es funktioniert gut.

Wenn Sie jedoch go run 71_hang.go nogood verwenden, wird es für immer ausgeführt. Der einzige Unterschied ist der zusätzliche fmt.Print("") in der default in der select Anweisung

Ich bin mir nicht sicher, aber ich vermute eine Art Endlosschleife und Race-Condition? Und hier ist meine Lösung.

Hinweis: Es ist nicht Deadlock als Go throw: all goroutines are asleep - deadlock!

package main 

import (
    "fmt" 
    "os" 
) 

type Fetcher interface { 
    // Fetch returns the body of URL and 
    // a slice of URLs found on that page. 
    Fetch(url string) (body string, urls []string, err error) 
} 

func crawl(todo Todo, fetcher Fetcher, 
    todoList chan Todo, done chan bool) { 
    body, urls, err := fetcher.Fetch(todo.url) 
    if err != nil { 
     fmt.Println(err) 
    } else { 
     fmt.Printf("found: %s %q\n", todo.url, body) 
     for _, u := range urls { 
      todoList <- Todo{u, todo.depth - 1} 
     } 
    } 
    done <- true 
    return 
} 

type Todo struct { 
    url string 
    depth int 
} 

// Crawl uses fetcher to recursively crawl 
// pages starting with url, to a maximum of depth. 
func Crawl(url string, depth int, fetcher Fetcher) { 
    visited := make(map[string]bool) 
    doneCrawling := make(chan bool, 100) 
    toDoList := make(chan Todo, 100) 
    toDoList <- Todo{url, depth} 

    crawling := 0 
    for { 
     select { 
     case todo := <-toDoList: 
      if todo.depth > 0 && !visited[todo.url] { 
       crawling++ 
       visited[todo.url] = true 
       go crawl(todo, fetcher, toDoList, doneCrawling) 
      } 
     case <-doneCrawling: 
      crawling-- 
     default: 
      if os.Args[1]=="ok" { // * 
       fmt.Print("") 
      } 
      if crawling == 0 { 
       goto END 
      } 
     } 
    } 
END: 
    return 
} 

func main() { 
    Crawl("http://golang.org/", 4, fetcher) 
} 

// fakeFetcher is Fetcher that returns canned results. 
type fakeFetcher map[string]*fakeResult 

type fakeResult struct { 
    body string 
    urls []string 
} 

func (f *fakeFetcher) Fetch(url string) (string, []string, error) { 
    if res, ok := (*f)[url]; ok { 
     return res.body, res.urls, nil 
    } 
    return "", nil, fmt.Errorf("not found: %s", url) 
} 

// fetcher is a populated fakeFetcher. 
var fetcher = &fakeFetcher{ 
    "http://golang.org/": &fakeResult{ 
     "The Go Programming Language", 
     []string{ 
      "http://golang.org/pkg/", 
      "http://golang.org/cmd/", 
     }, 
    }, 
    "http://golang.org/pkg/": &fakeResult{ 
     "Packages", 
     []string{ 
      "http://golang.org/", 
      "http://golang.org/cmd/", 
      "http://golang.org/pkg/fmt/", 
      "http://golang.org/pkg/os/", 
     }, 
    }, 
    "http://golang.org/pkg/fmt/": &fakeResult{ 
     "Package fmt", 
     []string{ 
      "http://golang.org/", 
      "http://golang.org/pkg/", 
     }, 
    }, 
    "http://golang.org/pkg/os/": &fakeResult{ 
     "Package os", 
     []string{ 
      "http://golang.org/", 
      "http://golang.org/pkg/", 
     }, 
    }, 
} 

Antwort

15

Einlochen default Anweisung in select ändert die Art und Weise ausgewählte Werke nicht. Ohne eine Standardanweisung blockiert die Auswahl das Warten auf Nachrichten auf den Kanälen. Mit einer Standardanweisung wird die Standardanweisung jedes Mal ausgeführt, wenn nichts aus den Kanälen gelesen werden kann. In deinem Code finde ich das eine Endlosschleife. Wenn Sie die Anweisung fmt.Print eingeben, kann der Scheduler andere Gloutines planen.

Wenn Sie Ihren Code so ändern, dann funktioniert es richtig, indem Sie auf eine nicht blockierende Weise auswählen, die den anderen goroutines erlaubt, richtig zu laufen.

for { 
     select { 
     case todo := <-toDoList: 
      if todo.depth > 0 && !visited[todo.url] { 
       crawling++ 
       visited[todo.url] = true 
       go crawl(todo, fetcher, toDoList, doneCrawling) 
      } 
     case <-doneCrawling: 
      crawling-- 
     } 
     if crawling == 0 { 
      break 
     } 
    } 

Sie können Ihren ursprünglichen Code funktionieren, wenn Sie GOMAXPROCS verwenden = 2, was ein weiterer Hinweis ist, dass der Planer in einer Endlos-Schleife besetzt ist.

Beachten Sie, dass goroutines kooperativ geplant werden. Was ich nicht vollständig über dein Problem verstehe, ist, dass select ein Punkt ist, wo die Goroutine ergeben sollte - ich hoffe, dass jemand anderes erklären kann, warum es in deinem Beispiel nicht ist.

+1

wählen nicht nachgibt * weil * der Standard-Aussage .Obwohl ich nicht sicher bin, ob du das nicht vollständig verstehst, weil du die "Standard" - und GOMAXPROCS-Erklärungen genagelt hast. – mna

+1

Genau das habe ich nicht gewusst, danke! –

+0

"Auswahl liefert wegen der Standardaussage nicht." ist was ich nicht weiß. Vielen Dank. – Sungam

5

Sie haben 100% CPU-Last, weil fast immer der Standardfall ausgeführt wird, was effektiv zu einer Endlosschleife führt, weil sie immer wieder ausgeführt wird. In dieser Situation übergibt der Go-Scheduler die Steuerung konstruktionsbedingt nicht an eine andere Goroutine. Jede andere Gorroutine hat also nie die Möglichkeit, crawling != 0 einzustellen, und Sie haben Ihre Endlosschleife.

Meiner Meinung nach sollten Sie den Standardfall entfernen und stattdessen einen anderen Kanal erstellen, wenn Sie mit der Select-Anweisung spielen möchten.

Ansonsten ist das runtime Paket hilft Ihnen, den schmutzigen Weg zu gehen:

  • runtime.GOMAXPROCS(2) arbeiten (oder Export GOMAXPROCS = 2), auf diese Weise mehr als eine OS Ausführungsthread
  • Aufruf haben runtime.Gosched() Crawl von Zeit zu Zeit. Obwohl die CPU-Auslastung 100% beträgt, wird die Steuerung die Steuerung an eine andere Goroutine übergeben.

Edit: Ja, und der Grund, warum fmt.Printf einen Unterschied macht: weil es explizit geht die Steuerung zu einem gewissen syscall Sachen ...;)

+0

"In diesem Fall wird die Steuerung vom Go-Scheduler nicht konstruktionsbedingt an eine andere Goroutine übergeben." Nicht ganz richtig. Der Go 1.0 (?) Scheduler tut dies, aber es ist ein unvollkommener Scheduler. Die Workarounds, die Sie aufgelistet haben (oder einen Syscall durch Aufruf von 'fmt.Println()') ausführen, wecken den Scheduler. Details zu diesen Verbesserungen in Go 1.2 finden Sie unter http://golang.org/doc/go1.2#preemption. – ayke