一、groutine
为了实现并发,产生groutine

带来的问题:各种并发带来的访问竞争

解决办法:各种并发原语/同步方式
1、加锁,即通过共享内存来通信
互斥锁 sync.Mutex
读写锁sync.RWMutex

2、waitgroup:可以控制一组groutine 同时运行并等待结果返回之后再进行后续操作
add 增加计数器
done 减少计数器
wait 判断计数器值进行阻塞

踩坑1:
wg传递 为指针类型
wait阻塞的是 该wait所在的groutine,故main groutine不会被阻塞,会继续执行

package main

import (
	"fmt"
	"sync"
)

func writeCh(wg *sync.WaitGroup, i int, ch chan []int) {
	defer wg.Done()
	data := make([]int, 0)
	data = append(data, i)
	ch <- data
	fmt.Printf("write data to ch = %v\n", data)
}

func readCh(ch chan []int) {
	for data := range ch {
		fmt.Printf("read ch data = %v\n", data)
	}
}

func main() {
	//注意:为指针类型
	wg := &sync.WaitGroup{}

	ch := make(chan []int)
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go writeCh(wg, i, ch)
	}

	go func() {
		//wait阻塞的是 该wait所在的groutine,故main groutine不会被阻塞,会继续执行
		wg.Wait()
		fmt.Println("wait end")
		close(ch)
	}()
	
	fmt.Println("start------------")
	
	//正确方法:此时for range由于ch未被关闭而阻塞,main函数不会继续执行下去,直到上一个wait 所在的groutine将ch关闭,main才执行
	for data := range ch {
		fmt.Printf("read ch data = %v\n", data)
	}
	
	//错误方法:由于main groutine不会被阻塞,所以 readCh 与 main并行执行,在readCh没执行结束时,main groutine就执行结束了,故readCh读不到任何东西
	//此时只会打印:
	//start------------
	//end
	//go readCh(ch)

	fmt.Println("end")

}

踩坑2:
goroutine 闭包不传递参数产生的错误

package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	c := make(chan int)
	
	//错误:
	//闭包= 函数+引用的变量
	//由于该变量被引用,分配在堆上,i还没写进管道,for循环就进到下一个i了
	//for i := 0; i < 10; i++ {
	//	wg.Add(1)
	//	go func() {
	//		defer wg.Done()
	//		c <- i
	//	}()
	//}
	
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			c <- i
		}(i)
	}

	go func() {
		for msg := range c {
			fmt.Println(msg)
		}
	}()

	wg.Wait()

	fmt.Println("end")
}

3、
3.1 once:保证函数只执行一次

// Go语言的入口 main() 函数所在的包(package)叫 main
// 通过 package main 该语句声明自己所在的包
// 在go中,任何一个package 都可有唯一一个带有main方法的go文件,即main.go只能有一个
package main

import (
	"fmt"
	"sync"
)

// 为什么要有once:并发情况下,期望某函数只被执行一次
// once可以被用来做什么:初始化配置、初始化实例(单例模式)、保持数据库连接等

type Student struct {
	Name string
	Age  int
}

//为什么该方法不正确?????????
//var s *Student
//
//func initialize() {
//	s = &Student{
//		Name: "gss",
//		Age:  18,
//	}
//}
//
//func main() {
//
//	var once sync.Once
//	once.Do(initialize())
//	fmt.Printf("%#v\n", s)
//}

var once sync.Once
var s *Student

func newInstance(wg *sync.WaitGroup) *Student {
	defer wg.Done()

	once.Do(func() {
		s = &Student{
			Name: "33",
			Age:  18,
		}
		fmt.Println("init")
	})
	return s
}

func main() {
	wg := &sync.WaitGroup{}
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go newInstance(wg)
	}
	wg.Wait()
	fmt.Printf("%#v\n", s)
}

