Skip to content
鼓励作者:欢迎打赏犒劳

多线程

  • 并行是两个队列同时使用两台咖啡机
  • 并发是两个队列交替使用一台咖啡机

协程初体验

其实上就类似于new一个线程,用到了go关键字

go
package main

import (
	"fmt"
	"time"
)

func newTask() {
	i := 0
	for {
		i++
		fmt.Printf("new goroutine: i = %d\n", i)
		time.Sleep(1 * time.Second) //延时1s
	}
}

func main() {
	//创建一个 goroutine,启动另外一个任务
	go newTask()

	i := 0
	//main goroutine 循环打印
	for {
		i++
		fmt.Printf("main goroutine: i = %d\n", i)
		time.Sleep(1 * time.Second) //延时1s
	}
	/**
	main goroutine: i = 1
	new goroutine: i = 1
	new goroutine: i = 2
	main goroutine: i = 2
	new goroutine: i = 3
	main goroutine: i = 3
	*/
}

主线程退出,携程也退出

和java一样

go
package main

import (
	"fmt"
	"time"
)

func newTask() {
	i := 0
	for {
		i++
		fmt.Printf("new goroutine: i = %d\n", i)
		time.Sleep(1 * time.Second) //延时1s
	}
}

func main() {
	//创建一个 goroutine,启动另外一个任务
	go newTask()
	fmt.Println("main goroutine exit")
}

Gosched-让出时间片

类似于java中的Thread.yield(),"我主动让出CPU,请调度其他可运行的goroutine,但我也可能立即被再次选中"

go
package main

import (
	"fmt"
	"runtime"
)

func main() {
	//创建一个goroutine
	go func(s string) {
		for i := 0; i < 2; i++ {
			fmt.Println(s)
			//time.Sleep(1 * time.Second) //延时1s
		}
	}("world")

	for i := 0; i < 2; i++ {
		//遇到runtime.Gosched()只是让出时间片而已,但它可能会再次被调度器选中,而不是"一定"由其他 goroutine 获得执行权
		runtime.Gosched()
		fmt.Println("hello")
	}
}

Goexit-终止线程

go
package main

import (
	"fmt"
	"runtime"
)

func main() {
	go func() {
		defer fmt.Println("A.defer")

		func() {
			defer fmt.Println("B.defer")
			runtime.Goexit() // 终止当前 goroutine, import "runtime"
			fmt.Println("B") // 不会执行
		}()

		fmt.Println("A") // 不会执行
	}() //别忘了()

	//死循环,目的不让主goroutine结束
	for {
	}
}

GOMAXPROCS-设置现成数

了解即可,一般不用,其实就是设置 Go 程序运行时可以使用的最大 CPU 核心数:

  • GOMAXPROCS(1) 强制所有 goroutine 在单一线程上交替执行,输出显示长时间的 1 和长时间的 0

  • GOMAXPROCS(2) 允许两个线程并行执行 goroutine,输出显示 0 和 1 更频繁地交替出现

go
package main

import (
	"fmt"
	"runtime"
)

func main() {
	//n := runtime.GOMAXPROCS(1) //打印结果:111111111111111111110000000000000000000011111...
	n := runtime.GOMAXPROCS(2) //打印结果:010101010101010101011001100101011010010100110...
	fmt.Printf("n = %d\n", n)

	for {
		go fmt.Print(0)
		fmt.Print(1)
	}
}

注意:

调用runtime.GOMAXPROCS(2)设置最大使用2个CPU核心,并返回之前设置的CPU核心数(假设之前是16)

然后调用runtime.GOMAXPROCS(0)查询当前设置,返回2,因为刚刚设置为2

所以:

  • n = 16 表示在设置2之前,程序使用的是16个CPU核心
  • n2 = 2 表示当前设置的是2个CPU核心

注意:runtime.GOMAXPROCS(0)只是查询,不改变设置。

go
n := runtime.GOMAXPROCS(2) 
n2 := runtime.GOMAXPROCS(0)
fmt.Printf("n = %d\n", n) //16
fmt.Printf("n2 = %d\n", n2) //2

无缓存channel

注意:发送和接收必须同时准备好,否则写也会阻塞;channel没数据,取也会阻塞

注意:无缓存channel只能存放一个值

初体验

go
package main

import (
	"fmt"
)

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

	go func() {
		defer fmt.Println("子协程结束")

		fmt.Println("子协程正在运行……")

		c <- 666 //666发送到c
	}()

	num := <-c //从c中接收数据,并赋值给num

	fmt.Println("num = ", num)
	fmt.Println("main协程结束")

	/***
	子协程正在运行……
	子协程结束
	num =  666
	main协程结束
	*/
}

生产者-消费者模式

也很好理解,一个往队列塞数据,一个取数据,如果队列没数据则阻塞直到队列关闭。

go
package main

import (
	"fmt"
)

