Goroutine

也就是 Golang 的多线程

只需在函数调用语句前添加 go 关键字,就可创建并发执行单元。

go func(){
    // your code...
}

开发⼈人员无需了解任何执行细节,调度器会自动将其安排到合适的系统线程上执行。事实上,入口函数 main 就以 goroutine 运行。

关于调度器的说明

调度器不能保证多个 goroutine 执行次序,且进程退出时不会等待它们结束。

所以对于这段代码:

package main

import "fmt"

func main() {
	go printSomeThing(1)
	go printSomeThing(2)
}

func printSomeThing(sth int) {
	for i := 0; i < 100; i++ {
		fmt.Println(sth)
	}
	fmt.Println("finish", sth)
}

// 其实什么输出都没有,直接就给结束了

默认情况下,进程启动后仅允许一个系统线程服务于 goroutine(粗略理解为默认单核运行)。可使用环境变量或标准库函数 runtime.GOMAXPROCS 修改,让调度器用多个线程实现多核并行,而不仅仅是并发。

一个简单的多线程demo

把上面的例子稍微修改亿点点,就是一个简单的多线程demo

package main

import (
	"fmt"
	"sync"
)

func main() {
	// WaitGroup 用于管理线程阻塞
	wg := new(sync.WaitGroup)

	// 在 WaitGroup 中需要管理两个线程,即 Counter = 2
	wg.Add(2)

	// 1号线程
	go func() {
		defer wg.Done() //执行完成时,Counter - 1
		printSomeThing(1)
	}()
	// 2号线程
	go func() {
		defer wg.Done()
		printSomeThing(2)
	}()

	wg.Wait() // 用于将主线程阻塞,以免发生上面那样直接结束的情况,当 Counter = 0 时结束阻塞
}

//线程执行方法
func printSomeThing(sth int) {
	for i := 0; i < 100; i++ {
		fmt.Println(sth)
	}
	fmt.Println("finish", sth)
}

线程控制

停止当前执行的 goroutine

调用 runtime.Goexit 将立即终止当前 goroutine 执行,调度器确保所有已注册 defer 延迟调用被执行。

继续拿上面那个举例

package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main() {
	wg := new(sync.WaitGroup)

	wg.Add(1)

	go func() {
		defer wg.Done()
		printSomeThing(1)
	}()

	wg.Wait()
}

//线程执行方法
func printSomeThing(sth int) {
	for i := 0; i < 100; i++ {
		fmt.Println(sth) // 只会输出一次
		runtime.Gosched() // 停止 goroutine
	}
	fmt.Println("finish", sth) // 不会执行
}

放弃当前运行权,与其他线程重新争夺

和协程 yield 作用类似,Gosched 让出底层线程,将当前 goroutine 暂停,放回队列等待下次被调度执行。(这并不代表其他线程就一定能获得执行权)

package main

import (
	"fmt"
	"runtime"
	"sync"
)

func main() {
	wg := new(sync.WaitGroup)

	wg.Add(2)

	go func() {
		defer wg.Done()
		printSomeThing(1)
	}()
	go func() {
		defer wg.Done()
		printSomeThing(2)
	}()

	wg.Wait()
}

func printSomeThing(sth int) {
	for i := 0; i < 100; i++ {
		fmt.Println(sth)

		// 每当循环3次就放一次权
		if i%3 == 0 {
			runtime.Gosched()
		}
	}
	fmt.Println("finish", sth)
}

Channel

一个经典的例子

我们先来看一个经典的例子(售票)

package main

import (
	"fmt"
	"sync"
)

var i = 100 // 有100张票

func main() {
	wg := new(sync.WaitGroup)

	wg.Add(2) // 有两个售票窗口

	go func() {
		defer wg.Done()
		sell("routine2")

	}()
	go func() {
		defer wg.Done()
		sell("routine1")
	}()

	wg.Wait()
}

func sell(routineName string) {
	for {
		if i <= 0 {
			fmt.Println(routineName," said no more ticket")
			break
		}
		fmt.Println(routineName," sells", i)
		i--
	}
}

/*	运行结果:
...
routine1 sells 0
routine1 said no more ticket
routine2 sells 47
routine2 said no more ticket
*/

可以从上面的运行结果中看到 routine1刚说票卖完了,routine2 转手就卖了一张,这是线程不安全导致的问题 —— 在 golang 中是通过使用管道(channel)并联各个线程来解决这个问题的