3.2 init函数:也只执行一次
在一个 go 的包(package)中,全局变量与函数一样,不管在哪个 go 代码文件中声明,名称是全包内唯一,不能重复的。init 是是 go 中唯一可以在一个包内的不同代码文件中多次定义的函数名称(即唯一一种可在包内重复命名,一个包中可以有多个init函数

init不能被调用(不需要被调用执行),而是会按照const、var、init、main顺序直接被隐式加载执行。
init函数不接受任何参数,也没有返回值

由于在程序初始时就会被执行,故假如init所加载的函数变量久久未被使用到,则会浪费内存;而once则是在使用到的时候才初始化加载
https://zhuanlan.zhihu.com/p/34211611
https://www.lanseyujie.com/post/golang-init-and-sync-once-difference.html

4、sync.Pool:并发安全的临时对象池
为什么要有Pool:比如网络通信时,客户端收到结果,go需要unmarshal序列化服务器返回的结果,到临时结构体变量中。当程序并发度非常高的情况下,短时间内需要创建大量的临时对象。而这些对象是都是分配在堆上的,会给 GC 造成很大压力,严重影响程序的性能。

作用:
作为可复用的对象池。池中的对象通过Get方法获取,通过Put方法返入池中。
各个groutine之间可以共享这些临时对象

注意:
存放在池中的对象如果不活跃了会被自动清理,是被不被通知的释放

package main

import (
	"fmt"
	"sync"
)

type Student struct {
	Name string
	Age  int
}

var pool = &sync.Pool{
	New: func() interface{} {
		//new:初始化基本数据类型的内存。参数必须为一个基本数据类型,new返回一个指向该类型的指针,即返回的是地址
		//new:会申请内存并将值清0
		//make:初始化 一些引用类型(变量存储的本身就是一个地址)的 内存,如slice、map、chan
		return new(Student)
	},
}

func main() {
	s1 := pool.Get().(*Student)
	fmt.Printf("s1 = %#v, s1 addr = %v\n", s1, &s1)
	fmt.Println(s1)
	fmt.Println(*s1)

	//defer pool.Put(s1)
	s1.Name = "gss"
	s1.Age = 18
	fmt.Printf("now s1 = %#v\n", s1)
	fmt.Printf("%v,%p\n", s1, &s1)

	//将s1放回临时对象池,此时不一定会放入原来s1所在的内存地址,可能s1被放入别的内存地址,故取出的s2地址变了
	pool.Put(s1)

	//Get从临时对象池取出对象,该对象会从pool中移除,返回给调用者。注意,Get返回接口类型,需要进行断言,即指针类型
	//假如pool中没有更多的空闲元素可返回时,就会调用 New 方法来创建新的元素;如果未设置 New,当无更多的空闲元素可返回时,Get 方法将返回 nil,表明当前没有可用元素,故Get方法需要用户判断返回值是否为 nil
	s2 := pool.Get().(*Student)
	fmt.Printf("s2 = %#v, s2 addr = %v\n", s2, &s2)
}

5、sync/atomic
原子操作(执行不会被中断),无须加锁

二、管道
不是通过共享内存来通信,而是通过通信来共享内存

第一版:
因切片不支持并发,故
协程2 写管道
协程1 for select等待读管道,将读出的值写入最终的切片

waitgroup只等了携程2执行结束
故主线程和协程1并行执行了,主线程结束,管道写切片还没写完

改为:
for range读一个关闭的管道,读完就会退出。若管道没有被关闭,for range就会阻塞
管道关闭后仍可以读,只不过不能写

for里开协程,并发 把所有结果写到最终的切片里,因为不能并发,所以引入管道

1、无缓冲管道:

package main

import (
	"fmt"
	"time"
)

func writeChan(ch chan int) {
	j := 6
	fmt.Println(j)
	//如果没有接收groutine,则阻塞在这个for,只能写进去0,然后函数一直停在这里
	for i := 0; i < 4; i++ {
		fmt.Printf("i = %v\n", i)
		ch <- i
	}
	close(ch)
}

func readChan(ch chan int) {
	j := 7
	fmt.Println(j)
	//for range可以读一个被关闭的管道,若管道为空且没有关闭,for range就会阻塞,但因为main groutine执行,该阻塞会被退出从而打印end
	for data := range ch {
		fmt.Printf("data = %v\n", data)
	}
}

func main() {
	//管道:用来在groutine之间通信(跨进程时channel无法通信)
	//无缓冲管道:必须在接收方准备好后,发送方才能发送成功,否则发送方阻塞在发送,无法继续发送成功。
	//          即无缓冲管道是同步收发,信必须交到收件人手上,快递员才能离开
	c := make(chan int)
	go writeChan(c)
	go readChan(c)
	time.Sleep(6 * time.Second)
	fmt.Println("end")
}

2、带缓冲管道

package main

import (
	"fmt"
	"time"
)

func writeChan(ch chan int) {
	j := 6
	fmt.Println(j)
	for i := 0; i < 4; i++ {
		ch <- i
		fmt.Printf("i = %v\n", i)
	}
	//close(ch)
}

func readChan(ch chan int) {
	for data := range ch {
		fmt.Printf("data = %v\n", data)
	}
}

func main() {
	//带缓冲管道:不用有接收方,即可写成功
	//发送阻塞只会出现在以下情况:
	//1、往通道发送数据时,只有当缓存满时,发送方才会阻塞(此时,main groutine也会继续执行,直到右括号main结束,发送方阻塞也被结束)
	c := make(chan int, 2)

	go writeChan(c)
	go readChan(c)

	time.Sleep(6 * time.Second)

	//2、从通道接收数据时,只有当缓存为空时,接收方才会阻塞(此时缓存为空,该语句被阻塞,永远也执行不到打印end)
	data := <-c
	fmt.Println(data)
	
	//3、容量与长度
	//第2个参数=容量,即能容纳的最大数据个数, cap(c3)
	//长度,当前时刻缓存的数据个数, len(c3)
	c3 := make(chan string, 2)
	c3 <- "gss"
	fmt.Printf("chan cap = %d\n", cap(c3))
	fmt.Printf("chan len = %d\n", len(c3))
	
	fmt.Println("end")
}

结果:

6
i = 0
i = 1
i = 2
data = 0
data = 1
data = 2
data = 3
i = 3

chan cap = 2
chan len = 1
end