go 并发编程 8

并发介绍

什么是高并发
高并发(High Concurrency)是互联网分布式系统架构设计中必须考虑的因素之一,它通常是指,通过设计保证系统能够同时并行处理很多请求。
高并发相关常用的一些指标有响应时间(Response Time),吞吐量(Throughput),每秒查询率QPS(Query Per Second),并发用户数等。
响应时间:系统对请求做出响应的时间。例如系统处理一个HTTP请求需要200ms,这个200ms就是系统的响应时间。
吞吐量:单位时间内处理的请求数量。
QPS:每秒响应请求数。在互联网领域,这个指标和吞吐量区分的没有这么明显。
并发用户数:同时承载正常使用系统功能的用户数量。例如一个即时通讯系统,同时在线量一定程度上代表了系统的并发用户数。

并发和并行

多线程程序在一个核的cpu上运行,就是并发。
多线程程序在多个核的cpu上运行,就是并行

进程和线程与协程
进程和线程

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

线程与协程

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

goroutine

Go语言中使用goroutine非常简单,只需要在调用函数的时候在前面加上go关键字,就可以为一个函数创建一个goroutine。
一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。
启动goroutine的方式非常简单,只需要在调用的函数(普通函数和匿名函数)前面加上一个go关键字。

package main
import (
	"fmt"
	"time"
)
func hello() {
	fmt.Println("Hello Goroutine!")
}
func main() {
	hello()
	fmt.Println("main goroutine done!")
	time.Sleep(time.Second)
}

这个示例中hello函数和下面的语句是串行的,执行的结果是打印完Hello Goroutine!后打印main goroutine done!。
接下来我们在调用hello函数前面加上关键字go,也就是启动一个goroutine去执行hello这个函数

func main() {
	go hello() // 启动另外一个goroutine去执行hello函数
	fmt.Println("main goroutine done!")
}

这一次的执行结果只打印了main goroutine done!,并没有打印Hello Goroutine!。为什么呢?
在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。
当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束,main函数所在的goroutine就像是权利的游戏中的夜王,其他的goroutine都是异鬼,夜王一死它转化的那些异鬼也就全部GG了。
所以我们要想办法让main函数等一等hello函数,最简单粗暴的方式就是time.Sleep了。

func main() {
	go hello() // 启动另外一个goroutine去执行hello函数
	fmt.Println("main goroutine done!")
	time.Sleep(time.Second)
}

执行上面的代码你会发现,这一次先打印main goroutine done!,然后紧接着打印Hello Goroutine!。
首先为什么会先打印main goroutine done!是因为我们在创建新的goroutine的时候需要花费一些时间,而此时main函数所在的goroutine是继续执行的。

启动多个goroutine

在Go语言中实现并发就是这样简单,我们还可以启动多个goroutine。让我们再来一个例子: (这里使用了sync.WaitGroup来实现goroutine的同步)

var wg sync.WaitGroup
func hello(i int) {
	defer wg.Done() // goroutine结束就登记-1
	fmt.Println("Hello Goroutine!", i)
}
func main() {
	for i := 0; i < 10; i++ {
		wg.Add(1) // 启动一个goroutine就登记+1
		go hello(i)
	}
wg.Wait() // 等待所有登记的goroutine都结束
}

多次执行上面的代码,会发现每次打印的数字的顺序都不一致。这是因为10个goroutine是并发执行的,而goroutine的调度是随机的。

runtime包

补充协程切换

runtime包运用

让出CPU时间片,重新等待安排任务(大概意思就是本来计划的好好的周末出去烧烤,但是你妈让你去相亲,两种情况第一就是你相亲速度非常快,见面就黄不耽误你继续烧烤,第二种情况就是你相亲速度特别慢,见面就是你侬我侬的,耽误了烧烤,但是还馋就是耽误了烧烤你还得去烧烤)

package main
import (
	"fmt"
	"runtime"
)
func main() {
	go func(s string) {
		for i := 0; i < 2; i++ {
			fmt.Println(s)
		}
	}("world")
	// 主协程
	for i := 0; i < 2; i++ {
	// 切一下,再次分配任务
		runtime.Gosched()
		fmt.Println("hello")
	}
}

退出当前协程(一边烧烤一边相亲,突然发现相亲对象太丑影响烧烤,果断让她滚蛋,然后也就没有然后了)

package main
import (
	"fmt"
	"runtime"
)
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 {
	}
}

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS 是m:n调度中的n)。
Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。
Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里举个例子:

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

