从实例学习 Go 语言、"并发内容" 学习笔记及心得体会、Go指南

准备工作😀

基础内容

从实例学习 Go 语言,两天速通 Golang 的学习笔记及心得体会

学习地址

两个 Go 教程平台 github 项目地址

两个 Go 教程平台 部署地址(使用这个

如果文章字体过小,请调整浏览器页面缩放。Windows: Ctrl + 鼠标滚轮

注意:本篇文章代码注释使用了 vscode 的 better-comments 拓展,具体请看 

[个人配置] VSCode Better Comments 扩展配置、高亮注释插件

练习答案

Go 指南里有部分练习题,下面是我写的答案,点击跳转

资料下载

部分代码:2020_3/Concurrence.zip

 


并发知识🤣

Go 程

Go 程(goroutine)是由 Go 运行时管理的轻量级线程。

go f(x, y, z)

会启动一个新的 Go 程并执行

f(x, y, z)

fxy 和 z 的求值发生在当前的 Go 程中,而 f 的执行发生在新的 Go 程中。

Go 程在相同的地址空间中运行,因此在访问共享的内存时必须进行同步。sync 包提供了这种能力,不过在 Go 中并不经常用到,因为还有其它的办法。

func say(s string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s)
	}
}

func main() {
	go say("world")
	say("hello")
}
hello
world
world
hello
hello
world
world
hello
hello
world

 


信道

信道使用方式

信道是带有类型的管道,你可以通过它用信道操作符 <- 来发送或者接收值。

ch <- v    // 将 v 发送至信道 ch。
v := <-ch  // 从 ch 接收值并赋予 v。

(“箭头”就是数据流的方向。)

和映射与切片一样,信道在使用前必须创建

ch := make(chan int)

默认情况下,发送和接收操作在另一端准备好之前都会阻塞。这使得 Go 程可以在没有显式的锁或竞态变量的情况下进行同步。

以下示例对切片中的数进行求和,将任务分配给两个 Go 程。一旦两个 Go 程完成了它们的计算,它就能算出最终的结果。

func sum(s []int, c chan int) {
	sum := 0
	for _, v := range s {
		sum += v
	}
	c <- sum // 将和送入 c
}

func main() {
	s := []int{7, 2, 8, -9, 4, 0}

	c := make(chan int)
	go sum(s[:len(s)/2], c)
	go sum(s[len(s)/2:], c)
	x, y := <-c, <-c // 从 c 中接收

	fmt.Println(x, y, x+y)
}
-5 17 12

只有当两个 go 程 都完成了各自的任务将结果输入信道后,位于主线程下的 x, y := <-c, <-c 才能不阻塞,向下执行

 


带缓冲的信道

信道可以是 带缓冲的。将缓冲长度作为第二个参数提供给 make 来初始化一个带缓冲的信道:

ch := make(chan int, 100)

仅当信道的缓冲区填满后,向其发送数据时才会阻塞。当缓冲区为空时,接受方会阻塞。

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	// @ ch <- 3 信道已满如果再输入那么会阻塞
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

 


range 、close

发送者可通过 close 关闭一个信道来表示没有需要发送的值了。接收者可以通过为接收表达式分配第二个参数来测试信道是否被关闭:若没有值可以接收且信道已被关闭,那么在执行完

v, ok := <-ch

之后 ok 会被设置为 false

循环 for i := range c 会不断从信道接收值,直到它被关闭。

*注意:* 只有发送者才能关闭信道,而接收者不能。向一个已经关闭的信道发送数据会引发程序恐慌(panic)。

func main() {
	c := make(chan int, 10)
	c <- 1
	c <- 2
	for v := range c {
		fmt.Println(v)
		close(c) // ^ 接收方不能关闭通道
	}
}

*还要注意:* 信道与文件不同,通常情况下无需关闭它们。只有在必须告诉接收者不再有需要发送的值时才有必要关闭,例如终止一个 range 循环。

func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x+y
	}
	close(c)
}

func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	for i := range c {
		fmt.Println(i)
	}
}

 


select 语句

select 语句使一个 Go 程可以等待多个通信操作。

select 会阻塞到某个分支可以继续执行为止,这时就会执行该分支。当多个分支都准备好时会随机选择一个执行。

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)
	go func() {
		for i := 0; i < 5; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}
0
1
1
2
3
quit

