Go语言笔记---goroutine

文章目录

  • 并发
    • 轻量级线程(goroutine)---根据需要随时创建的“线程”
    • 通道---在多个goroutine间通信的管道
    • 同步---保证并发环境下数据访问的正确性

并发

并发是指在同一时间可以执行多个任务。

  • Go语言通过编译器运行时,从语言上支持了并发的特性。Go语言的并发通过goroutine特性完成。goruntine类似于线程,但是可以根据需要创建多个goroutine并发工作。goroutine是由Go语言的运行时调度完成,而线程是由操作系统调度完成。
  • Go语言还支持通道(channel)在多个goroutine间进行通信。这两者是Go语言秉承的并发模式(CSP)并发模式的重要实现基础。

轻量级线程(goroutine)—根据需要随时创建的“线程”

  • 在编写Socket网络程序时,需要提前准备一个线程池为每一个Socket的收发分配一个线程。开发人员需要在线程数量和CPU数量间建立一个对应关系,以保证每个任务能及时地分配到CPU上进行处理,同时避免多个任务频繁地在线程间切换任务而损失效率。
    虽然线程池为逻辑编写者提供了线程分配的抽象机制。但是,如果面对随时随地可能发生的并发和线程处理需求,线程池就不是非常直观和方便了。
  • Go语言中的goroutine就可以解决这个问题,使用者分配足够多的任务,系统能自动帮助使用者把任务分配到CPU上,让这些任务尽量并发运作。
  • GO程序从main包的main()函数开始,在程序启动时,Go程序就会为main()函数创建一个默认的goroutine.

1.使用普通函数创建goroutine
写法如下:

go 函数名 (参数列表)
//使用go关键字创建goroutine时,被调用函数的返回值会被忽略。

例子:

package main

import (
	"fmt"
	"time"
)

func running() {
	var times int
	//构建一个无限循环
	for {
		times++
		fmt.Println("tick", times)

		//延时1秒
		time.Sleep(time.Second)
	}
}
func main() {
	//并发执行程序
	go running()

	//接收命令行输入,不做任何事情
	var input string
	fmt.Scanln(&input)

}

使用匿名函数创建routine

package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan int)
	go func() {
		var times int

		for {
			times++
			ch <- times
			fmt.Println("tick", ch)
			time.Sleep(time.Second)
		}
	}()
	var input string
	fmt.Scanln(&input)
}

Go语言的goroutine一般要搭配channel一起使用才行,如上述例子运行之后会直接运行结束。
调整并发的运行性能
在Go程序运行时实现了一个小型的任务调度器。
工作原理:

Go程序调度器可以高效地将CPU资源分配给每一个任务。传统逻辑中,开发者需要维护线程池中线程与CPU核心数量的对应关系。同样的,Go中可以通过**runtime.GOMAXPROCS(逻辑CPU数量)**函数做到,CPU数量有如下几种数值:
<1:不修改任何数值
=1:单核心执行
>1:多核并发执行

通道—在多个goroutine间通信的管道

使用通道的原因:单纯的将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发
执行函数的意义。
虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。
为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

多个goroutine为了争抢数据,势必造成执行的低效率,使用队列的方式是最高效的,channel就是一种队列一样的结构。
Go语言笔记---goroutine_第1张图片

通道的特性:

  • 通道是一种特殊的类型。在任何时候,同时只能有一个goroutine访问通道进行发送和获取数据。
  • 通道像一个传送带或者队列,总是遵循先入先出的规则,保证收发数据的顺序。

声明通道类型

var 通道变量 chan 通道类型
//chan类型的空值是nil,声明后需要配合make后才能使用。

创建通道

通道实例 := make(chan 数据类型)

ch1 := make(chan int)	//创建一个整型类型的通道
ch2 := make(chan interface{})	//创建一个空接口类型的通道,可以存放任意格式

使用通道发送数据
格式为:

通道变量 <-//例子
ch :=make(chan interface{})
ch <- 0		//将0放入通道中
ch <- "hello" 	//将hello字符放入通道中

使用通道接收数据
通道接收同样使用"<-"操作符,通道接收有如下的特性:

  • 通道的收发操作在不同的两个goroutine间进行。
    由于通道的数据在没有接收方处理时,数据方会持续阻塞,因此通道的接收必定在另一个goroutine中进行。
  • 接收将持续阻塞直到发送方发送数据
    如果接收方接收时,通道中没有发送方发送数据,接收方也会发生阻塞,直到发送方发送数据为止。
  • 每次只接收一个元素
    通道一次只能接收一个数据元素

