package main
import (
"fmt"
"sync"
)
//
// Several solutions to the crawler exercise from the Go tutorial
// https://tour.golang.org/concurrency/10
//
//
// Serial crawler
//
// 看Mit这节课的时候,感觉mit的学生真的很不一样,一直在提问题,而且直接打断老师并说出自己的想法。
// 可能他们在课前都做了一些预习,而且哪怕这个知识点很简单也会问,老师也会认真讲
func Serial(url string, fetcher Fetcher, fetched map[string]bool) {
if fetched[url] {
return
}
fetched[url] = true
urls, err := fetcher.Fetch(url)
if err != nil {
return
}
for _, u := range urls {
Serial(u, fetcher, fetched)
// go Serial(u, fetcher, fetched)
// 如果这里开启go的子线程,那么由于主线程for了三次之后就return了主线程直接结束了,那么开启的那三个子线程也就跟着结束了所以打印结果也就只有一个所以这里其实需要一个类似CountDownLatch的东西让主线程阻塞住,等待其他线程执行结束
}
return
}
//
// Concurrent crawler with shared state and Mutex
//
type fetchState struct {
mu sync.Mutex
fetched map[string]bool
}
func ConcurrentMutex(url string, fetcher Fetcher, f *fetchState) {
// 因为有多个线程同时搜索,可能会遍历到同一个url,所以需要加锁来实现互斥访问
// 在操作系统和并发编程中,"竞态条件"(Race Condition)是指多个进程或线程在并发执行时,对共享资源的访问顺序和时机不确定,导致程序行为无法预测的情况。竞态条件通常是编程错误,因为它们可能导致不一致的状态、数据损坏和不稳定的程序行为。
//竞态条件通常发生在多个进程或线程试图同时访问和修改共享数据时,而没有足够的同步措施来确保这些访问发生的顺序和时机正确。这可能导致数据竞争、死锁、数据损坏和程序崩溃等问题。
//为了避免竞态条件,程序员通常使用同步机制,如互斥锁(Mutex)、信号量(Semaphore)、条件变量(Condition Variable)等,来确保多个线程或进程之间对共享资源的访问是有序的和可控的。这些同步机制可以帮助确保一次只有一个线程或进程能够访问共享资源,从而防止竞态条件的发生。
//总之,竞态条件是多线程或多进程编程中的常见问题,需要小心管理共享资源的访问,以确保程序的正确性和稳定性。
f.mu.Lock()
already := f.fetched[url]
f.fetched[url] = true
f.mu.Unlock()
// 如果不加锁的话,会出现同一个url爬取两次
if already {
return
}
urls, err := fetcher.Fetch(url)
if err != nil {
return
}
var done sync.WaitGroup
/*
WaitGroup :同步原语,用于等待一组携程的执行完成。主要用于在主协程等待一组子协程完成
其工作时,以协调并发任务。
*/
for _, u := range urls {
done.Add(1) // 告诉WaitGroup 有一个协程需要等待
//u2 := u
//go func() {
// defer done.Done()
// ConcurrentMutex(u2, fetcher, f)
//}()
go func(u string) {
defer done.Done() // 告诉 WaitGroup 协程完成了
// 加了defer关键字就意味着在包含他的函数结束前一定会调用done.Done()
// 类似于Java中try catch finally 中的finally,无论该线程是否成功,最后一定都会执行defer的语句
// 学生提问:为什么两个不同threads对done的调用不构成竞态
// 答案:因为WaitGroup内部有互斥锁,多个线程去执行这个操作的时候,并不会造成并发安全问题
ConcurrentMutex(u, fetcher, f)
// 学生提问:为什么这里要把u当作参数传进来,而不是和fetcher参数一样使用外部参数u
// 答案:假如说直接使用了for循环中u变量,当线程1还没有开始的时候,for循环遍历
// 到了下一个u,外部参数u发生了变化,线程1在去使用u的时候用的是下一个u
// 这就造成了访问错误的问题
// 所以这里使用传参的方式是为了保证当前调用的u是当前需要的u,而不是线程执行时更新的u
// 如果使用传参的话,当前线程就会保存当前u的内容,我需要在去深入学习一下jvm
// 学生提问:如果你有一个内部函数访问定义在外部的变量,但是外部函数返回之后,内部函数的变量将指向那里?
// 答案:Go会分析内部函数,内部函数可以访问外部函数中定义的变量,这些变量通常称为"闭包变量"。当外部函数返回后,这些闭包变量仍然可以被内部函数引用,因为它们存在于内部函数的作用域中,而不是外部函数。
// 闭包变量通常会被存储在堆内存中,而不是栈内存中。这是因为栈内存通常用于存储函数的局部变量,而当外部函数返回时,其栈帧会被销毁,局部变量也会被释放。为了使闭包变量在外部函数返回后仍然可用,它们需要被存储在堆内存中。
// 当内部函数引用外部函数的变量时,编程语言会自动创建一个包含这些变量的数据结构,通常称为闭包(closure)。这个闭包包含了内部函数以及它所引用的外部变量的引用。这个闭包被分配在堆内存中,以确保在外部函数返回后,这些变量仍然可以访问。
}(u)
}
done.Wait() // 阻塞,直到所有协程都调用了Done
return
}
// 初始化fetchState
func makeState() *fetchState {
f := &fetchState{}
f.fetched = make(map[string]bool)
return f
}
//
// crawler with channels
//
func worker(url string, ch chan []string, fetcher Fetcher) {
urls, err := fetcher.Fetch(url)
if err != nil {
ch <- []string{}
} else {
ch <- urls
}
}
func coordinator(ch chan []string, fetcher Fetcher) {
n := 1
fetched := make(map[string]bool)
// Go 可以通过range 来实现遍历读取到的数据,每次都会从通道中取一个数据,当通道为空时
// 在取数据就会阻塞,所以需要加一个计数器n来记录当前管道中还有几个数据,当n==0时,表示全部取完了,结束循环
// 如果通道为空,接收操作会阻塞直到有数据可用。如果通道已满,发送操作也会阻塞直到有足够的空间。
// channel 内部有互斥锁,不会出现发送方发送数据的同时,接收方在接收数据
// 类似BFS
for urls := range ch {
for _, u := range urls {
if fetched[u] == false {
fetched[u] = true
n += 1
go worker(u, ch, fetcher)
}
}
n -= 1
// 如果没有break的话,那么将会一直阻塞,因为range ch 的过程中,如果没有数据可读的话,就会阻塞等待数据发送
if n == 0 {
break
}
}
}
func ConcurrentChannel(url string, fetcher Fetcher) {
ch := make(chan []string)
go func() {
ch <- []string{url}
}()
coordinator(ch, fetcher)
}
//
// main
//
func main() {
fmt.Printf("=== Serial===\n")
Serial("http://golang.org/", fetcher, make(map[string]bool))
fmt.Printf("=== ConcurrentMutex ===\n")
ConcurrentMutex("http://golang.org/", fetcher, makeState())
fmt.Printf("=== ConcurrentChannel ===\n")
ConcurrentChannel("http://golang.org/", fetcher)
}
//
// Fetcher
//
type Fetcher interface {
// Fetch returns a slice of URLs found on the page.
Fetch(url string) (urls []string, err error)
}
// 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, error) {
if res, ok := f[url]; ok {
fmt.Printf("found: %s\n", url)
return res.urls, nil
}
fmt.Printf("missing: %s\n", url)
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/",
},
},
}