Go语言并发编程(千锋教育)

Go语言并发编程(千锋教育)

视频地址:https://www.bilibili.com/video/BV1t541147Bc?p=14

作者B站:https://space.bilibili.com/353694001

源代码:https://github.com/rubyhan1314/go_goroutine

1、基本概念

1.1、并发与并行

其实操作系统里对这些概念都有所说明和举例。

并发

  • 并发是指多个任务在同一时间段内交替执行,从外部看似乎是同时执行的。
  • 具体来说,当一个任务在等待I/O操作的结果时,CPU可以切换到另一个任务上去执行,这样就不需要等待I/O操作完成,从而提高了CPU的利用率。
  • 并发通常需要一个调度器来协调多个任务的执行。

并行

  • 并行是指多个任务同时执行,需要多个处理器或者多核CPU来支持。
  • 并行可以大大提高程序的执行效率,因为多个任务可以同时运行,而不是交替执行。
  • 并行通常需要特殊的硬件支持。

==并行性(Parallelism)不会总是导致更快的执行实际。因为并行运行的组件可能需要相互通信。==这种通信的开销在并发(Concurrent)系统中很低,但在并行系统中开销很高。

1.2、进程、线程、协程

进程(Process),线程(Thread),协程(Coroutine,也叫轻量级线程)

进程 进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序”,它是CPU资源分配和调度的独立单位。 进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。 进程的局限是创建、撤销和切换的开销比较大。

线程 线程是在进程之后发展出来的概念。 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。 线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源,只拥有在运行时必不可少的资源,但同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。不过对于某些独占性资源存在锁机制,处理不当可能会产生“死锁”。

协程 协程是一种用户态的轻量级线程,又称微线程,英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。 子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

与传统的系统级线程和进程相比,协程的最大优势在于其"轻量级",可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万的。这也是协程也叫轻量级线程的原因。

Go语言对于并发的实现是靠协程,Goroutine。

2、初始Goroutine

2.1、什么是Goroutine

Goroutine是Go中特有的名词。区别于进程Process、线程Thread,协程Coroutine,因为Go语言的创造者觉得和他们是有区别的,所以专门创造了Goroutine。

Goroutine是与其他函数或方法同时运行的函数或方法。与线程相比,创建Goroutine的成本很小,它就是一段代码、一个函数入口,以及在堆上为其分配的一个堆栈。因此它非常廉价,Go应用程序可以并发运行数千个Goroutines。

Goroutine的优势主要体现在以下方面:

  • 栈内存小,约是Java线程栈的500~1000分之一
    Java线程栈普遍在M级别,也就是Java启动时-Xss设置的大小,可以通过java -XX:+PrintFlagsFinal -version|grep ThreadStackSize 查看,并且不支持动态扩展,满了会抛出栈溢出异常
    Goroutine的栈大小一般在2k,在内存不足时可进行动态扩展,可自动扩展至GB级别
  • 上下文切换快 ,是线程切换的5~8倍
    线程上下文切换由操作系统调度完成,相当于将一个线程从cpu核心上移动下来,把另一个线程移动上去,线程上下文切换耗时大约 1000-1500纳秒,约等于12k-18k条计算机指令
    Goruotine切换由Go运行时调度完成,相当于把一个Goroutine从线程上移动下来,把另一个Goroutine移动上去,协程上下文切换大约 200纳秒, 约2.4k条计算机指令
    如上所述:go运行时实现了类似操作系统调度线程的Goroutine调度器,线程的执行者是cpu核心,Goroutine执行者为线程

2.2、主goroutine

封装main函数的goroutine称为主goroutine。

Go语言并发编程(千锋教育)_第1张图片

2.3、如何使用Goroutines

在函数或方法的前面加上关键字go,在调用时将会同时运行一个新的Goroutine。

实例代码:

package main

import "fmt"

func main() {

	// 1.先创建或启动子goroutine,执行printNum()
	go printNum()

	// 2.main中打印字母
	for i := 0; i < 1000; i++ {
		fmt.Println("主goroutine中打印字符:", 'A')
	}

}

func printNum() {
	for i := 0; i < 1000; i++ {
		fmt.Println("\t子goroutine中打印数字:", i)
	}
}

