2012-09-27 22 views
6

Tôi đã thử các Go Tour exercise #71golang: goroute với lựa chọn không chỉ dừng lại trừ khi tôi thêm một fmt.Print()

Nếu nó được chạy như go run 71_hang.go ok, nó hoạt động tốt.

Tuy nhiên, nếu bạn sử dụng go run 71_hang.go nogood, nó sẽ chạy vĩnh viễn.

Sự khác biệt duy nhất là số fmt.Print("") thêm trong số default trong tuyên bố select.

Tôi không chắc chắn, nhưng tôi nghi ngờ một số loại vòng lặp vô hạn và điều kiện chủng tộc? Và đây là giải pháp của tôi.

Lưu ý: Nó không bế tắc như Go không 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/", 
     }, 
    }, 
} 

Trả lời

15

Đưa một tuyên bố default trong select của bạn thay đổi cách thức chọn các tác phẩm. Nếu không có một tuyên bố mặc định chọn sẽ chặn chờ đợi cho bất kỳ tin nhắn trên các kênh. Với một tuyên bố mặc định chọn sẽ chạy các tuyên bố mặc định mỗi khi không có gì để đọc từ các kênh. Trong mã của bạn, tôi nghĩ rằng điều này làm cho một vòng lặp vô hạn. Đặt câu lệnh fmt.Print vào là cho phép trình lập lịch biểu lên lịch các goroutine khác.

Nếu bạn thay đổi mã của bạn như thế này thì nó hoạt động bình thường, sử dụng chọn theo cách không chặn cho phép các goroutines khác chạy đúng cách.

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 
     } 
    } 

Bạn có thể làm cho mã ban đầu của bạn hoạt động nếu bạn sử dụng GOMAXPROCS = 2, đây là gợi ý khác mà trình lên lịch bận rộn trong vòng lặp vô hạn.

Lưu ý rằng các goroutines được lập lịch hợp tác. Những gì tôi không hoàn toàn hiểu về vấn đề của bạn là select là một điểm mà goroutine sẽ mang lại - Tôi hy vọng một người khác có thể giải thích lý do tại sao nó không có trong ví dụ của bạn.

+1

chọn không mang lại * vì * của báo cáo kết quả mặc định .Mặc dù tôi không chắc đó có phải là điều bạn không hiểu hoàn toàn hay không, bởi vì bạn đã đóng đinh các giải thích "mặc định" và GOMAXPROCS. – mna

+1

Đó là chính xác những gì tôi không biết, cảm ơn! –

+0

"chọn không mang lại do tuyên bố mặc định". là những gì tôi không biết. Cảm ơn. – Sungam

5

Bạn có tải CPU 100% vì gần như mọi lần trường hợp mặc định sẽ được thực hiện, dẫn đến hiệu quả trong một vòng lặp vô hạn vì nó được thực hiện lặp đi lặp lại. Trong trường hợp này, trình lập lịch biểu Go không kiểm soát được một goroutine khác, theo thiết kế. Vì vậy, bất kỳ goroutine khác sẽ không bao giờ có cơ hội để thiết lập crawling != 0 và bạn có vòng lặp vô hạn của bạn.

Theo ý kiến ​​của tôi, bạn nên xóa trường hợp mặc định và thay vào đó tạo một kênh khác nếu bạn muốn phát bằng câu lệnh chọn.

Nếu gói runtime giúp bạn đi theo con đường bẩn:

  • runtime.GOMAXPROCS(2) sẽ làm việc (hoặc xuất khẩu GOMAXPROCS = 2), bằng cách này bạn sẽ có nhiều hơn một thread OS thực hiện
  • gọi runtime.Gosched() bên trong Thu thập thông tin theo thời gian. Mặc dù tải CPU là 100%, điều này sẽ vượt qua kiểm soát một cách rõ ràng đến một Goroutine khác.

Chỉnh sửa: Vâng, và lý do tại sao fmt.Printf làm cho một sự khác biệt: bởi vì nó explicitely chuyển điều khiển đến một số nội dung syscall ...;)

+0

"Trong trường hợp này, Trình lên lịch biểu không điều khiển tay với một goroutine khác, theo thiết kế." Không hoàn toàn đúng. Bộ lập lịch Go 1.0 (?) Thực hiện điều này, nhưng nó là một bộ lập lịch không hoàn hảo. Các cách giải quyết bạn liệt kê (hoặc làm một syscall bằng cách gọi 'fmt.Println()') sẽ đánh thức trình lên lịch. Xem http://golang.org/doc/go1.2#preemption để biết chi tiết về những cải tiến này trong Go 1.2. – ayke