1.阻塞接收数据

data :=<- ch

执行该语句时将会阻塞,直到接收到数据并赋值给data变量

2.非阻塞接收数据

data,ok := <-ch

注意:非阻塞的通道接收方法可能造成高的CPU占用,因此使用非常少

3.接收任意数据,忽略接收的数据。

<- ch

执行该语句时将会发生阻塞,直到接收到数据,但接收到的数据会被忽略。这个方式实际上只是通过通道在goroutine间阻塞收发实现并发同步

什么是并发和并行?

  • 并发:把任务在不同的时间点交给处理器进行处理。在同一个时间点,任务并不会同时进行。
  • 并行:把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。
package main

import (
	"fmt"
)

func main() {
	//构建一个通道
	ch := make(chan int)

	//开启一个并发匿名函数
	go func() {
		fmt.Println("start goroutine")

		//通过通道通知main的goroutine
		ch <- 0
		fmt.Println("exit goroutine")
	}()
	fmt.Println("wait goroutine")
	//等待匿名goroutine
	<-ch
	fmt.Println("all done")
}

输出:
wait goroutine
start goroutine
exit goroutine
all done
为什么是这样的输出呢?
因为匿名goroutine即将结束时,通过通道通知main的goroutine,这一句会一直阻塞直到main的goroutine接收为止。

4.循环接收
通道的数据接收可以借用for range语句进行多个元素的接收操作,格式如下:

for data :=range ch{
}

通道ch 是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。通过for 遍历获得的变量只有一个,即data.

示例:使用for从通道中接收数据

package main

import (
	"fmt"
	"time"
)

func main() {
	//构建一个通道
	ch := make(chan int)

	//开启一个并发匿名函数
	go func() {
		//从3循环到0
		for i := 3; i >= 0; i-- {
			//发送3到0之间的数值
			ch <- i
			//每次发送完时等待
			time.Sleep(time.Second)
		}
	}()
	//遍历接收通道数据
	for data := range ch {
		//打印通道数据
		fmt.Println(data)
		//当遇到数据0时,退出接收循环
		if data == 0 {
			break
		}
	}
}

输出结果:
3
2
1
0

小示例:并发打印

  • 之前的例子创建的都是无缓冲通道,使用无缓冲通道往里面装入数据时,装入方将被阻塞,直到另外通道在另外一个goroutine中被取出。
  • 同样,如果通道中没有放入任何数据,接收方试图从通道中获取数据时,同样也是阻塞。
  • 发送和接收是同步完成的。
package main

import (
	"fmt"
)

func printer(c chan int) {
	//开始无限循环等待数据
	for {
		//从channel中获取一个数据
		data := <-c

		//将0视为数据结束
		if data == 0 {
			break
		}
		//打印数据
		fmt.Println(data)
	}
	//通知main已经结束循环
	c <- 0
}
func main() {
	//创建一个channel
	c := make(chan int)
	//并发执行printer,传入channel
	go printer(c)
	for i := 1; i <= 10; i++ {
		//将数据通过channel投送给printer
		c <- i
	}
	//通知并发的printer结束循环
	c <- 0
	//等待printer结束
	<-c
}

输出:
1
2
3
4
5
6
7
8
9
10

单向通道—通道中的单行道
单向通道:Go的通道可以在声明时约束其操作方向,如只发送或是只接收。这种被约束方向的通道被称为单向通道。

1.单向通道的生命格式
只能发送的通道类型为chan<-,只能接收的通道类型为<-chan,格式如下:

var 通道实例 chan<- 元素类型		//只能发送通道
var 通道实例 <-chan 元素类型		//只能接收通道

2.单向通道的使用例子

ch :=make(chan int)
//声明一个只能发送的通道类型,并赋值为ch
var chSendOnly chan<- int =ch 
//声明一个只能接收的通道类型,并赋值给ch
var chRecvOnly <-chan int =ch 

3.time包中的单向通道
time包中的计时器会返回一个timer实例,代码如下:

timer :=time.NewTimer(time.second)

//timer的Timer类型定义如下:
type Timer struct{
	C <-chan Time
	r runtimeTimer
}

C通道的类型就是一种只能接收的单向通道。如果此处不进行通道方向的约束,一旦外部向通道发送数据,将会造成其他使用到计时器的地方逻辑产生混乱

带缓冲的通道
所谓带缓冲的通道就是在无缓冲通道基础上,为通道增加一个有限大小的存储空间形成带缓冲通道。