类似于多线程,线程之间的调度与执行是不确定的,但是当主goroutine运行结束时,子goroutine也会运行结束。

因此在编程时为了保证子goroutine执行完毕主goroutine才结束,需要用到通道来传递消息。

3、Goroutine并发模型

常见的线程模型
常见的有用户级线程模型、内核级线程模型、两级线程模型

常见的线程模型_为什么两层线程模型比内核级线程模型_Schuyler_yuan的博客-CSDN博客

线程并发常见模型

G-P-M 模型(Goroutine调度器模型)
在操作系统提供的内核线程之上,Go 搭建了一个特有的两级线程模型。goroutine 机制实现了 M : N 的线程模型,goroutine 机制是协程(goroutine)的一种实现,Golang 内置的调度器,可以让多核 CPU 中每个 CPU 执行一个协程。
GMP 线程调度模型_gmp调度模型_Schuyler_yuan的博客-CSDN博客

4、runtime包

runtime 调度器是个非常有用的东西,关于 runtime 包几个方法:

  1. NumCPU:返回当前系统的 CPU 核数量

  2. GOMAXPROCS:设置最大的可同时使用的 CPU 核数

    通过runtime.GOMAXPROCS函数,应用程序何以在运行期间设置运行时系统中得P最大数量。但这会引起“Stop the World”。所以,应在应用程序最早的调用。并且最好是在运行Go程序之前设置好操作程序的环境变量GOMAXPROCS,而不是在程序中调用runtime.GOMAXPROCS函数。

    无论我们传递给函数的整数值是什么值,运行时系统的P最大值总会在1~256之间。

go1.8后,默认让程序运行在多个核上,可以不用设置了 go1.8前,还是要设置一下,可以更高效的利益cpu

  • Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行

    这个函数的作用是让当前 goroutine 让出 CPU,当一个 goroutine 发生阻塞,Go 会自动地把与该 goroutine 处于同一系统线程的其他 goroutine 转移到另一个系统线程上去,以使这些 goroutine 不阻塞。

  • Goexit:退出当前 goroutine(但是defer语句会照常执行)

  • NumGoroutine:返回正在执行和排队的任务总数

    runtime.NumGoroutine函数在被调用后,会返回系统中的处于特定状态的Goroutine的数量。这里的特指是指Grunnable\Gruning\Gsyscall\Gwaition。处于这些状态的Groutine即被看做是活跃的或者说正在被调度。

    注意:垃圾回收所在Groutine的状态也处于这个范围内的话,也会被纳入该计数器。

  • GOOS:目标操作系统

  • runtime.GC:会让运行时系统进行一次强制性的垃圾收集

    1. 强制的垃圾回收:不管怎样,都要进行的垃圾回收。
    2. 非强制的垃圾回收:只会在一定条件下进行的垃圾回收(即运行时,系统自上次垃圾回收之后新申请的堆内存的单元(也成为单元增量)达到指定的数值)。
  • GOROOT :获取goroot目录

5、安全问题

什么是临界资源

临界资源是一次仅允许一个进程使用的共享资源。*各进程采取互斥的方式,实现共享的资源称作临界资源。*属于临界资源的硬件有,打印机,磁带机等;软件有消息队列,变量,数组,缓冲区等。诸进程间采取互斥方式,实现对这种资源的共享。

临界资源安全问题

并发本身并不复杂,但是因为有了资源竞争的问题,程序就变得复杂起来。

// 火车站售票 4个窗口卖10张票
package main

import (
	"fmt"
	"strconv"
	"time"
)

var ticket = 10 // 10张票

func main() {
	for i := 1; i <= 4; i++ {
		go saleTicket(string("第" + strconv.Itoa(i) + "个窗口"))
	}
	time.Sleep(3 * time.Second)							// 等待子goroutine执行完毕
}

func saleTicket(name string) {
	for ticket > 0 {
		time.Sleep(1 * time.Millisecond)				// 模拟时延
		fmt.Println(name, "售出了第", ticket, "张票")
		ticket--
	}
}

