Open kmansei opened 1 year ago
Assigned solution
方針
fetchにtime.Sleep(time.Second)
を埋め込み、実行時間を計測したところ結果は3秒ほどとなった。
これは木構造を考えたときの木の高さと一致しているため、並列にcrawlできていると考える。
package main
import (
"fmt"
"sync"
"time"
)
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)
}
//追加
var cache = make(map[string]bool)
var waitGroup sync.WaitGroup
var mutex sync.Mutex
// Crawl uses fetcher to recursively crawl
// pages starting with url, to a maximum of depth.
func Crawl(url string, depth int, fetcher Fetcher) {
// TODO: Fetch URLs in parallel.
// TODO: Don't fetch the same URL twice.
// This implementation doesn't do either:
defer waitGroup.Done() //スレッドの終了時にカウント-1
if depth <= 0 {
return
}
//フェッチする前にキャッシュがあるか確認
//mutexで単一スレッドのみのアクセス
mutex.Lock()
if _, ok := cache[url]; ok {
mutex.Unlock()
return
} else {
cache[url] = true
mutex.Unlock()
}
body, urls, err := fetcher.Fetch(url)
if err != nil {
fmt.Println(err)
return
}
fmt.Printf("found: %s %q\n", url, body)
for _, u := range urls {
waitGroup.Add(1) //urlごとにカウント+1
go Crawl(u, depth-1, fetcher)
}
return
}
func main() {
//時間計測用
now := time.Now()
waitGroup.Add(1)
Crawl("https://golang.org/", 4, fetcher)
waitGroup.Wait()
fmt.Printf("実行時間: %vms\n", time.Since(now).Milliseconds())
}
// 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) {
time.Sleep(time.Second) //1秒待つ。時間計測用
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{
"https://golang.org/": &fakeResult{
"The Go Programming Language",
[]string{
"https://golang.org/pkg/",
"https://golang.org/cmd/",
},
},
"https://golang.org/pkg/": &fakeResult{
"Packages",
[]string{
"https://golang.org/",
"https://golang.org/cmd/",
"https://golang.org/pkg/fmt/",
"https://golang.org/pkg/os/",
},
},
"https://golang.org/pkg/fmt/": &fakeResult{
"Package fmt",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
"https://golang.org/pkg/os/": &fakeResult{
"Package os",
[]string{
"https://golang.org/",
"https://golang.org/pkg/",
},
},
}
授業で解説された上記の問題の回答例 https://pdos.csail.mit.edu/6.824/notes/crawler.go
クロージャの引数uを使わない形で実装できないかという議論 https://youtu.be/gA4YXUJX7t8?t=3440
実際に回答例のプログラムの一部を下記のように置き換えて実行してみた。
for _, u := range urls {
done.Add(1)
go func() {
defer done.Done()
ConcurrentMutex(u, fetcher, fs)
}()
}
実行結果は下記のようになった。
=== ConcurrentMutex ===
found: http://golang.org/
missing: http://golang.org/cmd/
全ての生成されたスレッドのConcurrentMutex(u, fetcher, fs)は "http://golang.org/cmd/" に対してfetchを行っていた。 これは全てのクロージャ(func())のuが外側のfor _, uのuを参照しているため。
1度目のイテレーションではuは "http://golang.org/pkg/" を指しているが、 2度目のイテレーションではuは "http://golang.org/cmd/" を指す。
つまり各スレッドで実行しているConcurrentMutexのuはエイリアスポインタになっていて、最初のスレッド(スレッド1とすると)が生成された時点ではuが "http://golang.org/pkg/" を指していたとしてめ、メインスレッドでイテレーションが回るとスレッド1の実行途中でuの参照が "http://golang.org/pkg/" に変わってしまう。
これを防ぐために回答例ではuのコピーを生成している。 (関数呼び出しの際にuをコピー)
for _, u := range urls {
done.Add(1)
go func(u string) {
defer done.Done()
ConcurrentMutex(u, fetcher, fs)
}(u)
}
GoのRece-detectorについて https://youtu.be/gA4YXUJX7t8?t=3968
online go tutorial 日本語版: https://go-tour-jp.appspot.com/concurrency/1