带缓冲的通道在发送时无需等待接收方即可完成发送过程,并且不会发生阻塞,只有当存储空间满时才会发生阻塞。
同理,如果缓冲通道中有数据,接收时将不会发生阻塞,直到通道中没有数据可读时,通道将会再度阻塞。

1.创建带缓冲通道
如何创建呢?格式如下:

通道实例 :=make(chan 通道类型,缓冲大小)

通过示例来了解:

package main

import (
	"fmt"
)

func main() {
	//创建一个3个元素缓冲大小的整型通道
	ch := make(chan int, 3)
	//查看当前通道的大小
	fmt.Println(len(ch))
	//发送3个整型元素到通道
	ch <- 1
	ch <- 2
	ch <- 3
	//查看当前通道的大小
	fmt.Println(len(ch))
}

输出:
0
3

2.阻塞条件
带缓冲通道在很多特性上和无缓冲通道是类似的。无缓冲通道可以看作是长度永远为0的带缓冲通道。
因此根据这个特性,带缓冲通道在下面列举的情况下依然发生阻塞:

  • 带缓冲通道被填满时,尝试再次发送数据时发生阻塞。
  • 带缓冲通道为空时,尝试接收数据时发生阻塞。

为什么Go语言对通道要限制长度而不提供无限长度的通道?

通道是两个goroutine之间的桥梁。使用goroutine的代码必然有一方提供数据,一方消费数据。当提供数据的一方的数据供给速度大于消费方的处理速度,不限制长度的话,那么内存将不断膨胀直到应用崩溃。因此,必须限制数据提供方的速度,供给数据量必须在消费方处理量+通道长度的范围内,才能正确地处理数据。

通道的多路复用—同时处理接收和发送多个通道的数据
多路复用通常表示在一个信道上传输多路信号或数据流的过程和技术。

Go语言中提供了select关键字,可以同时响应多个通道的操作。select的每个case都会对应一个通道的收发过程。当收发完成时,就会触发case中响应的语句。多个操作在每次select中挑选一个进行响应。格式如下:

