Go语言goroutine笔记

一、基本概念

在学习Goroutine编程思想之前,先来了解几个关键的概念:

1. 进程和线程

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

2. 并发和并行

  1. 多线程程序在一个核的CPU上运行,就是并发
  2. 多线程程序在多核CPU上运行,就是并行

3. 线程和协程

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

4. 互斥锁和读写锁

写比较频繁的时候,我们用互斥锁。

读比较频繁的时候,我们用读写锁。

5. 实践

先说一下个人理解,我们将会写一个demo,创建一个协程,而我们运行程序是跑在一个线程上的,换句话说,我们写的线程和协程是同时,并发的去运行的,所以有可能会出现一个情况,主线程结束了,但是协程还没有结束,缺被强行终止了。

写个demo看一下:

func main() {
	go test()
}

func test() {
	var i int
	for{
		fmt.Print(i)
		time.Sleep(time.Second)
	}
}

运行之后是什么都不打印出来,原因很简单,主线程结束的时候,协程还没来得及去打印。

我们如果想让他打印出来,可以在主线程加一个sleep操作也可以。

6. goRoutine之间的通信

那么,如果我们有多个goRoutine要怎么进行通信操作呢?

方法有两个:

  1. 全局变量和锁同步

我们先不写锁,先把程序解释一下,我们要实现的是一个阶乘,现在要把数字n和n的阶乘存放进map里,将map设置为全局变量,方便添加参数。

//设置全局变量
var(
	m=make(map[int]uint64)
)

type task struct{
	n int
}

func calc(t *task){//计算阶乘结果
	var sum uint64
	sum=1
	for i:=1;i<t.n;i++{
		sum *=uint64(i)
	}
	m[t.n] = sum
}
func main() {
	for i:=0;i<10;i++{
		t := &task{n:i}
		go calc(t)
	}
	time.Sleep(time.Second*10)
	for k,v := range m  {
		fmt.Printf("%d → %v \n",k,v)
	}
}

然后运行程序报错:

在这里插入图片描述

意思是无法并发的对map进行写入操作。这是因为我们让协程都同时对一个map进行的写操作。所以我们要修改这个bug,就要加锁,一次只用一个协程去写,就不会错了。修改之后的程序如下,其实就是加了加锁和去锁的代码:

//设置全局变量
var(
	m=make(map[int]uint64)
	lock sync.Mutex
)

type task struct{
	n int
}

func calc(t *task){
	var sum uint64
	sum=1
	for i:=1;i<t.n;i++{
		sum *=uint64(i)
	}
	lock.Lock()
	m[t.n] = sum
	lock.Unlock()
}
func main() {
	for i:=0;i<10;i++{
		t := &task{n:i}
		go calc(t)
	}
	time.Sleep(time.Second*10)
	lock.Lock()
	for k,v := range m  {
		fmt.Printf("%d → %v \n",k,v)
	}
	lock.Unlock()
}

  1. Channel

先说说channel的概念:

(1)类似unix的管道(pipe)
(2)先进先出
(3)线程安全,多个goroutine同时访问,不需要加锁
(4)channel是有类型的,一个整数的channel只能存放整数

channel的声明方式为:var 变量名 chan 类型

然后也写一个demo来看

比如我们要创建一个存储map类型的channel

func main() {
	var mapChannel chan map[string]string
	//初始化channel
	mapChannel = make(chan map[string]string,10)
	m:=make(map[string]string)
	m["1214"] = "SB"
	m["SB"] = "4121"
	mapChannel <- m
}

然后我们再创建一个结构体,然后根据结构体创建一个相关的channel

type student struct {
	name string
}

func main() {
	var stuChan chan student
	stuChan = make(chan student,10)
	stu:=student{name:"ljy"}
	stuChan <- stu
}

也可以这么写

type student struct {
	name string
}

func main() {
	var stuChan chan *student
	stuChan = make(chan *student,10)
	stu:=student{name:"ljy"}
	stuChan <- &stu
}

以上都是对将数据写入channel的写法,然后再来说明一下从channel中读数据的写法

a:=<-stuChan

7.GoRoutine与Channel结合

先说一下,我们的目的,现在我们有多个协程,而且多个协程之间是相互有关联的,我们可以想成一个生产者和消费者模型来理解。

我们的goroutine意在是让主线程连同多个协程异步执行。而Channel的意义在于让多个协程之间,搭配着执行。

而难点就在于,我们要如何实现相互搭配着执行,比如,生产后消费,没有消费的就阻塞,等待生产这种。

下面就写一个生产者消费者的demo

我们将生产的数据,放入channel,然后让消费者去读

func readChannel(ch chan int) {
	for{
		var b int
		b =<-ch
		fmt.Printf("get data: %d\n",b)
	}
}

func writeChannel(ch chan int) {
	for i := 0; i < 10; i++ {
		fmt.Printf("put data: %d\n",i)
		ch <-i
	}
}

func main() {
	intChan :=make(chan int,10)
	go writeChannel(intChan)
	go readChannel(intChan)

	time.Sleep(time.Second * 10)
}

写成这样就结束了,但是这只是循环了10个数,而且channel的容量也设置的是10,假如我们的生产速度的百万,那么就会出现,去消费还没来得及生产的数据或者消费的速度赶不上生产,生产了10个数据之后阻塞的情况。

这就需要我们去设置一个channel的阻塞功能。

之前我们实现阻塞,主要是通过lock去实现的,但是lock的弊端也很明显,我们无法灵活控制锁。

现在我们将channel的容量改为100,但是数据的产生上限改为100,还是使用生产者-消费者模型去计算1-100的质数,生产数据之后,对数据进行计算,然后存入另一个channel

func calculator(intChan chan int, resultChan chan int) {
	for v:= range intChan{
		flag:=true
		for i:=2;i<v;i++{
			if v%i == 0{
				flag=false
				break
			}
		}
		if flag{
			resultChan <- v
		}
	}

}
func main() {
	intChan := make(chan int,100)
	resultChan := make(chan int,100)
	for i := 0; i < 100; i++ {
		intChan <- i
	}
	for j := 0; j < 8; j++ {
		go calculator(intChan,resultChan)
	}

	go func() {
		for i:=range resultChan{
			fmt.Println(i)
		}
	}()
	time.Sleep(time.Second * 10)
}
发布了296 篇原创文章 · 获赞 53 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_41936805/article/details/104117553