goroutine

文章详细介绍了Go语言中的并发与并行概念,以及协程(goroutine)与主线程的区别。通过实例展示了goroutine的创建和运行,强调了Go协程的轻量级特性。在处理并发问题时,由于多个协程可能对共享资源(如map)造成竞态条件,文章提到了使用互斥锁(mutex)来解决并发写入的问题。此外,还讨论了如何设置CPU个数以优化程序性能,并指出channel是更优的协程间通信方式。
摘要由CSDN通过智能技术生成

1.基础知识

并发与并行

(从线程与CPU的关系角度分析)
并发:多个线程在单核上运行,宏观上同时运行,微观上某一时刻只有一个线程在运行
并行:多线程在多核上运行,无论宏观上还是微观上,某一时刻有多个线程在同时运行

进程和线程

进程:程序在操作系统中一次执行过程,系统进行资源分配和调度的基本单位
线程:进程的一个执行实例,是程序执行的最小单元,比进程更小的能独立运行的基本单位
一个进程创建和销毁多个线程,同一个进程中的线程可以并发执行

go协程(goroutine)与go主线程

一个Go主线程(线程或进程)可以起多个协程;

主线程是物理线程,直接对CPU资源划分,耗费CPU;

协程是主线程开启的轻量级线程,属于逻辑态,资源消耗小,不直接作用CPU,可以轻松开启上万个协程;

  • 协程的数量与运行效率提升也不是完全成正比,因为CPU资源与运算速率也是有限的,当CPU跑满之后,开启再多的线程也无法提升效率。这也是golang的一种并发优势。其他编程语言一般是基于线程的。

  • 运行时主线程与协程同时执行(多核情况下并行,单核情况下,并发) 主线程退出了,即使协程还没有执行完毕,也会退出

go协程特点

  1. 协程–轻量级的线程
  2. 调度由用户控制:
    用户可以控制协程之间的调度顺序(关系),而线程之间的调度关系是由操作系统控制的,用户无法操控
  3. 有独立的栈空间
  4. 共享程序堆空间

goroutine退出机制

主线程退出,协程退出
协程执行完毕后自己退出

goroutine的MPG(执行模式

G: Goroutine,即我们在 Go 程序中使用 go 关键字创建的执行体;
M: Machine,或 worker thread,即传统意义上进程的线程;
P: Processor,即一种人为抽象的、用于执行 Go 代码被要求局部资源。只有当 M 与一个 P 关联后才能执行 Go 代码。除非 M 发生阻塞或在进行系统调用时间过长时,没有与之关联的 P。
https://golang.design/under-the-hood/zh-cn/part2runtime/ch06sched/mpg/

当一个协程G0在主线程M0上出现阻塞时,会重新创建一个新的主线程M1(或从已有的线程池中取出一个线程M1)执行后面的协程G1,这种调度模式,可以让G0执行,同时避免了后面的协程阻塞,实现并发/并行。

goroutine运行范例

package main
import (
	"fmt"
	"strconv"
	"time"
)

// 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1秒输出 "hello,world"
// 在主线程中也每隔一秒输出"hello,golang", 输出10次后,退出程序
// 要求主线程和goroutine同时执行

//编写一个函数,每隔1秒输出 "hello,world"
func test() {
	for i := 1; i <= 10; i++ {
		fmt.Println("tesst () hello,world " + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

func main() {

	go test() // 开启了一个协程

	for i := 1; i <= 10; i++ {
		fmt.Println(" main() hello,golang" + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}
// output
 main() hello,golang1
tesst () hello,world 1
tesst () hello,world 2
 main() hello,golang2
tesst () hello,world 3
 main() hello,golang3
 main() hello,golang4
tesst () hello,world 4
tesst () hello,world 5
tesst () hello,world 8
tesst () hello,world 9
 main() hello,golang9
 main() hello,golang10
tesst () hello,world 10

设置CPU个数

package main

import (
	"fmt"
	"runtime"
)

func main() {
	cpuNum := runtime.NumCPU()
	fmt.Println("cpuNum=", cpuNum)

	//可以自己设置使用多个cpu
	runtime.GOMAXPROCS(cpuNum - 1)
	fmt.Println("ok")
}

2. 实例:goroutine实现 1-50 的各个数的阶乘的计算

需求:现在要计算 1-50 的各个数的阶乘,并且把各个数的阶乘放入到map中, 最后显示出来。要求使用goroutine完成

思路:

  1. 编写一个函数,来计算各个数的阶乘,并放入到 map中.
  2. 我们启动的协程多个,统计的将结果放入到 map中
  3. map 应该做出一个全局的.
package main

import (
	"fmt"
	"sync"
)

var (
	myMap = make(map[int]int, 10)
)

// test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) {

	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}

	//这里我们将 res 放入到myMap
	myMap[n] = res  //出现错误 concurrent map writes?  多个协程对map空间并发写入,

}

func main() {

	// 开启多个协程完成任务[50个]
	for i := 1; i <= 50; i++ {
		go test(i)
	}

	//这里我们输出结果,变量这个结果
	for i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}
}

不会有任何输出
原因:协程未执行完毕时,主线程已经退出,所以没有任何输出
解决方案1:加锁,如下所示

// 方案1:加锁
package main

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

var (
	myMap = make(map[int]int, 10)
	//声明一个全局的互斥锁
	//lock 是一个全局的互斥锁,
	//sync 是包: synchornized 同步
	Mutex : 是互斥
	lock sync.Mutex
)

// test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) {

	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}

	//这里我们将 res 放入到myMap
	//加写锁
	lock.Lock()
	myMap[n] = res //concurrent map writes?
	解锁
	lock.Unlock()
}

func main() {

	// 开启多个协程完成任务[50个]
	for i := 1; i <= 50; i++ {
		go test(i)
	}
	
	//休眠5秒钟
	time.Sleep(time.Second * 5)

	//加读锁;为什么?
	lock.Lock()

	//这里我们输出结果,变量这个结果
	for i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}
	lock.Unlock()

}