select 里 case 通道操作,需要另一方准备好才执行。比如说 case: <-quit 那我另一方需要发送数据 quit <- 0 ,这样这个 case 就可以执行

 


当 select 中的其它分支都没有准备好时,default 分支就会执行。

为了在尝试发送或者接收时不发生阻塞,可使用 default 分支:

select {
case i := <-c:
    // 使用 i
default:
    // 从 c 中接收会阻塞时执行
}
func main() {
	tick := time.Tick(100 * time.Millisecond)
	boom := time.After(500 * time.Millisecond)
	for {
		select {
		case <-tick:
			fmt.Println("tick.")
		case <-boom:
			fmt.Println("BOOM!")
			return
		default:
			fmt.Println("    .")
			time.Sleep(50 * time.Millisecond)
		}
	}
}
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
BOOM!

执行过程

  • tick 通道每100毫秒发送一次,初始没有值,是个空通道
  • boom 通道在500秒后发送一次,初始没有值,是个空通道
  • 执行 for 循环,select 语句被重复执行
  • 第一次执行 select,前两个 case 在接收时均阻塞(对方没有准备好),于是执行 default 分支,打印 ‘.’并延迟50毫秒
  • 第二次执行 select 同理
  • 第三次执行 select,经过100毫秒,tick 通道被发送了一次值,select 中第一条 case 中能接收到tick,可以执行,打印 'tick.',tick 通道数据被接收又变为空通道
  • 第四次执行 select 跟第一次同理,第五次跟第一次也同理,第六次跟第三次同理,循环下去
  • 直到500毫秒,boom 被发送了值,第二条 case 中能接收到boom,可以执行,打印 ‘boom!’并退出程序

 


练习:等价二叉树

题目地址:https://tour.go-zh.org/concurrency/7

不同二叉树的叶节点上可以保存相同的值序列。例如,以下两个二叉树都保存了序列 `1,1,2,3,5,8,13`。

在大多数语言中,检查两个二叉树是否保存了相同序列的函数都相当复杂。 我们将使用 Go 的并发和信道来编写一个简单的解法。

本例使用了 tree 包,它定义了类型:

type Tree struct {
    Left  *Tree
    Value int
    Right *Tree
}

1. 实现 Walk 函数。

2. 测试 Walk 函数。

函数 tree.New(k) 用于构造一个随机结构的已排序二叉查找树,它保存了值 k2k3k, ..., 10k

创建一个新的信道 ch 并且对其进行步进:

go Walk(tree.New(1), ch)

然后从信道中读取并打印 10 个值。应当是数字 1, 2, 3, ..., 10

3. 用 Walk 实现 Same 函数来检测 t1 和 t2 是否存储了相同的值。

4. 测试 Same 函数。

Same(tree.New(1), tree.New(1)) 应当返回 true,而 Same(tree.New(1), tree.New(2)) 应当返回 false

Tree 的文档可在这里找到。

package main

import (
	"fmt"

	"golang.org/x/tour/tree"
)

// Walk 步进 tree t 将所有的值从 tree 发送到 channel ch。
func Walk(t *tree.Tree, ch chan int) {
	switch {
	case t == nil:
		return
	case t != nil:
		Walk(t.Left, ch)
		ch <- t.Value
		Walk(t.Right, ch)
	}
}

// Same 检测树 t1 和 t2 是否含有相同的值。
func Same(t1, t2 *tree.Tree) bool {
	c1, c2 := make(chan int), make(chan int)
	go Walk(t1, c1)
	go Walk(t2, c2)
	for i := 0; i < 10; i++ {
		if <-c1 != <-c2 {
			return false
		}
	}

	return true
}

func main() {
	res := Same(tree.New(1), tree.New(1))
	res2 := Same(tree.New(1), tree.New(2))
	fmt.Println(res)
	fmt.Println(res2)
}
true
false

 


编程错误

func Same(t1, t2 *tree.Tree) bool {
	c1, c2 := make(chan int, 10), make(chan int, 10)
	go Walk(t1, c1)
	go Walk(t2, c2)
	for v1 := range c1 {
		for v2 := range c2 {
			println(v1, v2)
			if v1 != v2 {
				return false
			}
		}
	}
	return true
}

这是我第一次敲的,这段代码有两个明显的错误

  • 我没有关闭通道,那么 for 循环就永远跳不出
  • 第一次 for 循环,第二层 for range 会一直遍历下去。所以回不去第一层
