学go必会的并发知识(1)

go并发

终于走到并发了,一搞这就是第九天了,并发作为go最迷人的地方,一定要把基础打的牢牢的呀
再说回来哈,go的并发是由 goroutine实现的,他是属于 用户态, 开发者大哥写的(站在巨人的肩膀上就是爽)
要记下的是goroutine是运行时(runtime)进行调度完成的,而线程是由操作系统完成的哦。
go里面还设置了管道 channel 用于 多个goroutine之间的通信。

goroutine的使用

  • go开启多线程真的是巨简单,直接使用 go + 运行的函数后者方法即可
  • 举个例子

在这里插入图片描述

  • 从运行结果可以看出,并发是很成功的。啊哈哈哈哈哈哈
  • 使用方法,直接在前面加 go 就行了,超级方便
  • 然后我们看看匿名方法的并发。

func main() {
	for i := 0; i < 10; i++ {
		go func(i int) {
			fmt.Println(i)
		}(i)
	}
	time.Sleep(2 * 1000)
}

  • 舒服吧,里面的方法,相当于java里面的run方法体了
  • 我们可以看到哈,我加了个time.sleep去等待这个多线程搞完,那是因为for循环比较快,搞完main函数就结束了,里面的多线程就不会去打印了。所以需要去等,那有木有更优雅一点的方式呢?
  • 是有的,我们可以使用sync.WaitGroup来实现goroutine的同步问题
package main

import (
	"fmt"
	"sync"
)

var mg sync.WaitGroup

func main() {
	for i := 0; i < 10; i++ {
		mg.Add(1)
		go func(i int) {
			mg.Done()
			fmt.Println(i)
		}(i)
	}
	mg.Wait()
}

  • 如此,我们也能实现多线程的执行。go里面的多线程就是这么简单
  • 那么我们来个问题!

goroutine什么时候结束?

goroutine对应的函数结束了,goroutine就结束了
main函数执行完了,由main函数创建的那些goroutine就都结束了

并发可以配合随机数来玩,啊哈哈和,一起来看看吧

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

var mg sync.WaitGroup

func f() {
	defer mg.Done()   // 记得最后会自动执行哦 
	rand.Seed(time.Now().UnixNano())
	for i := 0; i < 5; i++ {
		r1 := rand.Int()    // int64
		r2 := rand.Intn(10) // 固定大小
		fmt.Println(r1, r2)
	}
}

func main() {
	mg.Add(1)
	go f()
	mg.Wait()
}

  • 记得加随机因子哦!!!



那么 goroutine 和 线程 有什么关系呢?

  • goroutine是用户态的线程,就是自己实现的线程,而平时说的线程指的是操作系统的线程OS线程。
  • 他们的栈不同,OS线程一般是固定的栈内存(2MB),一个goroutine栈在最开始的时候是最小的情况下只有2kb,goroutine的栈不是固定的,他可以按照需求来进行增大和缩小,最大可以到达1GB,因为开始的时候只需要2KB就可以运行,所以我们搞几十万个都是没有问题的



goroutine调度问题

GMP是 go语言运行时(runtime)层面的实现,是go语言自己实现的一套的调度系统,区别于操作系统调度os线程。

  • G很好理解,就是个goroutine的,里面除了存放本goroutine信息外 还有与所在P的绑定等信息。
  • P管理着一组goroutine队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址及地址边界),P会对自己管理的goroutine队列做一些调度(比如把占用CPU时间较长的goroutine暂停、运行后续的goroutine等等)当自己的队列消费完了就去全局队列里取,如果全局队列里也消费完了会去其他P的队列里抢任务。
  • M(machine)是Go运行时(runtime)对操作系统内核线程的虚拟, M与内核线程一般是一一映射的关系, 一个groutine最终是要放到M上执行的;
var mg sync.WaitGroup

func a() {
	defer mg.Done()
	for i := 0; i < 100; i++ {
		fmt.Printf("A:%d\n", i)
	}
}

func b() {
	defer mg.Done()
	for i := 0; i < 100; i++ {
		fmt.Printf("B:%d\n", i)
	}
}

func main() {
	fmt.Println(runtime.NumCPU()) //cpu逻辑核心数,你有多少个cpu
	mg.Add(2)
	go b()
	go a()
	mg.Wait()
}

操作系统线程和goroutine的关系:

  • 一个操作系统可以对应用户态多个goroutine
  • go语言可以同时使用多个操作系统线程
  • 所以他们的关系是 多对多的 , 即: m:n

channel

单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

  • Go语言的并发模型是CSP(Communicating Sequential Processes),提倡通过通信共享内存而不是通过共享内存而实现通信。
  • 如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

channel是一种类型,一种引用类型。声明通道类型的格式如下:

var c chan int

func main() {
	fmt.Println(c)			// nil
	c = make(chan int, 10)  // 带数字就是定义了缓存区
	fmt.Println(c)			// 指针
}

  • channel类型必须要进行初始化才可以使用,不然通道就会为nil

channel通道的三种操作


发送操作
  • 将一个值发送到通道中
ch <- 10   // 发送

接收操作
  • 从一个通道中接收值
x := <- ch  // 从ch中接收值并赋值给x
<- ch   	// 从ch中接收值,但是忽悠结果

关闭操作
  • 通过调用内置的close方法进行关闭
close(ch)  // 关闭通道

然后来实验一下吧!

var ch chan int
var mg sync.WaitGroup

func main() {
	mg.Add(1)
	ch = make(chan int, 10)
	go func() {
		defer mg.Done()
		x := <-ch
		fmt.Println("从后台通道里面取到了数据x:", x)
	}()
	ch <- 10
	fmt.Println("数据10发送到了通道里面!", ch)
	mg.Wait()
}

然后我们可以利用这些做个小题目了,

  • 先放入通道1 里面 100 个数据,再在通道2里面存放这一百个数据的平方,最后打印出来
var ch chan int
var ch2 chan int

func main() {
	ch = make(chan int, 10)
	ch2 = make(chan int, 10)
	go func() {
		for i := 0; i < 100; i++ {
			ch <- i
		}
		close(ch)
	}()
	go func() {
		for {
			v, ok := <-ch // 通道在关闭后再取值,会返回false
			if !ok {
				break
			}
			x := v * v
			ch2 <- x
		}
		close(ch2)
	}()
	for i := range ch2 {
		fmt.Println(i)
	}
}

  • 结果很成果哈,但是为什么我们缓存10也能装下100个数据呢,
  • 因为我们启动了两个线程哈,一边不停的放数据,一遍不停的取数据,这样就可以了,但是要记得关闭,不然会卡死在哪里的,因为缓存不够哈,啊哈哈哈哈哈

有的时候我们会将通道作为参数在多个任务函数间传递,很多时候我们在不同的任务函数中使用通道都会对其进行限制,比如限制通道在函数中只能发送或只能接收。

  • 单向通道就可解决这个问题

单向通道

  • chan<- int是一个只写单向通道(只能对其写入int类型值),可以对其执行发送操作但是不能执行接收操作;
  • <-chan int是一个只读单向通道(只能从其读取int类型值),可以对其执行接收操作但是不能执行发送操作。

主要做参数,用来限制你只能上传值,或者取值

通道总结

channelnil非空空的满了没满
接收阻塞接收值阻塞接收值接收值
发送阻塞发送值发送值阻塞发送值
关闭panic关闭成功,读完数据后返回false关闭成功返回false关闭成功,读取完数据后返回false关闭成功,读取完数据后返回false
  • 注意的是关闭已经关闭的管道也会爆出 panic 错误
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木木不会

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值