Go~并发编程runtime、workerPool、timer、ticker、sync、atomic

前言

并发的一些概念

进程和线程

  • A. 进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
    
  • B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
    • C.一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。

并发和并行

  • A. 多线程程序在一个核的cpu上运行,就是并发。
  • B. 多线程程序在多个核的cpu上运行,就是并行。
    在这里插入图片描述

协程和线程

  • 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
  • 线程:一个线程上可以跑多个协程,协程是轻量级的线程。

goroutine 只是由官方实现的超级"线程池"。
每个实力4~5KB的栈内存占用和由于实现机制而大幅减少的创建和销毁开销是go高并发的根本原因

goroutine 奉行通过通信来共享内存,而不是共享内存来通信。

  • 对于协程这里就不过多介绍了

runtime包

runtime.Gosched()

让出CPU时间片,重新等待安排任务

  • Gosched使当前go程放弃处理器,以让其它go程运行。它不会挂起当前go程,因此当前go程未来会恢复执行。

runtime.Goexit()

退出当前协程

func main() {
	go func() {
		defer fmt.Println("A.defer")
		func() {
			defer fmt.Println("B.defer")
			// 结束协程
			runtime.Goexit()
			defer fmt.Println("C.defer")
			fmt.Println("B")
		}()
		fmt.Println("A")
	}()

	
	for {
	}
}

B.defer
A.defer

  • 在runtime.Goexit()结束协程后,先执行压入defer栈的fmt.Println(“B.defer”),后执行defer栈的defer fmt.Println(“A.defer”)
  • 这里注意区分第二个func还是由协程去执行的,注意区分下面这种写法,好好体会一下,也证明说一个协程的defer栈是属于函数级别的
func main() {
	go func() {
		defer fmt.Println("A.defer")
		 go func() {
			defer fmt.Println("B.defer")
			// 结束协程
			//runtime.Goexit()
			defer fmt.Println("C.defer")
			fmt.Println("B")
		}()

		fmt.Println("A")
	}()


	for {
	}
}

B
C.defer
B.defer
A
A.defer

runtime.GOMAXPROCS

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

func a() {
    for i := 1; i < 10; i++ {
        fmt.Println("A:", i)
    }
}

func b() {
    for i := 1; i < 10; i++ {
        fmt.Println("B:", i)
    }
}

func main() {
    runtime.GOMAXPROCS(1)
    go a()
    go b()
    time.Sleep(time.Second)
}

B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9

如果加上runtime.gosched。就会出现下面的这种情况

func a() {
	for i := 1; i < 10; i++ {
		runtime.Gosched()
		fmt.Println("A:", i)
	}
}

func b() {
	for i := 1; i < 10; i++ {
		runtime.Gosched()
		fmt.Println("B:", i)
	}
}

func main() {
	runtime.GOMAXPROCS(1)
	go a()
	go b()
	time.Sleep(time.Second)
}

B: 1
A: 1
B: 2
A: 2
B: 3
A: 3
B: 4
A: 4
B: 5
A: 5
B: 6
A: 6
B: 7
A: 7
B: 8
A: 8
B: 9
A: 9

Go语言中的操作系统线程和goroutine的关系:

1.一个操作系统线程对应用户态多个goroutine。
2.go程序可以同时使用多个操作系统线程。
3.goroutine和OS线程是多对多的关系,即m:n。

Goroutine池(Pool)

worker pool(goroutine池)

本质上是生产者消费者模型
可以有效控制goroutine数量,防止暴涨

  • 比如实现下面的需求

计算一个数字的各个位数之和,例如数字123,结果为1+2+3=6
随机生成数字进行计算

type Job struct {
	Id int
	RandNum int
}

type Result struct {
	Job *Job
	Sum int
}

func main() {
	// 需要2个管道
	// 1.job管道
	jobChan := make(chan *Job, 128)
	// 2. result管道
	resultChan := make(chan *Result, 128)
	// 3. 创建工作池
	createWorkerPool(64, jobChan, resultChan)
	// 4.开个打印的协程
	go func(resultChan  <- chan *Result) {
		// 遍历结果管道打印
		for result := range resultChan {
			fmt.Printf("job id:%v randnum:%v result:%d\n", result.Job.Id,
				result.Job.RandNum, result.Sum)
		}
	}(resultChan)

	var id = 0
	// 循环创建job,输入到管道
	for  {
		job := Job{
			Id:      id,
			RandNum: rand.Int(),
		}
		jobChan <- &job
		id++
	}
}