1 1
1 2
false

可以看到第一次,第二次的数值都是 1,所以始终还在外层 for 循环的第一次运行过程之中

 


sync.Mutex

我们已经看到信道非常适合在各个 Go 程间进行通信。

但是如果我们并不需要通信呢?比如说,若我们只是想保证每次只有一个 Go 程能够访问一个共享的变量,从而避免冲突?

这里涉及的概念叫做 *互斥(mutual*exclusion)* ,我们通常使用 *互斥锁(Mutex)* 这一数据结构来提供这种机制。

Go 标准库中提供了 sync.Mutex 互斥锁类型及其两个方法:

  • Lock
  • Unlock

我们可以通过在代码前调用 Lock 方法,在代码后调用 Unlock 方法来保证一段代码的互斥执行。

加锁期间其他协程会进入阻塞状态直到解锁。我们也可以用 defer 语句来保证互斥锁一定会被解锁。

package main

import (
	"fmt"
	"sync"
	"time"
)

// SafeCounter 的并发使用是安全的。
type SafeCounter struct {
	v   map[string]int
	mux sync.Mutex
}

// Inc 增加给定 key 的计数器的值。
func (c *SafeCounter) Inc(key string) {
	c.mux.Lock()
	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
	c.v[key]++
	c.mux.Unlock()
}

// Value 返回给定 key 的计数器的当前值。
func (c *SafeCounter) Value(key string) int {
	c.mux.Lock()
	// Lock 之后同一时刻只有一个 goroutine 能访问 c.v
	defer c.mux.Unlock()
	return c.v[key]
}

func main() {
	c := SafeCounter{v: make(map[string]int)}
	for i := 0; i < 1000; i++ {
		go c.Inc("somekey")
	}

	time.Sleep(time.Second)
	fmt.Println(c.Value("somekey"))
}
1000

不使用互斥锁 fatal error: concurrent map writes

map为引用类型,高并发时对map并发写会产生竞争,不管是否同一个key都会报错,并发对map读是不会有问题的。

 


练习:Web 爬虫

题目地址:https://tour.go-zh.org/concurrency/10

在这个练习中,我们将会使用 Go 的并发特性来并行化一个 Web 爬虫。

修改 Crawl 函数来并行地抓取 URL,并且保证不重复。

提示:你可以用一个 map 来缓存已经获取的 URL,但是要注意 map 本身并不是并发安全的!

sync.WaitGroup,创建一个任务,sw.Add(1),加一;任务完成的时候使用 sw.Done() 来将任务减一;使用 sw.Wait() 来阻塞等待所有任务完成。

// ^ 创建一个映射,互斥锁,使用另外一种声明方法
var (
	m     = make(map[string]bool)
	mutex sync.Mutex
	wg    sync.WaitGroup
)

// Crawl 使用 fetcher 从某个 URL 开始递归的爬取页面,直到达到最大深度。
func Crawl(url string, depth int, fetcher Fetcher) {
	defer wg.Done()

	if m[url] == false {
		mutex.Lock()
		m[url] = true
		mutex.Unlock()
	} else {
		return
        // ^ 不能把 Lock() 放这里,已经return,会卡住
	}

	if depth <= 0 {
		return
	}
	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 {
		wg.Add(1)
		go Crawl(u, depth-1, fetcher)
	}

}

func main() {
	wg.Add(1) // ^ 主线程的调用也要算一次
	Crawl("https://golang.org/", 4, fetcher)
	wg.Wait()
}
found: https://golang.org/ "The Go Programming Language"
not found: https://golang.org/cmd/
found: https://golang.org/pkg/ "Packages"
found: https://golang.org/pkg/os/ "Package os"
found: https://golang.org/pkg/fmt/ "Package fmt"

添加一个映射,用于判定网页是否已经爬过,如果爬过了就直接return,修改映射的时候需要添加互斥锁,注意互斥锁添加的位置。用 sync.WaitGroup 来阻塞主线程,防止主线程先结束,那么子线程也就自动结束了。主线程下手动执行了 Crawl 所以我们要手动给 wg.Add(1)


点赞是一种积极的生活态度,喵喵喵!(疯狂暗示)

posted @ 2022-03-25 19:34  小能日记  阅读(189)  评论(0编辑  收藏  举报