// 生产者-消费者模式
func producer(ch chan<- int) {
	for i := 0; i < 5; i++ {
		ch <- i // 发送数据
		fmt.Println("producer ", i)
	}
	close(ch) // 关闭Channel
}

func consumer(ch <-chan int) {
	for num := range ch { // 循环接收直到Channel关闭
		fmt.Println("Received:", num)
	}
}

func main() {
	ch := make(chan int)
	go producer(ch)
	consumer(ch)
}

再来一个例子,就很好理解了。

go
package main

import (
	"fmt"
	"time"
)

var (
	count = 3
)

func main() {
	// 创建一个无缓存的channel
	ch := make(chan int, 0)

	// len(ch)缓冲区剩余数据个数,cap(ch)缓冲区大小
	fmt.Printf("len(ch) = %d, cap(ch) = %d\n", len(ch), cap(ch))

	// 新建子进程
	go func() {
		for i := 0; i < count; i++ {
			fmt.Printf("子进程:i=%d\n", i)
			ch <- i // 往chan写内容
			fmt.Printf("子进程:i=%d成功\n", i)
		}
	}()

	// 延时
	time.Sleep(time.Second * 2)

	//
	for i := 0; i < count; i++ {
		fmt.Printf("主进程:开始读数据\n")
		num := <-ch
		fmt.Printf("主进程:num = %d\n", num)
	}
}

/**
len(ch) = 0, cap(ch) = 0
子进程:i=0
主进程:开始读数据
主进程:num = 0
主进程:开始读数据
子进程:i=0成功
子进程:i=1
子进程:i=1成功
子进程:i=2
主进程:num = 1
主进程:开始读数据
主进程:num = 2
*/

有缓存channel

就相当于可以存放多个值的队列

go
package main

import (
	"fmt"
	"time"
)

func main() {
	c := make(chan int, 3) //带缓冲的通道

	//内置函数 len 返回未被读取的缓冲元素数量, cap 返回缓冲区大小
	fmt.Printf("len(c)=%d, cap(c)=%d\n", len(c), cap(c))

	go func() {
		defer fmt.Println("子协程结束")
		for i := 0; i < 6; i++ {
			c <- i
			fmt.Printf("子协程正在运行[%d]: len(c)=%d, cap(c)=%d\n", i, len(c), cap(c))
		}
	}()

	time.Sleep(2 * time.Second) //延时2s
	for i := 0; i < 6; i++ {
		num := <-c //从c中接收数据,并赋值给num
		fmt.Println("num = ", num)
	}
	fmt.Println("main协程结束")

	/**
	len(c)=0, cap(c)=3
	子协程正在运行[0]: len(c)=1, cap(c)=3
	子协程正在运行[1]: len(c)=2, cap(c)=3
	子协程正在运行[2]: len(c)=3, cap(c)=3
	num =  0
	num =  1
	num =  2
	num =  3
	子协程正在运行[3]: len(c)=2, cap(c)=3
	子协程正在运行[4]: len(c)=0, cap(c)=3
	num =  4
	num =  5
	子协程正在运行[5]: len(c)=1, cap(c)=3
	子协程结束
	main协程结束
	*/
}

关闭通道

  • channel不像文件一样需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的,才去关闭channel;
  • 关闭channel后,无法向channel 再发送数据(引发 panic 错误后导致接收立即返回零值);
  • 关闭channel后,可以继续向channel接收数据;
go
package main

import (
	"fmt"
)

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

	go func() {
		for i := 0; i < 5; i++ {
			c <- i
		}
		//把 close(c) 注释掉,程序会一直阻塞在 if data, ok := <-c; ok 那一行
		//Go 运行时有一个内置的死锁检测器,当发现所有 Goroutine 都处于等待状态时(如都在等待从 Channel 接收数据,但没有 Goroutine 发送数据),
		//会报错:fatal error: all goroutines are asleep - deadlock!
		close(c)
	}()

	for {
		//ok为true说明channel没有关闭,为false说明管道已经关闭
		if data, ok := <-c; ok {
			fmt.Println(data)
		} else {
			break
		}
	}

	fmt.Println("Finished")
}

单通道channel

默认情况下,通道是双向的,也就是,既可以往里面发送数据也可以同里面接收数据。

但是,我们经常见一个通道作为参数进行传递而值希望对方是单向使用的,要么只让它发送数据,要么只让它接收数据,这时候我们可以指定通道的方向。

单向channel变量的声明非常简单,如下:

go
var ch1 chan int       // ch1是一个正常的channel,不是单向的
var ch2 chan<- float64 // ch2是单向channel,只用于写float64数据
var ch3 <-chan int     // ch3是单向channel,只用于读取int数据
  • chan<- 表示数据进入管道,要把数据写进管道,对于调用者就是输出。
  • <-chan 表示数据从管道出来,对于调用者就是得到管道的数据,当然就是输入。