/*
	第1个窗口 售出了第 10 张票
    第2个窗口 售出了第 10 张票
    第3个窗口 售出了第 10 张票
    第4个窗口 售出了第 10 张票
    第2个窗口 售出了第 6 张票
    第4个窗口 售出了第 6 张票
    第1个窗口 售出了第 6 张票
    第3个窗口 售出了第 6 张票
    第3个窗口 售出了第 2 张票
    第4个窗口 售出了第 2 张票
    第2个窗口 售出了第 2 张票
    第1个窗口 售出了第 2 张票
    第3个窗口 售出了第 -2 张票
*/

解决

要想解决这样的问题,很多编程语言的解决方案都是同步。

通过上锁的方式,某一时间段,只能允许一个goroutine来访问这个数据,解锁后才允许其它goroutine来访问。

示例代码:

package main

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

var ticket = 10 // 10张票
var mutex sync.RWMutex

func main() {

	for i := 1; i <= 4; i++ {
		go saleTicket(string("第" + strconv.Itoa(i) + "个窗口"))
	}
	time.Sleep(3 * time.Second)
}

func saleTicket(name string) {
	for {
		time.Sleep(1 * time.Millisecond)
		mutex.Lock()
		if ticket > 0 {
			fmt.Println(name, "售出了第", ticket, "张票")
			ticket--
		}
		mutex.Unlock()
	}
}

写在最后

在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信,而要以通信的方式去共享内存。

在Go语言中不鼓励使用锁来保护共享状态的方式在不同的Goroutine中分享信息,而是鼓励使用channel将共享状态或共享状态的变化在各个Goroutine之间传递。

6、sync包

6.1、waitGroup

对于一个可寻址的 sync.WaitGroup 值 wg:

  • 我们可以使用方法调用 wg.Add(delta) 来改变值 wg 维护的计数。
  • 方法调用 wg.Done() 和 wg.Add(-1) 是完全等价的。
  • 如果一个 wg.Add(delta) 或者 wg.Done() 调用将 wg 维护的计数更改成一个负数,一个恐慌将产生。
  • 当一个协程调用了 wg.Wait() 时,
    • 如果此时 wg 维护的计数为零,则此 wg.Wait() 此操作为一个空操作(noop);
    • 否则(计数为一个正整数),此协程将进入阻塞状态。当以后其它某个协程将此计数更改至 0 时(一般通过调用 wg.Done()),此协程将重新进入运行状态(即 wg.Wait() 将返回)。

示例代码:

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(url string) {

			// 使用defer, 表示函数完成时将等待组值减1
			defer wg.Done()

			// 使用http访问提供的地址
			_, err := http.Get(url)

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

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

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

	fmt.Println("over")
}

6.2、互斥锁

互斥锁解决卖票问题:

package main

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

var ticket = 10 // 10张票
var mutex sync.Mutex

func main() {

	for i := 1; i <= 4; i++ {
		go saleTicket(string("第" + strconv.Itoa(i) + "个窗口"))
	}
	time.Sleep(3 * time.Second)
}

func saleTicket(name string) {
	for {
		time.Sleep(1 * time.Millisecond)
		mutex.Lock() // 上锁
		if ticket > 0 {
			fmt.Println(name, "售出了第", ticket, "张票")
			ticket--
		}
		mutex.Unlock() //解锁
	}
}

6.3、读写锁

Go语言包中的 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex。其中RWMutex是基于Mutex实现的,只读锁的实现使用类似引用计数器的功能。

RWMutex是读/写互斥锁。锁可以由任意数量的读取器或单个编写器持有。RWMutex的零值是未锁定的mutex。

如果一个goroutine持有一个RWMutex进行读取,而另一个goroutine可能调用lock,那么在释放初始读取锁之前,任何goroutine都不应该期望能够获取读取锁。特别是,这禁止递归读取锁定。这是为了确保锁最终可用;被阻止的锁调用会将新的读卡器排除在获取锁之外。