两个任务只有一个逻辑核心,此时是做完一个任务再做另一个任务。 将逻辑核心数设为2,此时两个任务并行执行,代码如下。

func t1() {
	fmt.Println("t1")
}
func t2() {
	fmt.Println("t2")
}
func t3() {
	fmt.Println("t3")
}
func t4() {
	fmt.Println("t4")
}
func t5() {
	fmt.Println("t5")
}
func main() {
// runtime.GOMAXPROCS(5)
	runtime.GOMAXPROCS(1)
	go t1()
	go t2()
	go t3()
	go t4()
	go t5()
	time.Sleep(time.Second)
}

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

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

2. GOMAXPROCS说明

在 gc 编译器下(6g 或者 8g)你必须设置 GOMAXPROCS 为一个大于默认值 1 的数值来允许运行时支持使用多于 1 个的操作系统线程,所有的协程都会共享同一个线程除非将 GOMAXPROCS 设置为一个大于 1 的数。
当 GOMAXPROCS 大于 1 时,会有一个线程池管理许多的线程。通过 gccgo 编译器 GOMAXPROCS 有效的与运行中的协程数量相等。假设 n 是机器上处理器或者核心的数量。如果你设置环境变量GOMAXPROCS>=n,或者执行 runtime.GOMAXPROCS(n),接下来协程会被分割(分散)到 n 个处理器上。更多的处理器并不意味着性能的线性提升。有这样一个经验法则,对于 n 个核心的情况设置 GOMAXPROCS 为 n-1 以获得最佳性能,也同样需要遵守这条规则:协程的数量 > 1 + GOMAXPROCS > 1。

所以如果在某一时间只有一个协程在执行,不要设置 GOMAXPROCS!

还有一些通过实验观察到的现象:在一台 1 颗 CPU 的笔记本电脑上,增加 GOMAXPROCS 到 9 会带来性能提升。在一台 32 核的机器上,设置 GOMAXPROCS=8 会达到最好的性能,在测试环境中,更高的数值无法提升性能。如果设置一个很大的 GOMAXPROCS 只会带来轻微的性能下降;设置 GOMAXPROCS=100,使用 top 命令和 H 选项查看到只有 7 个活动的线程。

增加 GOMAXPROCS 的数值对程序进行并发计算是有好处的;
总结:GOMAXPROCS 等同于(并发的)线程数量,在一台核心数多于1个的机器上,会尽可能有等同于核心数的线程在并行运行。

Channel

协程间的信道

在第一个例子中,协程是独立执行的,他们之间没有通信。他们必须通信才会变得更有用:彼此之间发送和接收信息并且协调/同步他们的工作。协程可以使用共享变量来通信,但是很不提倡这样做,因为这种方式给所有的共享内存的多线程都带来了困难。

而 Go 有一种特殊的类型,通道(channel),就像一个可以用于发送类型化数据的管道,由其负责协程之间的通信,从而避开所有由共享内存导致的陷阱;这种通过通道进行通信的方式保证了同步性。数据在通道中进行传递:在任何给定时间,一个数据被设计为只有一个协程可以对其访问,所以不会发生数据竞争。 数据的所有权(可以读写数据的能力)也因此被传递。

工厂的传送带是个很有用的例子。一个机器(生产者协程)在传送带上放置物品,另外一个机器(消费者协程)拿到物品并打包。
通道服务于通信的两个目的:值的交换,同步的,保证了两个计算(协程)任何时候都是可知状态

channel类型

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

var 变量 chan 元素类型

var ch1 chan int // 声明一个传递整型的通道
var ch2 chan bool // 声明一个传递布尔型的通道
var ch3 chan []int // 声明一个传递int切片的通道

创建channel

通道是引用类型,通道类型的空值是nil。

var ch chan int
fmt.Println(ch) // <nil>

通道操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。
发送和接收都使用 <- 符号。
这个操作符直观的标示了数据的传输:信息按照箭头的方向流动。

ch := make(chan int)

流向通道(发送)
ch <- int1 表示:用通道 ch 发送变量 int1(双目运算符,中缀 = 发送)
如下实例:

ch <- 10

从通道流出(接收)
x := <-ch // 从ch中接收值并赋值给变量x
<-ch // 从ch中接收值,忽略结果,我们也可以直接利用它进行if判断
这种情况就是从通道中获取ch的信息,并进行赋值给x

关闭
我们通过调用内置的close函数来关闭通道。

close(ch)

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的

1.对一个关闭的通道再发送值就会导致panic。
2.对一个关闭的通道进行接收会一直获取值直到通道为空。
3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
4.关闭一个已经关闭的通道会导致panic。

