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。

image-20230803231623395

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执行。。")
	}

}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/64889.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

鉴源论坛·观擎丨浅谈操作系统的适航符合性(上)

作者 | 蔡喁 上海控安可信软件创新研究院副院长 版块 | 鉴源论坛 观擎 社群 | 添加微信号“TICPShanghai”加入“上海控安51fusa安全社区” 01 源头和现状​​​​​​​ 在越来越多的国产机载系统研制中&#xff0c;操作系统软件的选择对后续开展研制以及适航举证活动带来…

码云 Gitee + Jenkins 配置教程

安装jdk 安装maven 安装Jenkins https://blog.csdn.net/minihuabei/article/details/132151292?csdn_share_tail%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22132151292%22%2C%22source%22%3A%22minihuabei%22%7D 插件安装 前往 Manage Jen…

基于Windows手动编译openssl和直接安装openssl

零、环境 win10-64位 VS2019 一、手动编译 前言&#xff1a;对于一般的开发人员而言&#xff0c;在 openssl 上下载已经编译好的 openssl 库&#xff0c;然后直接拿去用即可&#xff0c;&#xff0c;不用手动编译&#xff0c;{见下文直接安装}。。。对于一些开发人员&#…

Jmeter录制HTTPS脚本

Jmeter录制HTTPS脚本 文章目录 添加“HTTP代理服务器”设置浏览器代理证书导入存在问题 添加“HTTP代理服务器” 设置浏览器代理 保持端口一致 证书导入 点击一下启动让jmeter自动生成证书&#xff0c;放在bin目录下&#xff1a; 打开jmeter的SSL管理器选择刚刚生成的证书&…

# 关于Linux下的parted分区工具显示起始点为1049kB的问题解释

关于Linux下的parted分区工具显示起始点为1049kB的问题解释 文章目录 关于Linux下的parted分区工具显示起始点为1049kB的问题解释1 问题展示&#xff1a;2 原因3 修改为KiB方式显示4 最后 1 问题展示&#xff1a; kevinTM1701-b38cbc23:~$ sudo parted /dev/nvme1n1 GNU Part…

SAP 开发编辑界面-关闭助手

打开关闭助手时的开发界面如下&#xff1a; 关闭关闭助手后的界面如下&#xff1a; 菜单栏&#xff1a; 编辑--》修改操作--》关闭助手

会这个Python的测试员,工作都不会太差!

Python语言得天独厚的优势使之在业界的火热程度有增无减&#xff0c;尤其是在经历了互联网&#xff0c;物联网&#xff0c;云计算&#xff0c;大数据&#xff0c;人工智能等浪潮的推动下&#xff0c;其关注度&#xff0c;普适度一路走高。 对于测试人员来说&#xff0c;很多人…

【CSS】旋转中的视差效果

效果 index.html <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"/><meta http-equiv"X-UA-Compatible" content"IEedge"/><meta name"viewport" content"widthdevice-…

RK3568 lunch新增设备

rk3568 android9.0 &#xff0c;32位平台 1.进入devices/rockchip/rk356x/ 将rk3568_box_32 拷贝一份&#xff0c;命名为hdx6 2.打开vendorsetup.sh,添加lunch选项 add_lunch_combo hdx6-user add_lunch_combo hdx6-userdebug 3.进入hdx6&#xff0c;修改rk3568_box_32.mk…

Linux root用户执行修改密码命令,提示 Permission denied

问题 linux系统中&#xff08;ubuntu20&#xff09;&#xff0c;root用户下执行passwd命令&#xff0c;提示 passwd: Permission denied &#xff0c;如下图&#xff1a; 排查 1.执行 ll /usr/bin/passwd &#xff0c;查看文件权限是否正确&#xff0c;正常情况是 -rwsr-xr…

阿里云二级域名配置

阿里云二级域名配置 首先需要进入阿里云控制台的域名管理 1.选择域名点击解析 2.添加记录 3.选择A类型 4.主机记录设置【可以aa.bb或者aa.bb.cc】 到时候会变成&#xff1a;aa.bb.***.com 5.解析请求来源设置为默认 6.记录值 设置为要解析的服务器的ip地址 7.TTL 默认即…

MyCat水平分表

1.水平拆分案例场景 2.MyCat配置 这个表只是在 schema.xml配置的逻辑表&#xff0c;在具体的数据库里面是没有的 根据id的模确定数据存在哪个节点上&#xff01;&#xff01;

基于图像形态学处理的目标几何形状检测算法matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 5.算法完整程序工程 1.算法运行效果图预览 2.算法运行软件版本 matlab2022a 3.部分核心程序 .................................................... %二进制化图像 Images_bin imbinari…

7个月的测试经验,来面试居然开口要18K,我一问连5K都不值...

2021年8月份我入职了深圳某家创业公司&#xff0c;刚入职还是很兴奋的&#xff0c;到公司一看我傻了&#xff0c;公司除了我一个测试&#xff0c;公司的开发人员就只有3个前端2个后端还有2个UI&#xff0c;在粗略了解公司的业务后才发现是一个从零开始的项目&#xff0c;目前啥…

前端技术基础-css

前端技术基础-css【了解】 一、css理解 概念&#xff1a;CSS&#xff1a;C(cascade) SS(StyleSheet) &#xff0c;级联样式表。作用&#xff1a;对网页提供丰富的视觉效果&#xff0c;进行美化页面(需要在html页面基础上)样式规则&#xff1a;样式1&#xff1a;值1;样式2&…

项目中使用git vscode GitHubDesktopSetup-x64

一、使用git bash 1.使用git bash拉取gitee项目 1.在本地新建一个文件夹&#xff08;这个文件夹是用来存放从gitee上拉下来的项目的&#xff09; 2.在这个文件夹右键选择 git bash here 3.输入命令 git init (创建/初始化一个新的仓库) 4.输入命令 git remote add origin …

51单片机程序烧录教程

STC烧录步骤 &#xff08;1&#xff09;STC单片机烧录方式采用串口进行烧录程序&#xff0c;连接的方式如下图&#xff1a; &#xff08;2&#xff09;所以需要先确保USB转串口驱动是识别到&#xff0c;且驱动运行正常&#xff1b;是否可通过电脑的设备管理器查看驱动是否正常…

AVS3:跨多通道预测PMC

前面的文章中介绍了TSCPM&#xff0c;它是AVS3中用于intra模式的跨通道预测技术&#xff0c;它利用线性模型根据亮度重建像素预测色度像素&#xff0c; 跨通道预测技术用于去除不同通道间的冗余信息&#xff0c;TSCPM可以去除Y-Cb、Y-Cr通道间的冗余&#xff0c;然而却忽略了…

鉴源实验室丨SOME/IP协议安全攻击

作者 | 张昊晖 上海控安可信软件创新研究院工控网络安全组 来源 | 鉴源实验室 社群 | 添加微信号“TICPShanghai”加入“上海控安51fusa安全社区” 01 引 言 随着汽车行业对于数据通信的需求不断增加&#xff0c;SOME/IP作为支持汽车以太网进程和设备间通信的一种通信协议应…

C#调用百度翻译API自动将中文转化为英文,按行转换

我们可以使用百度翻译API获取到翻译结果 翻译API地址&#xff1a; http://api.fanyi.baidu.com/api/trans/vip/translate 一、新建窗体应用程序TranslatorDemo&#xff0c;将默认的Form1重命名为FormTranslator。 窗体FormTranslator设计器如图&#xff1a; 窗体设计器源代码…