我们怎么理解读写锁呢?当有一个 goroutine 获得写锁定,其它无论是读锁定还是写锁定都将阻塞直到写解锁;当有一个 goroutine 获得读锁定,其它读锁定仍然可以继续;当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定。所以说这里的读锁定(RLock)目的其实是告诉写锁定:有很多人正在读取数据,你给我站一边去,等它们读(读解锁)完你再来写(写锁定)。我们可以将其总结为如下三条:

  1. 同时只能有一个 goroutine 能够获得写锁定。
  2. 同时可以有任意多个 gorouinte 获得读锁定。
  3. 同时只能存在写锁定或读锁定(读和写互斥)。

所以,RWMutex这个读写锁,该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景

基本遵循两大原则:

1、可以随便读,多个goroutine同时读。

2、写的时候,啥也不能干。不能读也不能写。

读写锁即是针对于读写操作的互斥锁。它与普通的互斥锁最大的不同就是,它可以分别针对读操作和写操作进行锁定和解锁操作。读写锁遵循的访问控制规则与互斥锁有所不同。在读写锁管辖的范围内,它允许任意个读操作的同时进行。但是在同一时刻,它只允许有一个写操作在进行。

并且在某一个写操作被进行的过程中,读操作的进行也是不被允许的。也就是说读写锁控制下的多个写操作之间都是互斥的,并且写操作与读操作之间也都是互斥的。但是,多个读操作之间却不存在互斥关系。

示例代码:

package main

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

var rwMutex *sync.RWMutex
var wg *sync.WaitGroup

func main() {

	rwMutex = new(sync.RWMutex)
	wg = new(sync.WaitGroup)

	//wg.Add(2)

	//多个同时读取
	//go readData(1)
	//go readData(2)

	wg.Add(3)
	go writeData(1)
	go readData(2)
	go writeData(3)

	wg.Wait()
	fmt.Println("main..over...")
}

func writeData(i int) {
	defer wg.Done()
	fmt.Println(i, "开始写:write start。。")
	rwMutex.Lock() //写操作上锁
	fmt.Println(i, "正在写:writing。。。。")
	time.Sleep(3 * time.Second)
	rwMutex.Unlock()
	fmt.Println(i, "写结束:write over。。")
}

func readData(i int) {
	defer wg.Done()

	fmt.Println(i, "开始读:read start。。")

	rwMutex.RLock() //读操作上锁
	fmt.Println(i, "正在读取数据:reading。。。")
	time.Sleep(3 * time.Second)
	rwMutex.RUnlock() //读操作解锁
	fmt.Println(i, "读结束:read over。。。")
}

7、channel

7.1、什么是通道

通道可以被认为是Goroutines通信的管道。类似于管道中的水从一端到另一端的流动,数据可以从一端发送到另一端,通过通道接收。当多个Goroutine想实现共享数据的时候,虽然也提供了传统的同步机制,但是Go语言强烈建议的是使用Channel通道来实现Goroutines之间的通信。

“不要通过共享内存来通信,而应该通过通信来共享内存” 这是一句风靡golang社区的经典语

Go语言中,要传递某个数据给另一个goroutine(协程),可以把这个数据封装成一个对象,然后把这个对象的指针传入某个channel中,另外一个goroutine从这个channel中读出这个指针,并处理其指向的内存对象。Go从语言层面保证同一个时间只有一个goroutine能够访问channel里面的数据,为开发者提供了一种优雅简单的工具,所以Go的做法就是使用channel来通信,通过通信来传递内存数据,使得内存数据在不同的goroutine中传递,而不是使用共享内存来通信。

7.2、通道的声明

// 声明通道
var 通道名 chan 数据类型
// 创建通道:如果通道为nil(就是不存在),就需要先创建通道
通道名 = make(chan 数据类型)

// 也可以使用短声明
通道名 := make(chan 数据类型)

示例代码:

package main

import "fmt"

func main() {
	var a chan int
	if a == nil {
		fmt.Println("通道是 nil 的,不能使用,需要先创建通道。。")
		a = make(chan int)
		fmt.Printf("通道的类型为:%T\n", a)
	}
}

7.3、通道的注意点

  • channel是引用类型的数据,在作为参数传递的时候,传递的是内存地址。
  • 用于goroutine传递消息的。
  • 通道,每个都有相关联的数据类型, nil chan,不能使用
  • 阻塞: 发送数据:chan <- data,阻塞的,直到另一条goroutine,读取数据来解除阻塞 读取数据:data <- chan,也是阻塞的。直到另一条goroutine,写出数据解除阻塞。
  • 本身channel就是同步的,意味着同一时间,只能有一条goroutine来操作。