// 创建工作池
// 参数1:开几个协程
func createWorkerPool(goroutineNum int, jobChan chan *Job, resultChan chan *Result)  {
	for i := 0; i < goroutineNum; i++ {
		// 根据开协程个数,去跑运行
		go func(jobChan <-chan *Job, resultChan chan<- *Result) {
			// 执行运算
			// 遍历job管道所有数据,进行相加
			for job := range jobChan {
				// 随机数接过来
				r_num := job.RandNum
				// 随机数每一位相加
				// 定义返回值
				var sum int
				for r_num != 0 {
					tmp := r_num % 10
					sum += tmp
					r_num /= 10
				}
				// 想要的结果是Result
				r := &Result{
					Job: job,
					Sum: sum,
				}
				//运算结果扔到管道
				resultChan <- r
			}
		}(jobChan, resultChan)
	}
}

job id:781815 randnum:6453223047687343154 result:77
job id:781816 randnum:6918599203160232304 result:73
job id:781817 randnum:1718977818466032997 result:103
job id:781818 randnum:4200694634289817021 result:76
job id:781819 randnum:434379070686837522 result:84
job id:781820 randnum:3237239547880555746 result:93
job id:781821 randnum:1690978111853846622 result:87
job id:781822 randnum:6968134001245023126 result:63

定时器

Timer(只执行一次)

时间到了执行,只执行1次

func main() {
	test4()
	for  {

	}
}
func test1()  {
	// 1.timer基本使用 延时俩秒
	timer1 := time.NewTimer(2 * time.Second)
	t1 := time.Now()
	fmt.Printf("t1:%v\n", t1)
	t2 := <-timer1.C
	fmt.Printf("t2:%v\n", t2)
}
func test2()  {
	// 2.验证timer只能响应1次
	timer2 := time.NewTimer(time.Second)
	for {
		// 第一次考验正常执行,第二次就会发生deadlock
	 <-timer2.C
	 fmt.Println("时间到")
	}
}
func test3()  {
	// 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秒到")
}
func test4()  {
	// 4.停止定时器
	timer4 := time.NewTimer(2 * time.Second)
	go func() {
		fmt.Println("进入func")
		<-timer4.C
		fmt.Println("定时器开始执行")
	}()
	//time.Sleep(2*time.Second)
	b := timer4.Stop()
	if b {
	 fmt.Println("timer4已经关闭")
	} else {
		fmt.Println("timer4无法关闭")
	}
}
func test5()  {
	// 5.重置定时器
	timer5 := time.NewTimer(3 * time.Second)
	timer5.Reset(1 * time.Second)
	fmt.Println(time.Now())
	fmt.Println(<-timer5.C)
}

多去体会上述代码的输出即可

Ticker(时间到了多次执行)

时间到了,多次执行

func main() {
	// 1.获取ticker对象
	ticker := time.NewTicker(1 * time.Second)
	i := 0
	// 子协程
	go func() {
		for {
			//<-ticker.C
			i++
			fmt.Println(<-ticker.C)
			if i == 5 {
				//停止
				ticker.Stop()
			}
		}
	}()

	for {
	}
}

并发安全和锁(Sync)

有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问题(数据竞态)。

sync.WaitGroup

在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务的同步。 sync.WaitGroup有以下几个方法:
在这里插入图片描述
sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

func hello() {
	defer wgroup.Done()
	fmt.Println("Hello Goroutine!")
	time.Sleep(time.Second) // 假设这个任务耗时1秒
}
func main() {
	wgroup.Add(1)
	go hello() // 启动另外一个goroutine去执行hello函数
	fmt.Println("main goroutine done!")
	wgroup.Wait()
	fmt.Println("goroutine done!")
}

互斥锁(sync.Mutex)

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型来实现互斥锁。

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)
}

读写互斥锁(sync.RWMutex)

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。

读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

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))
}

惰性初始化(sync.Once)

在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等

从加载方面的意义上来说他是懒汉形式的单例模式的实现

Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once。

sync.Once只有一个Do方法,其签名如下:

func (o *Once) Do(f func()) {}

注意:如果要执行的函数f需要传递参数就需要搭配闭包来使用。

加载配置文件示例

延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作就不是必须要做的。我们来看一个例子:

var icons map[string]image.Image

func loadIcons() {
	icons = map[string]image.Image{
		"left":  loadIcon("./left.png"),
		"up":    loadIcon("./up.png"),
		"right": loadIcon("./right.png"),
		"down":  loadIcon("./down.png"),
	}
}

