2024年C C++最全go语言并发之道-笔记_《go语言并发之道》读书笔记(1),C C++高分面试指南

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

3. 饥饿(hunger)

贪婪的work抢占共享锁以完成整个工作循环,而和平的work则试图只在需要使用的时候才锁定。相同的时间间隔内,和平的work比贪婪的work少处理一半的工作量。

贪婪的work不必要扩大对临界区持有时间,并阻止了和平的work高效工作。

可以通过记录进程速度是否达到预期,检测某个进程是否饥饿。

饥饿会导致程序表现低效。

1.2.5. 确定并发安全

在编写函数的时候,需要对函数做一些注解,提醒使用者需要考虑以下问题:

  • 谁负责并发?
  • 如何利用并发原语解决这个问题的?
  • 谁负责同步?
1.2.6. 面对复杂性的简单性

golang的gc在1.8版本开始,gc暂停一般都是 10~100μs

2.对你的代码建模:通信顺序进程

并发于并行的区别

宣讲概念,容易让人觉得好为人师,矫情的不谦逊。

Erlang作者Joe Armstrong举例子:

swzFE9.jpg

Concurrent

Two queues and one coffee machine.

Parallel

Two queues and two coffee machines.

什么是CSP

communicating sequential processes(通讯顺序进程)。

Go语言CSP:通信顺序进程简述

CSP 并发模型是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享 channel(管道)进行通信的并发模型。

Go语言就是借用 CSP 并发模型的一些概念为之实现并发的,但是Go语言并没有完全实现了 CSP 并发模型的所有理论,仅仅是实现了 process 和 channel 这两个概念。

process 就是Go语言中的 goroutine,每个 goroutine 之间是通过 channel 通讯来实现数据共享。

Go语言的并发哲学

分辨使用传统锁、channel;

sURMA1.png

总结:追求简洁,尽量使用channel,并且认为gorountine的使用是没有成本的。

3.Go语言并发组件

goroutine

开启一个golang的协程。作者8G内存的机器,理论上可以创建数百万个goroutine。而且协程切换上下文的时候,成本比较低。

sync包

WaitGroup

可以把它当作线程安全的计数器。Add函数增加计数,Done函数减少计数,Wait函数阻塞,直到计数为0.

var wg sync.WaitGroup
wg.Add(1)

go func() {
  defer wg.Done()
}()
wg.Wait()

互斥锁和读写锁

互斥锁用于保护临界区资源

var lock sync.Mutex
lock.Lock()
lock.Unlock()

读写锁

var m = sync.RWMutex
m.RLocker()
RWMutex提供了四个方法:

func (*RWMutex) Lock // 写锁定

func (*RWMutex) Unlock // 写解锁

func (*RWMutex) RLock // 读锁定

func (*RWMutex) RUnlock // 读解锁

cond
var locker = new(sync.Mutex)
var cond = sync.NewCond(locker)


func test(x int) {
        cond.L.Lock() //获取锁
        fmt.Println("aaa: ", x)
        cond.Wait()//等待通知  暂时阻塞
        fmt.Println("bbb: ", x)
        time.Sleep(time.Second * 2)
        cond.L.Unlock()//释放锁
}


func main() {
        for i := 0; i < 5; i++ {
                go test(i)
        }
        fmt.Println("start all")
        time.Sleep(time.Second * 1)
        fmt.Println("broadcast")
        cond.Signal()   // 下发一个通知给已经获取锁的goroutine
        time.Sleep(time.Second * 1)
        cond.Signal()// 3秒之后 下发一个通知给已经获取锁的goroutine
        time.Sleep(time.Second * 1)
        cond.Broadcast()//3秒之后 下发广播给所有等待的goroutine
        time.Sleep(time.Second * 10)
        fmt.Println("finish all")

}


once

让一个操作只调用一次,就可以使用这个方式来制作。

package main

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

var once sync.Once

func main() {

	for i, v := range make([]string, 10) {
		once.Do(onces)
		fmt.Println("count:", v, "---", i)
	}
	for i := 0; i < 10; i++ {

		go func() {
			once.Do(onced)
			fmt.Println("213")
		}()
	}
	time.Sleep(4000)
}
func onces() {
	fmt.Println("onces")
}
func onced() {
	fmt.Println("onced")
}

池是对于Pool模式的一种并发安全实现。

对外提供Get方法,如果发现池子里面没有空闲的元素,将会新建一个元素出来。

当使用完成了之后,将会需要调用Put方法将内存归还。

在构建池的时候,需要提供给它一个构造元素的函数。这个函数是线程安全的。

