go的学习--2
go的并发
goroutine 奉行通过通信来共享内存,而不是共享内存来通信。
goroutine
实现goroutine的方式就是给函数前加go关键字调用
package main
import "fmt"
func main() {
fmt.Println("njjnjkn")
go Do() // 启动另外一个goroutine去执行Do函数
}
func Do() {
fmt.Println("it is goroutine")
}
如果主协程退出了,那么其他协程也会推出
package main
import (
"fmt"
"time"
)
func main() {
// 合起来写
go func() {
i := 0
for {
i++
fmt.Printf("new goroutine: i = %d\n", i)
time.Sleep(time.Second)
}
}()
i := 0
for {
i++
fmt.Printf("main goroutine: i = %d\n", i)
time.Sleep(time.Second)
if i == 2 {
break
}
}
}
/*
main goroutine: i = 1
new goroutine: i = 1
new goroutine: i = 2
main goroutine: i = 2
new goroutine: i = 3
*/
channel通道
单纯的讲函数并发执行是没有意义的。函数与函数间需要数据交换才能体现并发执行函数的意义。
虽然可使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。
go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存是想通讯。
如果说goroutine是go程序并发的执行体,channel就是他们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。
go语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,遵循先入先出的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是很声明channel的时候需要为其指定元素类型
channel的三种操作
通道有发送(send)、接收(receive)和关闭(close)三种操作。发送和接收都使用<-符号
通道不一定要关闭,但是文件关闭是必须的
关闭后的通道的特点:
1.对一个关闭的通道再发送值就会导致panic。
2.对一个关闭的通道进行接收会一直获取值直到通道为空。
3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
4.关闭一个已经关闭的通道会导致panic。
无缓冲通道(阻塞通道)
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 20
fmt.Println("发送成功")
}
/*
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/Users/tiger/GolandProjects/awesomeProject/10.go:7 +0x31
*/
// 无缓冲通道必须要有接收才可以发送,出现这种情况是因为没有接收。
将以上程序修一下即可:
package main
import "fmt"
func main() {
ch := make(chan int)
go recv(ch)
ch <- 20
fmt.Println("发送成功")
}
func recv(ch chan int) {
ret := <-ch
fmt.Printf("接收的值为%v\n", ret)
}
/*
接收的值为20
发送成功
*/
有缓冲的通道
只要通道缓冲数量大于0就是有缓冲通道
package main
import "fmt"
func main() {
ch := make(chan int, 1)
//go recv(ch)
ch <- 20
fmt.Println("发送成功")
}
func recv(ch chan int) {
ret := <-ch
fmt.Printf("接收的值为%v\n", ret)
}
判断通道是否关闭用range遍历;如果通道被关闭, 该通道发送值会引发panic,从该通道里接收的值一直都是类型零值
单向通道
package main
import "fmt"
func counter(out chan<- int) {
for i := 0; i < 100; i++ {
out <- i
}
close(out)
}
func squarer(out chan<- int, in <-chan int) {
for i := range in {
out <- i * i
}
close(out)
}
func printer(in <-chan int) {
for i := range in {
fmt.Println(i)
}
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go counter(ch1)
go squarer(ch2, ch1)
printer(ch2)
}
1.chan<- int是一个只能发送的通道,可以发送但是不能接收;
2.<-chan int是一个只能接收的通道,可以接收但是不能发送。
定时器
package main
func main() {
// 1.timer基本使用
//timer1 := time.NewTimer(10 * time.Second) // 间隔n时间响应
//t1 := time.Now()
//fmt.Printf("t1:%v\n", t1) // t1:2021-10-21 16:10:27.005484 +0800 CST m=+0.000210609
//t2 := <-timer1.C
//fmt.Printf("t2:%v\n", t2) // t2:2021-10-21 16:10:37.008027 +0800 CST m=+10.002833252
// 2.验证timer只能响应1次
//timer2 := time.NewTimer(time.Second)
//for {
// <-timer2.C
// fmt.Println("时间到")
//}
/*
时间到
fatal error: all goroutines are asleep - deadlock!
*/
// 3.timer实现延时的功能
//(1)
//time.Sleep(time.Second)
//(2)
//timer3 := time.NewTimer(2 * time.Second)
//<-timer3.C
//fmt.Println("2秒到")
//(3)
//<-time.After(2*time.Second)
//fmt.Println("2秒到")
// 4.停止定时器
//timer4 := time.NewTimer(2 * time.Second)
//go func() {
// <-timer4.C
// fmt.Println("定时器执行了")
//}()
//b := timer4.Stop()
//if b {
// fmt.Println("timer4已经关闭")
//}
/*
timer4已经关闭
*/
}
select多路复用
select可以同时监听一个或多个channel,直到其中一个channel ready
package main
import (
"fmt"
"time"
)
func test1(ch chan string) {
time.Sleep(time.Second * 5)
ch <- "test1"
}
func test2(ch chan string) {
time.Sleep(time.Second * 2)
ch <- "test2"
}
func main() {
// 2个管道
output1 := make(chan string)
output2 := make(chan string)
// 跑2个子协程,写数据
go test1(output1)
go test2(output2)
// 用select监控
select {
case s1 := <-output1:
fmt.Println("s1=", s1)
case s2 := <-output2:
fmt.Println("s2=", s2)
}
}
/*
s2= test2
*/
如果多个channel同时ready,则随机选择一个执行
package main
import (
"fmt"
)
func main() {
// 创建2个管道
int_chan := make(chan int, 1)
string_chan := make(chan string, 1)
go func() {
//time.Sleep(2 * time.Second)
int_chan <- 1
}()
go func() {
//time.Sleep(2 * time.Second)
string_chan <- "hello"
}()
select {
case value := <-int_chan:
fmt.Println("int:", value)
case value := <-string_chan:
fmt.Println("string:", value)
}
fmt.Println("main结束")
}
/*
string: hello
main结束
*/
判断管道是否存满
package main
import (
"fmt"
"time"
)
// 判断管道有没有存满
func main() {
// 创建管道
output1 := make(chan string, 10)
// 子协程写数据
go write(output1)
// 取数据
for s := range output1 {
fmt.Println("res:", s)
time.Sleep(time.Second)
}
}
func write(ch chan string) {
for {
select {
// 写数据
case ch <- "hello":
fmt.Println("write hello")
default:
fmt.Println("channel full")
}
time.Sleep(time.Millisecond * 500)
}
}
sync
sync.WaitGroup
go中使用sync.WaitGroup来实现并发任务的同步
sync.WaitGroup 内部维护者一个计数器,计数器的值可以增加和减少。比如启动n个并发任务时,就将计数器的值增加n。灭个任务完成时调用Done方法将计数器减1.通过Wait()来等待并发任务执行完。当计数器值为0时,表示所有并发任务已经完成
方法名 功能
(wg * WaitGroup) Add(delta int) 计数器+delta
(wg *WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func hello() {
defer wg.Done()
fmt.Println("Hello Goroutine!")
}
func main() {
wg.Add(2)
go hello() // 启动另外一个goroutine去执行hello函数
go hello() // 启动另外一个goroutine去执行hello函数
fmt.Println("main goroutine done!")
wg.Wait()
}
/*
main goroutine done!
Hello Goroutine!
Hello Goroutine!
*/
sync.Once
在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。
Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。
如果要执行的函数f需要传递参数就需要搭配闭包来使用
sync.Once只有一个Do方法,其签名如下:
func (o *Once) Do(f func()) {}
package main
import (
"fmt"
"sync"
"time"
)
func onceDo() {
var num int
sign := make(chan bool)
var once sync.Once
f := func(ii int) func() {
return func() {
num = (num + ii*2)
sign <- true
}
}
for i := 0; i < 3; i++ {
fi := f(i + 1)
go once.Do(fi)
}
for j := 0; j < 3; j++ {
select {
case <-sign:
fmt.Println("recieve a signal")
case <-time.After(100 * time.Millisecond):
fmt.Println("timeout!")
}
}
fmt.Printf("Num: %d.\n", num)
}
func main() {
onceDo()
}
sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。
看一下源码
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package sync
import (
"sync/atomic"
)
// Once is an object that will perform exactly one action.
//
// A Once must not be copied after first use.
type Once struct {
// done indicates whether the action has been performed.
// It is first in the struct because it is used in the hot path.
// The hot path is inlined at every call site.
// Placing done first allows more compact instructions on some architectures (amd64/386),
// and fewer instructions (to calculate offset) on other architectures.
done uint32
m Mutex
}
// 可以看出Once是一个结构体,包含一个rune类型和锁
// Do calls the function f if and only if Do is being called for the
// first time for this instance of Once. In other words, given
// var once Once
// if once.Do(f) is called multiple times, only the first call will invoke f,
// even if f has a different value in each invocation. A new instance of
// Once is required for each function to execute.
//
// Do is intended for initialization that must be run exactly once. Since f
// is niladic, it may be necessary to use a function literal to capture the
// arguments to a function to be invoked by Do:
// config.once.Do(func() { config.init(filename) })
//
// Because no call to Do returns until the one call to f returns, if f causes
// Do to be called, it will deadlock.
//
// If f panics, Do considers it to have returned; future calls of Do return
// without calling f.
//
func (o *Once) Do(f func()) {
// Note: Here is an incorrect implementation of Do:
//
// if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
// f()
// }
//
// Do guarantees that when it returns, f has finished.
// This implementation would not implement that guarantee:
// given two simultaneous calls, the winner of the cas would
// call f, and the second would return immediately, without
// waiting for the first's call to f to complete.
// This is why the slow path falls back to a mutex, and why
// the atomic.StoreUint32 must be delayed until after f returns.
if atomic.LoadUint32(&o.done) == 0 {
// Outlined slow-path to allow inlining of the fast-path.
o.doSlow(f) // 执行函数
}
}
func (o *Once) doSlow(f func()) {
o.m.Lock() // 这边枷锁
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}
sync.Map
package main
import (
"fmt"
"strconv"
"sync"
)
var m = sync.Map{}
func main() {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n)
value, _ := m.Load(key)
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
}
/*
k=:2,v:=2
k=:3,v:=3
k=:8,v:=8
k=:10,v:=10
k=:6,v:=6
k=:19,v:=19
k=:13,v:=13
k=:7,v:=7
k=:11,v:=11
k=:12,v:=12
k=:5,v:=5
k=:14,v:=14
k=:4,v:=4
k=:15,v:=15
k=:16,v:=16
k=:17,v:=17
k=:18,v:=18
k=:1,v:=1
k=:0,v:=0
k=:9,v:=9
*/
并发安全和锁
go代码中可能存在多个goroutine同时操作一个资源,这种情况会发生竞态问题
package main
import (
"fmt"
"sync"
)
var x int64
var wg sync.WaitGroup
func add() {
for i := 0; i < 5000; i++ {
x = x + 1
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
// 期望结果是10000。 结果并不是
下面代码如果开启两个goroutine去累加变量x的值,这两个goroutine在访问和修改变量x的时候就会存在数据竞争,导致与预期结果不符合。
互斥锁
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。 使用互斥锁来修复上面代码的问题:
package main
import (
"fmt"
"sync"
)
var x int64
var wg sync.WaitGroup
var lock sync.Mutex
func add() {
for i := 0; i < 5000; i++ {
lock.Lock() // 加锁
x = x + 1
lock.Unlock() // 解锁
}
wg.Done()
}
func main() {
wg.Add(2)
go add()
go add()
wg.Wait()
fmt.Println(x)
}
// 达到预期结果10000
读写互斥锁
互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。
读写锁分为两种:读所和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是写锁就会等待;如果获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。
package main
import (
"fmt"
"sync"
"time"
)
var (
x int64
wg sync.WaitGroup
lock sync.Mutex
rwlock sync.RWMutex
)
func write() {
lock.Lock() // 加互斥锁
//rwlock.Lock() // 加写锁
x = x + 1
time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒
//rwlock.Unlock() // 解写锁
lock.Unlock() // 解互斥锁
wg.Done()
}
func read() {
lock.Lock() // 加互斥锁
//rwlock.RLock() // 加读锁
time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒
//rwlock.RUnlock() // 解读锁
lock.Unlock() // 解互斥锁
wg.Done()
}
func main() {
start := time.Now()
for i := 0; i < 10; i++ {
wg.Add(1)
go write()
}
for i := 0; i < 1000; i++ {
wg.Add(1)
go read()
}
wg.Wait()
end := time.Now()
fmt.Println(end.Sub(start))
// 读写锁花费113.212093ms
// 互斥锁花费1.482275024s
}
需要注意的是读写锁非常适合读多写少的场景,如果读和写的操作差别不大,读写锁的优势就发挥不出来
GMP原理与调度
线程与协程
-
最早期的单进程的操作系统,只能一个任务一个人任务的处理;后来的操作系统就有了多进程并发的能力,一个进程阻塞,切换到另外的进程执行
-
多进程/多线程操作系统中,解决了阻塞问题;调度cpu的算法可以保证运行的进程都可以被分配到cpu的运行时间片,但是进程占用太多的资源,它的创建、切换、销毁都会占用很长时间。如果进程过多,cpu很大一部分都被用来做进程调度了。
-
协程来调高cpu利用率。多进程和多线程已经提高了系统并发能力,但是创建很多线程是有问题的,他会占用大量内存(进程虚拟内存占用4GB[32bit],线程需要大概4MB)和调度的高消耗cpu。在后面,工程师发现线程其实分为“内核态”线程和“用户态”线程。一个用户态线程必须绑定一个内核态线程,但是cpu只认识内核态线程(linux的PCB控制块)

-
在细细划分一下,内核线程依然叫线程(thread),用户线程叫协程(co-routine)

-
我们来划分一下线程和协程的数量关系映射
-
N:1关系
n个协程绑定一个线程,优点是协程在用户态线程即完成切换,不会陷入内核态,这种切换非常的轻量快速。但也有很大缺点,一个进程的所有协程都绑定在了一个线程上。缺点:某个程序运用不了硬件的多喝加速能力;一旦某协程阻塞,造成线程阻塞,进程的其他协程都无法执行,就没有并发的能力了。
-

- 1:1关系
1 个协程绑定 1 个线程,这种最容易实现。协程的调度都由 CPU 完成了,不存在 N:1 缺点。但是缺点也很明显,协程的创建、删除和切换的代价都由 CPU 完成,有点略显昂贵了。

- M:N关系
M 个协程绑定 1 个线程,是 N:1 和 1:1 类型的结合,克服了以上 2 种模型的缺点,但实现起来最为复杂。

协程和线程是有区别的,线程是cpu抢占式的,而协程是协作式的,一个协程工作完让出cpu执行下一个协程
go的goroutine
go提供了更容易并发的方法,使用了goroutine和channel。goroutine来自协程的概念,让一组可复用的函数运行在一组线程之上,即使有协程阻塞,该线程的其他协程也可以被runtime调用,转移到其他可运行的线程上。go的goroutine只占几KB,并且足够goroutine运行完毕,这就能在有限内存空间内支持大量goroutine,支持了更多的并发,如果需要更多空间,runtime会为goroutine自动分配。
goroutine特点
占用内存更小(几kb)
调度由runtime调度更灵活
goroutine的GMP模型的设计思想
G—>代表go程,M—>代表线程,P—>代表处理器,它包含了运行 goroutine 的资源,如果线程想运行 goroutine,必须先获取 P,P 中还包含了可运行的 G 队列。
在go中,调度器的功能是把可运行的goroutine分配到工作线程上

全局队列(Global Queue):存放等待运行的G
P的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G时,G优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列
P列表:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置)个
M:线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或者从其他本地队列偷一半放到自己的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断的重复
Goroutine 调度器和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行。
关于P和M的个数问题
1、P的数量:
由启动时环境变量 G O M A X P R O C S 或 者 是 由 r u n t i m e 的 方 法 G O M A X P R O C S ( ) 决 定 。 这 意 味 着 在 程 序 执 行 的 任 意 时 刻 只 有 GOMAXPROCS或者是由runtime的方法GOMAXPROCS()决定。这意味着在程序执行的任意时刻只有 GOMAXPROCS或者是由runtime的方法GOMAXPROCS()决定。这意味着在程序执行的任意时刻只有GOMAXPROCS个goroutine在同时运行
2、M的数量:
- go语言本身的限制:go程序启动时,会设置M的最大数量,默认10000。但是内核很难支持这么多的线程数,所以这个限制可以忽略
- runtime/debug中的SetMaxThreads函数,设置M的最大数量
- 一个M阻塞了,会创建新的M
M和P的数量没有绝对关系,一个M阻塞后,P就会创建或者切换另一个M,所以,即使P的默认数量是1,也有可能创建很多个M出来
P和M何时会被创建
- P:在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P
- M:没有足够的M来关联P并运行其中可运行的G。例如所有的M此时全部阻塞,而P中还有很多任务就绪,就会去寻找空闲的M,没有空闲的则会创建新的。
分享一个go爬虫的代码
package main
import (
"fmt"
"github.com/PuerkitoBio/goquery"
"io/ioutil"
"net/http"
"regexp"
"strings"
"sync"
)
const (
reID = `playlist/(\d+)`
)
var waitGroup sync.WaitGroup
func main() {
// 为每个歌单启动go程
// 检查任务任务是否完成,完成则关闭管道
const (
qqUrl = "https://y.qq.com/"
commemtUrl = "https://y.qq.com/n/ryqq/playlist/"
)
musicUrls := make(chan string, 10000)
// 获取所有的歌单ID
musicIDs := getMusicIds(qqUrl)
fmt.Println(musicIDs)
musicIDs = Set(musicIDs)
fmt.Println(musicIDs)
go CheckOk(len(musicIDs), musicUrls)
for _, id := range musicIDs{
waitGroup.Add(1)
go getComments(commemtUrl + id, musicUrls)
}
waitGroup.Wait()
}
func getMusicIds(url string) []string {
musicIds := make([]string, 1, 1)
resp, _ := http.Get(url)
defer resp.Body.Close()
pageBytes, _ := ioutil.ReadAll(resp.Body)
pageStr := string(pageBytes)
re := regexp.MustCompile(reID)
results := re.FindAllStringSubmatch(pageStr, -1)
var id string
for _, result := range results {
id = strings.ReplaceAll(result[1], "playlist/", "")
if id != "" {
musicIds = append(musicIds, id)
}
}
return musicIds[1:]
}
func getComments(url string, chanMusic chan string) {
resp, _ := http.Get(url)
defer resp.Body.Close()
doc, _ := goquery.NewDocumentFromReader(resp.Body)
doc.Find("#app > div > div.main > div.detail_layout > div.detail_layout__main > div.mod_songlist > ul.songlist__list").Each(func(i int, s *goquery.Selection) {
musicName := s.Find("div > div.songlist__songname > span > a ").Text()
musicAlbum := s.Find( "div > div.songlist__artist > a").Text()
musicSong := s.Find( "div > div.songlist__album > a").Text()
fmt.Printf("%d:歌名--%s--专辑--%s--歌手--%s \n", i, musicName, musicAlbum, musicSong)
})
chanMusic <- url
waitGroup.Done()
}
// 任务统计协程
func CheckOk(num int, chanMusic chan string) {
var count int
for {
url := <- chanMusic
fmt.Printf("%s 完成了爬取任务\n", url)
count ++
if count == num {
fmt.Println("全部链接爬取完成")
}
}
}
// 去重
func Set(sli []string) []string {
set := make(map[string]struct{}, len(sli))
j := 0
for _, v := range sli {
_, ok := set[v]
if ok {
continue
}
set[v] = struct{}{}
sli[j] = v
j++
}
return sli[:j]
}
3237

被折叠的 条评论
为什么被折叠?