func loadIcon(photoPath string) image.Image {
	// 打开文件
	file, err := os.Open(photoPath)
	if err != nil {
		log.Fatal("open file error")
		return nil
	}
	// 关闭
	defer file.Close()
	// 创建一个带缓冲区的Reader
	reader := bufio.NewReader(file)
	decode, _, _ := image.Decode(reader)
	return decode
}

// Icon 被多个goroutine调用时不是并发安全的,而且会出现重复反复多次加载消耗系统性能
func Icon(name string) image.Image {
	if icons == nil {
		loadIcons()
	}
	return icons[name]
}

有俩个问题:

  1. 会出现多次重复文件加载,很耗资源
  2. 在这种情况下就会出现即使判断了icons不是nil也不意味着变量初始化完成了

考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons的时候不会被其他的goroutine操作,但是这样做还是会有性能问题。

使用sync.Once改造的示例代码如下:

var loadIconsOnce sync.Once
// Icon 是并发安全的
func Icon(name string) image.Image {
	loadIconsOnce.Do(loadIcons)
	return icons[name]
}

sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成。这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

闭包下的惰性加载

如果有需求,比如加载的时候需要参数,那就得使用闭包了。

sync.Map

Go语言中内置的map不是并发安全的。请看下面的示例:

var m = make(map[string]int)

func get(key string) int {
    return m[key]
}

func set(key string, value int) {
    m[key] = value
}

func main() {
    wg := sync.WaitGroup{}
    for i := 0; i < 20; i++ {
        wg.Add(1)
        go func(n int) {
            key := strconv.Itoa(n)
            set(key, n)
            fmt.Printf("k=:%v,v:=%v\n", key, get(key))
            wg.Done()
        }(i)
    }
    wg.Wait()
}

上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。

像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。

  • 开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。
  • 同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。
var syncMap = sync.Map{}

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 20; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			syncMap.Store(key, n)
			value, _ := syncMap.Load(key)
			fmt.Printf("k=:%v,v:=%v\n", key, value)
			wg.Done()
		}(i)
	}
	wg.Wait()

	syncMap.Range(func(key, value interface{}) bool {
		fmt.Printf("key:%v value:%v\n", key, value)
		return true
	})
}

原子操作(atomic包)

代码中的加锁操作因为涉及内核态的上下文切换会比较耗时、代价比较高。

  • 针对基本数据类型我们还可以使用原子操作来保证并发安全,因为原子操作是Go语言提供的方法它在用户态就可以完成,因此性能比加锁操作更好。Go语言中原子操作由内置的标准库sync/atomic提供。

atomic包

在这里插入图片描述
我们填写一个示例来比较下互斥锁和原子操作的性能。

var num int64
var l sync.Mutex
var wGroup sync.WaitGroup

// 普通版加函数
func add() {
	// x = x + 1
	num++ // 等价于上面的操作
	wGroup.Done()
}

// 互斥锁版加函数
func mutexAdd() {
	l.Lock()
	num++
	l.Unlock()
	wGroup.Done()
}

// 原子操作版加函数
func atomicAdd() {
	atomic.AddInt64(&num, 1)
	wGroup.Done()
}

func main() {
	start := time.Now()
	for i := 0; i < 10000; i++ {
		wGroup.Add(1)
		 go add()       // 普通版add函数 不是并发安全的
	}
	wGroup.Wait()
	end := time.Now()
	fmt.Println(num)
	fmt.Println("1: " + end.Sub(start).String())

	num = 0
	start = time.Now()
	for i := 0; i < 10000; i++ {
		wGroup.Add(1)
		 go mutexAdd()  // 加锁版add函数 是并发安全的,但是加锁性能开销大
	}
	wGroup.Wait()
	end = time.Now()
	fmt.Println(num)
	fmt.Println("2: " + end.Sub(start).String())

	num = 0
	start = time.Now()
	for i := 0; i < 10000; i++ {
		wGroup.Add(1)
		go atomicAdd() // 原子操作版add函数 是并发安全,性能优于加锁版
	}
	wGroup.Wait()
	end = time.Now()
	fmt.Println(num)
	fmt.Println("3: " + end.Sub(start).String())
}

9688
1: 3.7994ms
10000
2: 2.615ms
10000
3: 2.634ms

atomic包提供了由底层操作系统提供的原子级内存操作,可在用户态进行关中断式的原子级别计算和比较,对于同步算法的实现很有用。但这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值