使用场景是这些东西是需要被多个线程共用,而且需求的东西是非常同质化,需要考虑的事情就是new出来,初始化的成本。打个比方:如果需要新建一个db的连接,今后其他的进程还需要使用,这样的情况使用Pool。如果找程序申请一段边长的内存块,那就最好使用new直接搞定。

需要注意的点:

  • 当实例化sync.Pool,使用new方法创建一个成员变量,在调用时是线程安全的。
  • 当你收到了一个来自Get的实例时,不要对所接受的对象的状态做任何的假设。
  • 当你用完了一个Pool中取出来的对象时,一定要调用Put,否则,Pool无法复用这个实例。
  • Pool内的分布必须大致均匀。

channel

channel是由Hoare的CSP派生出来的同步原语之一。

channel通常使用Stream来命名这种变量。

var dataStream chan interface{}
dataStream = make(chan interface{})
var recvChan <-chan interface{}
var sendChan <-chan interface{}
recvChan = dataStream
sendChan = dataStream

<-writeStream
// invaslid operation: <- writeStream (receive from send-only type)

上述的例子是构建了一个双向链表,recvChan和sendChan是为了约束数据的流向,防止在生产者场景下读取了数据。

channel里面读取数据的写法如下:

salutation,ok := <-stringStream

当channel被关闭了,返回的ok也是失败的。

操作Channel状态结果
Readnil阻塞
打开且非空输出值
关闭的<默认值>,false
只读编译报错
Writenil阻塞
打开的但填满阻塞
打开,但不满写入值
关闭的panic
只读编译报错
closenilpanic
打开且非空关闭channel;读取成功,直到通道耗尽,然后读取产生值的默认值
打开但空关闭channel;读到生产者的默认值
关闭的panic
只读编译报错

如果关闭了channel,其实还是会将里面的内容都读取出来的。

	var stringStream chan string
	stringStream = make(chan string, 1)
	var recvStream <-chan string
	var sendStream chan<- string

	recvStream = stringStream
	sendStream = stringStream

	go func() {
		time.Sleep(2 \* time.Second)
		for {
			if val, ok := <-recvStream; ok {
				fmt.Println(val)
			} else {
				fmt.Println("no data")
				break
			}
		}
	}()
	sendStream <- "hh"
	sendStream <- "hh"
	close(stringStream)
	time.Sleep(8 \* time.Second)

对于channel来说,生产者负责发送数据,并且负责销毁。消费者只负责读取,当无法读取的时候,就说明关闭了。

channel是goroutine的黏合剂

select语句

select是将channel绑定到一块的黏合剂。

select将会选择在其语句段内的某个可工作的通道工作一次。

GOMAXPROCES控制

runtime.GOMAXPROCS(runtime.NumCPU())

在1.5之前版本,这个值都是设置成1。后面的版本好像都是已经按照cpu个数来决定多少个线程。2

4.Go语言的并发模型

本章主要是使用3章节中学习到的原语,构建模型出来。

约束

使用词法阅书将channel从生产者传给消费者的时候,只给只读的接口。

for-select循环

按照类似这种结构来组织代码


for {
  select{
    case str <- recvStream:
    // ...
  }
}

for _, s := range[]string{"a","b","c"}{
  select {
    case <-done:
      return
    case stringStream <- e:
  }
}

防止goroutine泄露

goroutine是存在泄露风险,且会造成内存增长。

goroutine有3中情况下种植

完成工作。
由于不可恢复的错误,造成不能工作。
当它被告知需要关闭。

设计原则就是谁创建channel,谁负责将channel关闭。

or-channel

将一个或多个完成的channel合并到一个完成channel,任何channel关闭时自己也关闭。


func main() {
	var or func(channels ...<-chan interface{}) <-chan interface{}
	or = func(channels ...<-chan interface{}) <-chan interface{} { // <1>
		switch len(channels) {
		case 0: // <2>
			return nil
		case 1: // <3>
			return channels[0]
		}

		orDone := make(chan interface{})
		go func() { // <4>
			defer close(orDone)

			switch len(channels) {
			case 2: // <5>
				select {
				case <-channels[0]:
				case <-channels[1]:
				}
			default: // <6>
				select {
				case <-channels[0]:
				case <-channels[1]:
				case <-channels[2]:
				case <-or(append(channels[3:], orDone)...): // <6>
				}
			}
		}()
		return orDone
	}
	sig := func(after time.Duration) <-chan interface{} { // <1>
		c := make(chan interface{})
		go func() {
			defer close(c)
			time.Sleep(after)
		}()
		return c
	}

	start := time.Now() // <2>
	<-or(
		sig(2\*time.Hour),
		sig(5\*time.Minute),
		sig(1\*time.Second),
		sig(1\*time.Hour),
		sig(1\*time.Minute),
	)
	fmt.Printf("done after %v", time.Since(start)) // <3>
}