如果不加写锁,将不会有有任何输出,协程没有执行完毕,主线程就执行结束,所以协程也跟着结束
func main 不加读,运行 go build -race main.go
结果出现数据竞争 :Found 2 data race(s) (示例如下)
原因分析:main函数读取部分为什么需要加互斥锁,按理说5秒数上面的协程都应该执行完,后面就不应该出现资源竞争的问题了,但是在实际运行中,还是可能在红框部分出现(运行时增加-race参数,确实会发现有资源竞争问题),因为我们程序从设计上可以知道5秒就执行完所有协程,但是主线程并不知道,因此底层可能仍然不断尝试读取,从而出现资源争夺,因此加入互斥锁即可解决问题

不添加读锁,仍然会有数据竞争

//不添加读锁,仍然会有数据竞争
map[27]=-5483646897237262336
map[29]=-7055958792655077376
map[36]=9003737871877668864
map[48]=-5844053835210817536
map[24]=-7835185981329244160
map[18]=6402373705728000
map[21]=-4249290049419214848
map[22]=-1250660718674968576
map[25]=7034535277573963776
map[38]=4789013295250014208
map[42]=7538058755741581312
map[49]=8789267254022766592
map[14]=87178291200
map[50]=-3258495067890909184
map[15]=1307674368000
map[19]=121645100408832000
map[26]=-1569523520172457984
map[28]=-5968160532966932480
map[41]=-2894979756195840000
map[43]=-7904866829883932672
map[44]=2673996885588443136
map[2]=2
map[46]=1150331055211806720
map[11]=39916800
map[16]=20922789888000
map[33]=3400198294675128320
map[34]=4926277576697053184
map[7]=5040
map[13]=6227020800
map[23]=8128291617894825984
map[30]=-8764578968847253504
map[35]=6399018521010896896
map[47]=-1274672626173739008
map[8]=40320
map[6]=720
map[10]=3628800
map[3]=6
map[4]=24
map[5]=120
map[20]=2432902008176640000
map[31]=4999213071378415616
map[32]=-6045878379276664832
map[37]=1096907932701818880
map[39]=2304077777655037952
map[1]=1
map[45]=-8797348664486920192
map[40]=-70609262346240000
map[12]=479001600
map[17]=355687428096000
map[9]=362880
Found 2 data race(s)
exit status 66

同时添加读写锁,无竞争冲突

//同时添加读写锁,无竞争冲突
map[8]=40320
map[21]=-4249290049419214848
map[17]=355687428096000     
map[13]=6227020800          
map[39]=2304077777655037952 
map[5]=120                  
map[25]=7034535277573963776 
map[38]=4789013295250014208 
map[29]=-7055958792655077376
map[40]=-70609262346240000  
map[44]=2673996885588443136 
map[3]=6                    
map[7]=5040                 
map[24]=-7835185981329244160
map[26]=-1569523520172457984
map[35]=6399018521010896896 
map[47]=-1274672626173739008
map[33]=3400198294675128320 
map[27]=-5483646897237262336
map[1]=1                    
map[2]=2                    
map[10]=3628800             
map[15]=1307674368000       
map[20]=2432902008176640000 
map[19]=121645100408832000  
map[42]=7538058755741581312
map[46]=1150331055211806720
map[4]=24
map[16]=20922789888000
map[14]=87178291200
map[32]=-6045878379276664832
map[31]=4999213071378415616
map[28]=-5968160532966932480
map[36]=9003737871877668864
map[34]=4926277576697053184
map[9]=362880
map[11]=39916800
map[12]=479001600
map[22]=-1250660718674968576
map[23]=8128291617894825984
map[41]=-2894979756195840000
map[6]=720
map[18]=6402373705728000
map[30]=-8764578968847253504
map[43]=-7904866829883932672
map[45]=-8797348664486920192
//没有数据竞争,但存在数据溢出

加锁的主要目的是为了解决协程与主线程的通讯问题,那么,如何让主程序自动判定协程是否结束?channel是更好的解决方案。

参考: 【尚硅谷】Golang入门到实战教程丨一套精通GO语言 P264–P271

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值