可以将 channel 隐式转换为单向队列,只收或只发,不能将单向 channel 转换为普通 channel:

go
c := make(chan int, 3)
var send chan<- int = c // send-only
var recv <-chan int = c // receive-only

单次定时器-Timer

Timer是一个定时器,代表未来的一个单一事件,你可以告诉timer你要等待多长时间,它提供一个channel,在将来的那个时间向channel提供了一个时间值。

初体验

go
package main

import (
	"fmt"
	"time"
)

// 定时器重置
func main() {
	//创建定时器,2秒后,定时器就会向自己的channel发送一个time.Time类型的元素值,值就是当前时间
	timer1 := time.NewTimer(time.Second * 2)
	t1 := time.Now() //当前时间
	fmt.Printf("当前时间: %v\n", t1)

	//如果没值的话则会阻塞
	t2 := <-timer1.C
	fmt.Printf("定时器到期时间: %v\n", t2)
}

延迟执行

3种方法都可以,sleep更简单一些

go
package main

import (
	"fmt"
	"time"
)

func main() {
	//如果只是想单纯的等待的话,可以使用 time.Sleep 来实现
	timer2 := time.NewTimer(time.Second * 2)
	<-timer2.C
	fmt.Println("2s后")

	time.Sleep(time.Second * 2)
	fmt.Println("再一次2s后")

	<-time.After(time.Second * 2)
	fmt.Println("再再一次2s后")
}

停止定时器

也很好理解,停止定时器,channel也就没有值,会一直阻塞,主线程执行完了,代码也就停了

go
package main

import (
	"fmt"
	"time"
)

// 定时器重置
func main() {
	timer3 := time.NewTimer(time.Second)
	go func() {
		<-timer3.C
		fmt.Println("Timer 3 expired")
	}()

	stop := timer3.Stop() //停止定时器
	if stop {
		fmt.Println("Timer 3 stopped")
	}
}

重置定时器

go
package main

import (
	"fmt"
	"time"
)

// 定时器重置
func main() {
	fmt.Println("before")
	timer4 := time.NewTimer(time.Second * 5) //原来设置3s
	timer4.Reset(time.Second * 1)            //重新设置时间
	<-timer4.C
	fmt.Println("after")
}

循环定时器-Ticker

Ticker是一个定时触发的计时器,它会以一个间隔(interval)往channel发送一个事件(当前时间),而channel的接收者可以以固定的时间间隔从channel中读取事件。

初体验

go

package main

import (
	"fmt"
	"time"
)

// 定时器重置
func main() {
	//创建定时器,每隔1秒后,定时器就会给channel发送一个事件(当前时间)
	ticker := time.NewTicker(time.Second * 1)

	i := 0
	go func() {
		for { //循环
			x := <-ticker.C
			i++
			fmt.Printf("i = %d; x = %v \n", i, x)

			if i == 5 {
				ticker.Stop() //停止定时器
			}
		}
	}() //别忘了()

	//死循环,特地不让main goroutine结束
	for {
	}

	/***
	i = 1; x = 2025-09-15 20:20:08.4370563 +0800 CST m=+1.000000001
	i = 2; x = 2025-09-15 20:20:09.4370563 +0800 CST m=+2.000000001
	i = 3; x = 2025-09-15 20:20:10.4370563 +0800 CST m=+3.000000001
	i = 4; x = 2025-09-15 20:20:11.4370563 +0800 CST m=+4.000000001
	i = 5; x = 2025-09-15 20:20:12.4370563 +0800 CST m=+5.000000001
	*/
}

select 监听channel

Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。

select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。

与switch语句可以选择任何可使用相等比较的条件相比, select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作,大致的结构如下:

go
select {
	case <-chan1:
	// 如果chan1成功读到数据,则进行该case处理语句
	case chan2 <- 1:
	// 如果成功向chan2写入数据,则进行该case处理语句
	default:
		// 如果上面都没有成功,则进入default处理流程
	}

下面的代码其实也很好理解,就是一个协程去操作channel,主线程for循环的select监听,channel的写入和取出。

go
package main

import "fmt"

func main() {
	ch := make(chan int)
	quit := make(chan bool)
	// 消费者,从channel中读取内容
	// 新建协程
	go func() {
		for i := 0; i < 8; i++ {
			num := <-ch
			fmt.Println("", num)
		}
		// 可以停止
		quit <- true
	}()

	// 生产者,产生数字,写入channel
	fibonacci(ch, quit)
}

// 原生斐波那契数列
func fibonacci(ch chan<- int, quit <-chan bool) {
	x, y := 1, 1
	for {
		//监听channel数据的流动
		select {
		case ch <- x:
			x, y = y, x+y
			break
		case flag := <-quit:
			fmt.Println("flag =", flag)
			return
		default:
			fmt.Println("default.............")
		}

	}
}

如有转载或 CV 的请标注本站原文地址