这样就能将多个channel的结束,合并到一个channel中,任意一个channel结束了就结束。

后续使用"context包"也能做这个事情。

错误处理

错误处理核心问题是“谁负责处理错误”?

谁有全景呈现问题的完整信息,就交付给谁来发起对于错误的报告。

pipeline

不要编写大函数,看待程序应该从两个方面来看待:1.流程;2.处理细节。

构建pipeline的最佳实践

代码中使用了之前防止goroutine泄露写法,防止goroutine无法正常退出。

使用pipeline封装每个stage的处理,可以方便让其能分离出多端独立的逻辑来,然后就能做一些并发的事务了。并且这样做是比较安全的。

一些便利的生成器

这章节的实例也是编写了两个stage来处理生成器,一个负责发生随机字符,另一个控制需要拿多少个。

本章还通过对比测试,其实在多核的时候,并行计算将会更加快速。

扇入,扇出

fan-out,fan-in技术。

本章节其实讨论的问题就是,如何处理多个stage里面不能畅快的跑的问题。

扇入其实就是多个流汇成一个流来处理。

扇出就是将一个流分派给多个流来处理了。

一个处理的pipeline,中间有很重的处理过程,这样只能拓宽这个处理的stage,而负载轻的可以使用少量的stage来处理。

注意:

如果结果到达的顺序不重要,循环独立运行性很重要。

or-done-channel

用于处理已经发起了退出操作,但是channel的数据需要处理完。普通写法比较直观,但是最好还是将代码封装出来,返回一个输出式的channel,外层处理逻辑者比较好写。

tee-channel

类似Unix系统中的tee函数,输入的内容可以在屏幕上输出,并且也输出到一些设定好的文件里面。这种模式其实就是将一份数据并发的分配给两个channel,然后出发后续他们的处理。

桥接channel模式

需要从这个结构里面将其中的channel拿出来,直接写逻辑。使用这个模式就式为了完成这项工作。

<- chan  <- chan  interface{}

里面也使用了orDone方式读取 <- chan interface{}

队列排队

有时候,在队列没有准备好的时候,就开始接受请求很有用,这种情况叫做队列。

队列的真正用途是将stage分离,以便一个stage的运行时间不会影响另一个stage的运行时间。以这种方式解耦stage,然后级联以改变整个系统的运行时行为。

这里文中举了个例子,写文件io的,先大量的调用bufio.Writer将内容写入到缓冲区,直到累积到一定程度开始写入硬盘。这个速度提升大概有3倍。但是这样有一些让内存消耗大一些。

利特尔法则

  • L = 系统中平均负载数。
  • λ

\lambda

λ = 负载的平均到达率。

  • W = 负载在系统中花费的平均时间。

L =

λ

W

\lambda W

λW

这个等式应用于稳定系统,稳定系统的定义就是输入管道的速率和输出的速率相等。

n

L

=

λ

n

W

nL = \lambda nW

nL=λnW

L

=

λ

i

=

1

W

i

L = \lambda \sum_{i=1}^{\infty} Wi

L=λ∑i=1∞​Wi

通过利特尔法则,我们已经证明了队列不会有助于减少在系统中所花费的时间。你的管道只会和最慢的stage一样快。

利特尔法则无法预知处理请求的失败。

队列可能会很有用,但是它是复杂的,作者建议作为最后的优化手段。

context包

由于某种原因(超时,或者强制退出)我们希望中止这个goroutine的计算任务,那么就用得到这个Context了。

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

n

W

nL = \lambda nW

nL=λnW

L

=

λ

i

=

1

W

i

L = \lambda \sum_{i=1}^{\infty} Wi

L=λ∑i=1∞​Wi

通过利特尔法则,我们已经证明了队列不会有助于减少在系统中所花费的时间。你的管道只会和最慢的stage一样快。

利特尔法则无法预知处理请求的失败。

队列可能会很有用,但是它是复杂的,作者建议作为最后的优化手段。

context包

由于某种原因(超时,或者强制退出)我们希望中止这个goroutine的计算任务,那么就用得到这个Context了。

[外链图片转存中…(img-iKK97t2M-1715527188524)]
[外链图片转存中…(img-5HW9Uvsr-1715527188524)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

  • 13
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值