最后:通道是goroutine之间的连接,所以通道的发送和接收必须处在不同的goroutine中。

死锁

使用通道时要考虑的一个重要因素是死锁。如果Goroutine在一个通道上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发生,那么程序将在运行时出现死锁。

类似地,如果Goroutine正在等待从通道接收数据,那么另一些Goroutine将会在该通道上写入数据,否则程序将会死锁。

7.4、通道的使用

发送和接收数据

data := <- a 	// read from channel a  
a <- data 		// write to channel a

在通道上箭头的方向指定数据是发送还是接收。

另外:

v, ok := <- a 	// 从通道a中读取

示例代码1:

package main

import "fmt"

func main() {

	var ch1 chan bool
	ch1 = make(chan bool)

	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println("子Goroutine中,i:", i)
		}
		// 循环结束后,向通道中写数据,表示执行完成
		ch1 <- true
		fmt.Println("结束。。。")
	}()

	data := <-ch1
	fmt.Println("main...data---->", data)
	fmt.Println("main...over...")

}

示例代码2:

package main

import (
	"fmt"
)

func main() {
	ch1 := make(chan int)

	go func() {
		fmt.Println("子goroutine开始执行。。")
		data := <-ch1
		fmt.Println(data)
	}()
	ch1 <- 10
	fmt.Println("main..over..")

}

一个通道发送和接收数据,默认是阻塞的。当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个goroutine从通道中读取数据。相对的,当从通道中读取数据时,读取被阻塞,直到一个goroutine将数据写入该通道。

这些通道的特性帮助goroutines有效地进行通信,而无需像其它编程语言中非常常见的显示锁或条件变量。

7.5、通道的关闭和范围循环

通道关闭

发送者可以通过关闭信道,来通知接收方不会有更多的数据被发送到channel上。

close(ch)

接收者可以在接收来自通道的数据时使用额外的变量来检查通道是否已经关闭。

语法结构:

v, ok := <- ch  

在上面的语句中,如果ok的值是true,表示成功的从通道中读取了一个数据value。如果ok是false,这意味着我们正在从一个封闭的通道读取数据。从闭通道读取的值将是通道类型的零值。

实例代码:

package main

import (
	"fmt"
	"time"
)

func main()  {
	ch1 := make(chan int)
	go sendData(ch1)
	/*
        子goroutine,写出数据10个
                每写一个,阻塞一次,主程序读取一次,解除阻塞

        主goroutine:循环读
                每次读取一个,阻塞一次,子程序,写出一个,解除阻塞

        发送发,关闭通道的--->接收方,接收到的数据是该类型的零值,以及false
	 */
	//主程序中获取通道的数据
	for{
		time.Sleep(1*time.Millisecond)
		v, ok := <- ch1 		//其他goroutine,显示的调用close方法关闭通道。
		if !ok{
			fmt.Println("已经读取了所有的数据,", ok)
			break
		}
		fmt.Println("取出数据:",v, ok)
	}

	fmt.Println("main...over....")
}

func sendData(ch1 chan int)  {
	// 发送方:10条数据
	for i:=0;i<10 ;i++  {
		ch1 <- i		//将i写入通道中
	}
	close(ch1) 			//将ch1通道关闭了。
}

范围循环 for-range

	for v := range ch {
        
	}

实例代码:

package main

import "fmt"

func main() {

	ch := make(chan int)
	go sendData(ch)
	for v := range ch {
		fmt.Println("读取数据:", v)
	}
	fmt.Println("main。。over。。")

}

func sendData(ch chan int) {
	for i := 0; i < 10; i++ {
		ch <- i
	}
	close(ch)
}

注意,一定要手动关闭通道!

7.6、缓冲通道

之前学习的所有通道基本上都没有缓冲。发送和接收到一个未缓冲的通道是阻塞的。

一次发送操作对应一次接收操作,对于一个goroutine来讲,它的一次发送,在另一个goroutine接收之前都是阻塞的。同样的,对于接收来讲,在另一个goroutine发送之前,它也是阻塞的。