栗子:

ch := make(chan int, 1)
ch <- 10
x := <-ch
ch <- 120
fmt.Println("x", x)
fmt.Println("ch", ch)
fmt.Println("ch <- ", <-ch)

channel缓冲通道

先看个异常效果:

ch := make(chan int)
ch <- 10
x := <-ch
fmt.Println("x", x)

运行会出现异常

因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。

为什么会这样?运行时(runtime)会检查所有的协程(像本例中只有一个)是否在等待着什么东西(可从某个通道读取或者写入某个通道),这意味着程序将无法继续执行。这是死锁(deadlock)的一种形式,而运行时(runtime)可以为我们检测到这种情况。

无缓冲的通道

在这里插入图片描述
因为我们使用ch := make(chan int)创建的是无缓冲的通道,无缓冲的通道只有在有人接收值的时候才能发送值。就像你住的小区没有快递柜和代收点,快递员给你打电话必须要把这个物品送到你的手中,简单来说就是无缓冲的通道必须有接收才能发送。
上面的代码会阻塞在ch <- 10这一行代码形成死锁,那如何解决这个问题呢?

一种方法是启用一个goroutine去接收值,例如:

func main() {
	ch := make(chan int)
	go recv(ch)
	ch <- 10
	fmt.Println("ok")
	time.Sleep(1e9)
}
func recv(ch chan int) {
	fmt.Println("<- ch", <- ch)
}

默认情况下,通信是同步且无缓冲的:在有接受者接收数据之前,发送不会结束。可以想象一个无缓冲的通道在没有空间来保存数据的时候:必须要一个接收者准备好接收通道的数据然后发送者可以直接把数据发送给接收者。所以通道的发送/接收操作在对方准备好之前是阻塞的.
1)对于同一个通道,发送操作(协程或者函数中的),在接收者准备好之前是阻塞的:如果ch中的数据无人接收,就无法再给通道传入其他数据:新的输入无法在通道非空的情况下传入。所以发送操作会等待 ch 再次变为可用状态:就是通道值被接收时(可以传入变量)。

2)对于同一个通道,接收操作是阻塞的(协程或函数中的),直到发送者可用:如果通道中没有数据,接收者就阻塞了。

有缓冲的通道

我们可以在使用make函数初始化通道的时候为其指定通道的容量,例如:
在这里插入图片描述

func main() {
	ch := make(chan int, 1) // 创建一个容量为1的有缓冲区通道
	ch <- 10
	fmt.Println("发送成功")
}

通过一个(或多个)通道交换数据进行协程同步

通信是一种同步形式:通过通道,两个协程在通信(协程会和)中某刻同步交换数据。无缓冲通道成为了多个协程同步的完美工具。
甚至可以在通道两端互相阻塞对方,形成了叫做死锁的状态。Go 运行时会检查并 panic,停止程序。死锁几乎完全是由糟糕的设计导致的。
无缓冲通道会被阻塞。设计无阻塞的程序可以避免这种情况,或者使用带缓冲的通道。

如下代码是运行报错的

func main() {
	ch := make(chan int)
	ch <- 10
	go recv(ch)
	fmt.Println("ok")
	// time.Sleep(1e9)
	close(ch)
}
func recv(ch chan int) {
	fmt.Println("recv")
	fmt.Println("<- ch", <-ch)
}