语法

声明/开启

管道自声明以后,便是开着的,只有开着的管道才能向线程传输数据

使用 make 关键字来声明 channel: make (chan datatype)

关闭

使用 close(channel_name) 来关闭管道

判断管道的开闭

第一种方式是使用关键字 range,如果 channel 关闭,就会退出循环

for value := range channelName {
	// your code here...
}

除用 range 外,还可用 ok-idiom 模式判断 channel 是否关闭。

for {
	if d, ok := <-data; ok {
		fmt.Println(d)
	} else {
		break
	}
}

数据的发送与接收

data <- 1 发送数据到管道(假设管道名为 data,数据类型为 chan int
<- data 从管道接受数据(假设管道名为 data,数据类型为 chan int

向 closed channel 发送数据引发 panic 错误,接收立即返回零值。而 nil channel,无论收发都会被阻塞。

应用模式

下面说说常见的两种应用模式:

同步模式

同步模式,需要发送和接收配对。否则会被阻塞,直到另一方准备好后被唤醒。

package main

import "fmt"

func main() {
	data := make(chan int)  // 数据交换队列
	exit := make(chan bool) // 退出通知

	go func() {
		for d := range data { // 从队列迭代接收数据,直到 close 。
			fmt.Println(d)
		}
		fmt.Println("recv over.")
		exit <- true // 发出退出通知。
	}()

	data <- 1 // 发送数据。
	data <- 2
	data <- 3
	close(data) // 关闭队列。

	fmt.Println("send over.")
	<-exit // 等待退出通知。
}
/*	运行结果:
    1
    2
    send over.
    3
    recv over.
*/

异步模式

异步方式通过判断缓冲区来决定是否阻塞。如果缓冲区已满,发送被阻塞;缓冲区为空,接收被阻塞。

通常情况下,异步 channel 可减少排队阻塞,具备更高的效率。但应该考虑使用指针规避大对象拷贝,将多个元素打包,减小缓冲区大小等。

package main

import "fmt"

func main() {
	data := make(chan int, 3) // 缓冲区可以存储 3 个元素
	exit := make(chan bool)

	data <- 1 // 在缓冲区未满前,不会阻塞。
	data <- 2
	data <- 3

	go func() {
		for d := range data { // 在缓冲区未空前,不会阻塞。
			fmt.Println(d)
		}

		exit <- true
	}()

	data <- 4 // 如果缓冲区已满,阻塞。
	data <- 5
	close(data)

	<-exit
}

缓冲区是内部属性,并非类型构成要素。

var a, b chan int = make(chan int), make(chan int, 3) // 无论是否加了缓冲大小都能给  chan 类型赋值

内置函数 len 返回未被读取的缓冲元素数量,cap 返回缓冲区大小。

d1 := make(chan int)
d2 := make(chan int, 3)

d2 <- 1

fmt.Println(len(d1), cap(d1)) // 0 0
fmt.Println(len(d2), cap(d2)) // 1 3

回到前面的例子

有了 channel 以后,怎么改造前面例子中的代码能够线程安全呢? —— 利用异步阻塞

package main

import (
	"fmt"
	"sync"
)

func main() {
	wg := new(sync.WaitGroup)
	wg.Add(2)

	ticketChan := make(chan int, 1)
	ticketChan <- 100

	go func() {
		defer wg.Done()
		sell("routine1", ticketChan)
	}()

	go func() {
		defer wg.Done()
		sell("routine2", ticketChan)
	}()

	wg.Wait()

}

func sell(routineName string, ticketChan chan int) {
	//for {
	//	if left, ok := <-ticketChan; ok {
	//		if left <= 0 {
	//			fmt.Println(routineName, "said no more ticket")
	//			close(ticketChan)
	//			break
	//		}
	//		fmt.Println(routineName, "sells", left)
	//		left--
	//		ticketChan <- left
	//	} else {
	//		break
	//	}
	//}

	for left := range ticketChan {
		if left <= 0 {
			fmt.Println(routineName, "said no more ticket")
			close(ticketChan)
			break
		}
		fmt.Println(routineName, "sells", left)
		left--
		ticketChan <- left
	}
}


/*	运行结果:
	routine1 sells 3
	routine2 sells 2
	routine1 sells 1
	routine2 said no more ticket
*/

问题解决~


参考

Q.E.D.


此 生 无 悔 恋 真 白 ,来 世 愿 入 樱 花 庄 。