缓冲通道就是指一个通道,带有一个缓冲区。发送到一个缓冲通道只有在缓冲区满时才被阻塞。类似地,从缓冲通道接收的信息只有在缓冲区为空时才会被阻塞。

可以通过将额外的容量参数传递给make函数来创建缓冲通道,该函数指定缓冲区的大小。

语法:

ch := make(chan type, capacity)  

上述语法的容量应该大于0,以便通道具有缓冲区。默认情况下,无缓冲通道的容量为0,因此在之前创建通道时省略了容量参数。

示例代码:

package main

import (
	"fmt"
	"strconv"
	"time"
)

func main() {
	/*
        非缓存通道:make(chan T)
        缓存通道:make(chan T ,size)
            缓存通道,理解为是队列:

        非缓存,发送还是接受,都是阻塞的
        缓存通道,缓存区的数据满了,才会阻塞状态。。
	 */
    
	ch1 := make(chan int)           //非缓存的通道
	fmt.Println(len(ch1), cap(ch1)) //0 0
	//ch1 <- 100	//阻塞的,需要其他的goroutine解除阻塞,否则deadlock

	ch2 := make(chan int, 5)        //缓存的通道,缓存区大小是5
	fmt.Println(len(ch2), cap(ch2)) //0 5
	ch2 <- 100                      //
	fmt.Println(len(ch2), cap(ch2)) //1 5

	//ch2 <- 200
	//ch2 <- 300
	//ch2 <- 400
	//ch2 <- 500
	//ch2 <- 600
	fmt.Println("--------------")
    
	ch3 := make(chan string, 4)
	go sendData3(ch3)
	for {
		time.Sleep(1*time.Second)
		v, ok := <-ch3
		if !ok {
			fmt.Println("读完了,,", ok)
			break
		}
		fmt.Println("\t读取的数据是:", v)
	}

	fmt.Println("main...over...")
}

func sendData3(ch3 chan string) {
	for i := 0; i < 10; i++ {
		ch3 <- "数据" + strconv.Itoa(i)
		fmt.Println("子goroutine,写出第", i, "个数据")
	}
	close(ch3)
}

7.7、定向通道

双向通道

通道,channel,是用于实现goroutine之间的通信的。一个goroutine可以向通道中发送数据,另一条goroutine可以从该通道中获取数据。前面所学习的通道,都是既可以发送数据,也可以读取数据,我们又把这种通道叫做双向通道。

单向通道

/*
        双向:
            chan T -->
                chan <- data,写出数据,写
                data <- chan,获取数据,读
        单向:定向
            chan <- T,
                只支持写,
            <- chan T,
                只读
*/

示例代码:

package main

import "fmt"

func main() {

	ch1 := make(chan int) //双向,读,写
	//ch2 := make(chan <- int) // 单向,只写,不能读
	//ch3 := make(<- chan int) //单向,只读,不能写
	//ch1 <- 100
	//data :=<-ch1
	//ch2 <- 1000
	//data := <- ch2
	//fmt.Println(data)
	//	<-ch2 //invalid operation: <-ch2 (receive from send-only type chan<- int)
	//ch3 <- 100
	//	<-ch3
	//	ch3 <- 100 //invalid operation: ch3 <- 100 (send to receive-only type <-chan int)

	//go fun1(ch2)
	go fun1(ch1)
	data := <-ch1
	fmt.Println("fun1中写出的数据是:", data)

	//fun2(ch3)
	go fun2(ch1)
	ch1 <- 200
	fmt.Println("main。。over。。")
}

// 该函数接收,只写的通道
func fun1(ch chan<- int) {
	// 函数内部,对于ch只能写数据,不能读数据
	ch <- 100
	fmt.Println("fun1函数结束。。")
}

func fun2(ch <-chan int) {
	//函数内部,对于ch只能读数据,不能写数据
	data := <-ch
	fmt.Println("fun2函数,从ch中读取的数据是:", data)
}

定向通道在实际使用时一般作为参数限制函数内部的操作,实际上传递进来的通道一般还是双向通道。