首先我们这里通过make(chan int),开辟的通道是一种无缓 冲通道,所以当对这个缓冲通道写的时候,会-直阻塞等到某个协程对这个缓冲通道读(大家发现没有这个与典型的生产者消费者有点不一样,当队列中“内容"已经满了,生产者再生往里放东西才会阻塞,而这里我讲c<-'A理解为生产,他却是需要等到某个协程读了再能继续运行)。

main函数的执行在go语言中本身就是- -个协程的执行,所以在执行到c<-'A’的时候,执行main函数的协程将被阻塞,换句话说main函数被阻塞了,此时不能在继续往下执行了,所以go testDeadL ock@这个函数无法执行到了,就无法读到c中的内容了,所以整个程序阻塞,发生了死锁。

通道的方向

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

var sendChOnly chan<- int // 单向发送通道
var recvChOnly <-chan int // 单向获取通道
func main() {
	ch := make(chan int, 1)
	sendChOnly = ch
	sendChOnly <- 1
	recvChOnly = ch
	fmt.Println(<-recvChOnly)
	close(ch)
	ch1 := make(chan int)
	ch2 := make(chan int)
	go counter(ch1)
	go squarer(ch2, ch1)
	printer(ch2)
}
func counter(out chan<- int) {
	for i := 0; i < 100; i++ {
		out <- i
	}
	close(out)
}
func squarer(out chan<- int, in <-chan int) {
	for i := range in {
		out <- i * i
	}
	close(out)
}
func printer(in <-chan int) {
	for i := range in {
		fmt.Println(i)
	}
}

协程的同步:关闭通道-测试阻塞的通道

通道可以被显式的关闭;尽管它们和文件不同:不必每次都关闭。只有在当需要告诉接收者不会再提供新的值的时候,才需要关闭通道。只有发送者需要关闭通道,接收者永远不会需要。

ch := make(chan float64)
defer close(ch)
v, ok := <-ch // ok is true if v received value

分析go官方的协程通道案例-》过滤质数

// https://www.jianshu.com/p/b98b68987b20
package main
import "fmt"
func generate(ch chan int) {
	for i := 2; i < 10; i++ {
		ch <- i // Send 'i' to channel 'ch'.
	}
}
func filter(in, out chan int, prime int) {
	for {
		i := <-in // Receive value of new variable 'i' from 'in'.
		if i%prime != 0 {
			out <- i // Send 'i' to channel 'out'.
		}
	}
}
func main() {
	ch := make(chan int) // Create a new channel.
	go generate(ch) // Start generate() as a goroutine.
	for {
		prime := <-ch
		fmt.Print(prime, " \n")
		ch1 := make(chan int)
		go filter(ch, ch1, prime)
		ch = ch1
	}
}

select与channel

在某些场景下我们需要同时从多个通道接收数据。通道在接收数据时,如果没有数据可以接收将会发生阻塞。你也许会写出如下代码使用遍历的方式来实现

func setest1(ch chan<- string) {
	time.Sleep(time.Second * 5)
	ch <- "setest1"
}
func setest2(ch chan<- string) {
	time.Sleep(time.Second * 2)
	ch <- "setest2"
}
func TestSelect(t *testing.T) {
	t.Log("========>>>>>>>>>select============》》》》")
	ch1 := make(chan string)
	ch2 := make(chan string)
	//跑协程写数据
	go setest1(ch1)
	go setest2(ch2)
	//读取数据
	data1 := <-ch1
	data2 := <-ch2
	t.Log("data1",data1)
	t.Log("data2",data2)
}

采用select

func setest1(ch chan<- string){
	time.Sleep(time.Second * 5)
	ch <- "setest1"
}

1. time与chan=》ticker 协程超时

在time包中存在这time.Ticker结构体,这个对象可以指定的时间间隔重复的想通道C发送时间值

package time
import "errors"
type Ticker struct {
	C <-chan Time // The channel on which the ticks are delivered.
	r runtimeTimer
}
func NewTicker(d Duration) *Ticker {
	c := make(chan Time, 1) // 为什么这里设置为 1 ?????????
	// ..
	return t
}
func (t *Ticker) Stop() {
	// ..
}
func (t *Ticker) Reset(d Duration) {
	// ..
}
func Tick(d Duration) <-chan Time {
	// ..
}

时间间隔的单位是 ns(纳秒,int64),在工厂函数 time.NewTicker 中以 Duration 类型的参数传入:func NewTicker(dur) *Ticker。
在协程周期性的执行一些事情(打印状态日志,输出,计算等等)的时候非常有用。
应用:可以避免某一些协程任务超时导致程序问题

package main
import (
	"fmt"
	"time"
)
func t1() {
	ch := make(chan string)
	data := make([]string, 0)
	go rt1_way3(ch)
	go rt2_way3(ch)
R:
	for {
	select {
		case d1 := <-ch:
			data = append(data, d1)
			if len(data) == 2 {
				break R
			}
		case <-time.After(time.Second * 1):
			fmt.Println(data)
			fmt.Println("超时了")
			// return // 直接结束程序
			break R
		}
	}
	fmt.Println(data)
}
func rt1_way3(ch chan string) {
	time.Sleep(1e9)
	ch <- "rt1_way3"
}
func rt2_way3(ch chan string) {
	time.Sleep(1e9)
	ch <- "rt2_way3"
}

也可以用于限制某些请求的频率

func TestTimer(t *testing.T) {
	ticker := time.Tick(1e9)
		for {
		<-ticker // 为什么这样 ??
		go curl()
	}
}
func curl() {
	fmt.Println("请求")
}

下一篇:go锁
上一篇:文件处理 07

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值