select{
case 操作1:
	响应操作1
case 操作2:
	响应操作2
...
default:
	没有操作情况

关闭通道之后继续使用通道
通道是一个引用对象,和map类似。Go程序在运行时会自动对内存进行垃圾回收。类似,通道也可以被垃圾回收,但是通道也可以被主动关闭
1.格式:

close(ch)

2.给被关闭通道发送数据会触发panic
被关闭通道不会置为nil,如果尝试对已经关闭的通道发送数据,将会触发宕机

package main

import (
	"fmt"
)

func main() {
	//创建一个整型的通道
	ch := make(chan int)
	//关闭通道
	close(ch)
	//打印通道的指针,容量和长度
	fmt.Printf("ptr:%p cap:%d len:%d\n", ch, cap(ch), len(ch))
	//给关闭的通道发送数据
	ch <- 1
}

果然发生了宕机:
在这里插入图片描述

3。 从已关闭的通道接收数据时将不会发生阻塞
已经关闭的通道接收数据或者正在接收数据时,将会接收到通道类型的零值,然后停止阻塞并返回。

操作关闭后的通道:

package main

import (
	"fmt"
)

func main() {
	//创建一个整型带两个缓冲的通道
	ch := make(chan int, 2)

	//给通道放入两个数据
	ch <- 0
	ch <- 1

	//关闭缓冲
	close(ch)

	//遍历缓冲所有 数据,且多遍历1个
	for i := 0; i < cap(ch)+1; i++ {
		//从通道中取出数据
		v, ok := <-ch
		//打印取出数据的状态
		fmt.Println(v, ok)
	}
}

输出:
0 true
1 true
0 false

前两行正常输出带缓冲通道的数据,表明缓冲通道在关闭后依然可以访问内部的数据。
运行第三行,此时通道已经空了,我们发现在通道关闭后,即便通道没有数据,在获取时也不会发生阻塞,但此时取出数据会失败。

同步—保证并发环境下数据访问的正确性

Go程序可以使用通道进行多个goroutine间的数据交换,但这仅仅是数据同步中的一种方法。
通道内部的实现依然使用了各种锁,在某些轻量级的场合,原子访问,互斥锁以及等待组能最大程度满足需求。

竞态条件—检测代码在并发环境下可能出现的问题
当多线程并发运行的程序竞争访问和修改同一块资源时,会发生竞态问题。

原子操作(atomic)。
可以使用原子操作对变量进行增减操作,虽然也可以使用互斥锁(sync.Mutex)解决竞态问题,但是对性能消耗较大。

//尝试原子的增加序列号
return atomic.AddInt64(&seq,1)

互斥锁(sync.Mutex)—保证同时只有一个goroutine可以访问共享资源
互斥锁操作:

package main 

import (
	"fmt"
	"sync"
)

var(
	//逻辑中使用的某个变量
	count int 
	//与变量对应的使用互斥锁
	countGuard sync.Mutex
)
func GetCount() int{
	//锁定
	countGuard.Lock()
	//在函数退出时解除锁定
	defer countGuard.Unlock()
	return count
}
func SetCount(c int) {
	countGuard.Lock()
	count =c 
	countGuard.Unlock()
} 

func main() {
	//可以进行并发安全的设置
	SetCount(1)
	//可以进行并发安全的获取
	fmt.Println(GetCount())
}

输出1
在GetCount()函数中,对countGuard互斥量进行加锁。一旦countGuard发生加锁,如果另一个goroutine尝试继续加锁将会发生阻塞,直到这个countGuard被解锁。
使用defer将解锁延迟,解锁操作将会发生在GetCount()函数返回时
同样的,设置值时,也要进行加锁、解锁操作。

读写互斥锁(sync.RWMutex)—在读比写多的环境下比互斥锁更高效
应用环境:读多写少
将互斥锁例子中的一部分代码修改为读写互斥锁:

var (
	//逻辑中使用的某个变量
	count int
	//与变量对应的使用互斥锁
	countGuard sync.RWMutex
)

func GetCount() int {
	//锁定
	countGuard.RLock()
	//在函数退出时解除锁定
	defer countGuard.RUnlock()
	return count
}

等待组(sync.WaitGroup)—保证在并发环境中完成指定数量的任务
使用等待组进行多个任务的同步。

方法名 功能
(wg *WaitGroup)Add(delta int) 等待组的计数器+1
(wg *WaitGroup)Done() 等待组的计数器-1
(wg *WaitGroup)Wait() 当等待组计数器不等于0时阻塞直到变0

等待组内部有一个计数器,计数器的值可以通过方法实现计数器的增加和减少。当我们添加了N个并发任务进行工作时,就将等待组的计数增加N。每个任务完成时,这个值减1.同时,在另外一个goroutine中等待组的计数器值为0时,表示所有任务已经完成。

等待组的应用:

package main

import (
	"fmt"
	"net/http"
	"sync"
)

func main() {
	//声明一个等待数组
	var wg sync.WaitGroup

	//准备一系列的网站
	var urls = []string{
		"http://www.github.com/",
		"https://www.qiniu.com/",
		"https://www.golangtc.com/",
	}
	//遍历这些地址
	for _, url := range urls {
		//每一个任务开始时,将等待组增加1
		wg.Add(1)
		//开启一个并发
		go func() {
			//使用defer,表示函数完成时将等待组值减1
			defer wg.Done()
			//使用http访问提供的地址
			_, err := http.Get(url)

			//访问完成后,打印地址和可能发生的错误
			fmt.Println(url, err)

		}() //通过参数传递url地址

		//等待所有的任务完成
		wg.Wait()

		fmt.Println("over")
	}
}

输出结果:
Go语言笔记---goroutine_第2张图片

结构体标签—对结构体字段的额外信息标签
通过reflect.Type获取结构体成员信息reflect.StructField结构中的Tag被称为结构体标签。
Json、Bson等格式进行序列化及对象关系映射,系统都会用到结构体标签,这些系统使用标签设定字段在处理时应该具备的特殊属性和可能发生的行为,这些信息都是静态的,无须实例化结构体,可以通过反射获取到。
1.结构体标签的格式

key1:"value1" key2:"value2"

2.从结构体标签中获取值
StructTag拥有一些方法,可以进行Tag信息的解析和提取,

//根据Tag中的键获取对应的值
func (tag StructTag) Get(key string) string
//根据Tag中的键查询值是否存在
func (tag StructTag)Lookup(key string)(value string,ok bool)

3.结构体标签格式错误导致的问题
编写Tag时,必须严格遵守键值对的规则。
如果格式错误,编译和运行不会提示任何错误,看示例:

package main 

import (
	"fmt"
	"reflect"
)

func main() {
	type cat struct{
		Name string 
		Type int 'json: "type" id:"100"'
	}
	
	typeOfCat := reflect.TypeOf(cat{})

	if catType, ok :=typeOfCat.FieldByName("Type"); ok{
		fmt.Println(catType.Tag.Get("json"))
	}
}

这个代码中json: "type"中间多了一个空格。没有按照规则来写,这种错误就很难被察觉到。

你可能感兴趣的:(计算机语言---go语言)