7.8、time包中的通道

主要就是定时器,标准库中的Timer让用户可以定义自己的超时逻辑,尤其是在应对select处理多个channel的超时、单channel读写的超时等情形时尤为方便。

Timer是一次性的时间触发事件,这点与Ticker不同,Ticker是按一定时间间隔持续触发时间事件。

Timer常见的创建方式:

t:= time.NewTimer(d)
t:= time.AfterFunc(d, f)
c:= time.After(d)

虽然说创建方式不同,但是原理是相同的。

Timer有3个要素:

定时时间:就是那个d
触发动作:就是那个f
时间channel: 也就是t.C

7.8.1、time.NewTimer()

NewTimer()创建一个新的计时器,该计时器将在其通道上至少持续d之后发送当前时间。

它的返回值是一个Timer。

源代码:

// NewTimer creates a new Timer that will send
// the current time on its channel after at least duration d.
func NewTimer(d Duration) *Timer {
	c := make(chan Time, 1)
	t := &Timer{
		C: c,
		r: runtimeTimer{
			when: when(d),
			f:    sendTime,
			arg:  c,
		},
	}
	startTimer(&t.r)
	return t
}

通过源代码我们可以看出,首先创建一个channel,关联的类型为Time,然后创建了一个Timer并返回。

  • 用于在指定的Duration类型时间后调用函数或计算表达式。
  • 如果只是想指定时间之后执行,使用time.Sleep()
  • 使用NewTimer(),可以返回的Timer类型在计时器到期之前,取消该计时器
  • 直到使用<-timer.C发送一个值,该计时器才会过期

示例代码:

package main

import (
	"time"
	"fmt"
)

func main() {

	/*
		1.func NewTimer(d Duration) *Timer
			创建一个计时器:d时间以后触发,go触发计时器的方法比较特别,就是在计时器的channel中发送值
	 */
	//新建一个计时器:timer
	timer := time.NewTimer(3 * time.Second)
	fmt.Printf("%T\n", timer) //*time.Timer
	fmt.Println(time.Now())   //2019-08-15 10:41:21.800768 +0800 CST m=+0.000461190

	//此处在等待channel中的信号,执行此段代码时会阻塞3秒
	ch2 := timer.C     //<-chan time.Time
	fmt.Println(<-ch2) //2019-08-15 10:41:24.803471 +0800 CST m=+3.003225965

}

7.8.2、timer.Stop

示例代码:

package main

import (
	"fmt"
	"time"
)

func main() {

	/*
		1.func NewTimer(d Duration) *Timer
			创建一个计时器:d时间以后触发,go触发计时器的方法比较特别,就是在计时器的channel中发送值
	*/
	//新建一个计时器:timer
	timer := time.NewTimer(3 * time.Second)
	fmt.Printf("%T\n", timer) //*time.Timer
	fmt.Println(time.Now())   //2023-08-03 14:03:16.7591597 +0800 CST m=+0.002060301

	//此处在等待channel中的信号,执行此段代码时会阻塞3秒
	ch2 := timer.C     //<-chan time.Time
	fmt.Println(<-ch2) //2023-08-03 14:03:19.7699419 +0800 CST m=+3.012825001

	fmt.Println("-------------------------------")

	//新建计时器,一秒后触发

	timer2 := time.NewTimer(5 * time.Second)

	//新开启一个协程来处理触发后的事件
	go func() {
		//等触发时的信号
		<-timer2.C
		fmt.Println("Timer 2 结束。。")
	}()

	//由于上面的等待信号是在新线程中,所以代码会继续往下执行,停掉计时器
	time.Sleep(3 * time.Second)
	stop := timer2.Stop()

	if stop {
		fmt.Println("Timer 2 停止。。")
	}

}

7.8.3、time.After()

在等待持续时间之后,然后在返回的通道上发送当前时间。它相当于NewTimer(d).C。在计时器触发之前,垃圾收集器不会恢复底层计时器。如果效率有问题,使用NewTimer代替,并调用Timer。如果不再需要计时器,请停止。

源码:

// After waits for the duration to elapse and then sends the current time
// on the returned channel.
// It is equivalent to NewTimer(d).C.
// The underlying Timer is not recovered by the garbage collector
// until the timer fires. If efficiency is a concern, use NewTimer
// instead and call Timer.Stop if the timer is no longer needed.
func After(d Duration) <-chan Time {
	return NewTimer(d).C
}

示例代码:

package main

import (
	"fmt"
	"time"
)

func main() {

	/*
		func After(d Duration) <-chan Time
			返回一个通道:chan,存储的是d时间间隔后的当前时间。
	*/
	ch1 := time.After(3 * time.Second) //3s后
	fmt.Printf("%T\n", ch1)            // <-chan time.Time
	fmt.Println(time.Now())            //2023-08-03 14:09:21.065501 +0800 CST m=+0.003107401
	time2 := <-ch1
	fmt.Println(time2)			 	   //2023-08-03 14:09:24.0662873 +0800 CST m=+3.003884401

}

7.9、select语句

select 是 Go 中的一个控制结构。select 语句类似于 switch 语句,但是select会**随机执行**一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。

select语句的语法结构和switch语句很相似,也有case语句和default语句:

select {
    case communication clause  :
       statement(s);      
    case communication clause  :
       statement(s); 
    /* 你可以定义任意数量的 case */
    default : /* 可选 */
       statement(s);
}

说明:

  • 每个case都必须是一个通信

  • 所有channel表达式都会被求值

  • 所有被发送的表达式都会被求值

  • 如果有多个case都可以运行,select会随机公平地选出一个执行。其他不会执行。

  • 否则:

    如果有default子句,则执行该语句。

    如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

示例代码:

package main

import (
	"fmt"
	"time"
)

func main() {

	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		time.Sleep(3 * time.Second)
		ch1 <- 100
	}()

	go func() {
		time.Sleep(2 * time.Second)
		ch2 <- 100
	}()

	select {
	case num1 := <-ch1:
		fmt.Println("从通道1中获取的数据。。", num1)
	case num2, ok := <-ch2:
		if ok {
			fmt.Println("从通道2中获取的数据。。", num2)
		} else {
			fmt.Println("通道已经关闭")
		}
	default:
		fmt.Println("没有获取到数据")
	}
	fmt.Println("main...over...")

}

结合timer可以用来监听通道上的数据流动:

package main

import (
	"fmt"
	"time"
)

func main() {

	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		time.Sleep(3 * time.Second)
		ch1 <- 100
	}()

	select {
	case <-ch1:
		fmt.Println("case1可以执行")
	case <-ch2:
		fmt.Println("case2可以执行")
	case <-time.After(3 * time.Second):
		fmt.Println("case3执行。。timeout。。")
        //default:
        //	fmt.Println("default执行。。")
	}

}
  • 否则:

    如果有default子句,则执行该语句。

    如果没有default字句,select将阻塞,直到某个通信可以运行;Go不会重新对channel或值进行求值。

示例代码:

package main

import (
	"fmt"
	"time"
)

func main() {

	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		time.Sleep(3 * time.Second)
		ch1 <- 100
	}()

	go func() {
		time.Sleep(2 * time.Second)
		ch2 <- 100
	}()

	select {
	case num1 := <-ch1:
		fmt.Println("从通道1中获取的数据。。", num1)
	case num2, ok := <-ch2:
		if ok {
			fmt.Println("从通道2中获取的数据。。", num2)
		} else {
			fmt.Println("通道已经关闭")
		}
	default:
		fmt.Println("没有获取到数据")
	}
	fmt.Println("main...over...")

}

结合timer可以用来监听通道上的数据流动:

package main

import (
	"fmt"
	"time"
)

func main() {

	ch1 := make(chan int)
	ch2 := make(chan int)

	go func() {
		time.Sleep(3 * time.Second)
		ch1 <- 100
	}()

	select {
	case <-ch1:
		fmt.Println("case1可以执行")
	case <-ch2:
		fmt.Println("case2可以执行")
	case <-time.After(3 * time.Second):
		fmt.Println("case3执行。。timeout。。")
        //default:
        //	fmt.Println("default执行。。")
	}

}

你可能感兴趣的:(Go,golang,开发语言,并发,通道,后端)