理解go语言编程-并发编程

在工程上,有两种最常见的并发通信模型:共享数据和消息。共享数据是指多个并发单元分别保持对同一个数据的引用,实现对该数据的共享。被共享的数据可能有多种形式,比如内存数据块、磁盘文件、网络数据等。在实际工程应用中最常见的无疑是内存了,也就是常说的共享内存。Go语言提供的是另一种通信模型,即以消息机制而非共享内存作为通信方式。

消息机制认为每个并发单元是自包含的、独立的个体,并且都有自己的变量,但在不同并发单元间这些变量不共享。每个并发单元的输入和输出只有一种,那就是消息。这有点类似于进程的概念,每个进程不会被其他进程打扰,它只做好自己的工作就可以了。不同进程间靠消息来通信,它们不会共享内存。

channel是Go语言在语言级别提供的goroutine间的通信方式。如果对Unix管道有所了解的话,就不难理解channel,可以将其认为是一种类型安全的管道。

我们通过ch <- 1语句向对应的channel中写入一个数据。在这个channel被读取前,这个操作是阻塞的。在所有的goroutine启动完成后,我们通过<-ch语句从10个channel中依次读取数据。在对应的channel写入数据前,这个操作也是阻塞的。这样,我们就用channel实现了类似锁的功能,进而保证了所有goroutine完成后主函数才返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
import "fmt"
func Count(ch chan int) {
ch <- 1
fmt.Println("Counting")
}
func main() {
chs := make([]chan int10)
for i := 0; i < 10; i++ {
chs[i] = make(chan int)
go Count(chs[i]) }
for _, ch := range(chs) {
<-ch // 阻塞等待,不然退出了
}
}

声明定义channel

1
2
var m map[string] chan bool // 声明一个map,元素是bool型的channel
ch := make(chan int)

Go语言直接在语言级别支持select关键字,用于处理异步IO问题。
select的用法与switch语言非常类似,由select开始一个新的选择块,每个选择条件由case语句来描述。与switch语句可以选择任何可使用相等比较的条件相比,select有比较多的限制,其中最大的一条限制就是每个case语句里必须是一个IO操作.
select不像switch,后面并不带判断条件,而是直接去查看case语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
select {
case <-chan1: // 如果chan1成功读到数据,则进行该case处理语句
case chan2 <- 1: // 如果成功向chan2写入数据,则进行该case处理语句
default: // 如果上面都没有成功,则进入default处理流程
}

// 随机输出0,1
ch := make(chan int, 1)
for {
select {
case ch <- 0:
case ch <- 1:
}
i := <-ch
fmt.Println("Value received:", i)
}

创建了一个大小为1024的int类型channel,即使没有读取方,写入方也可以一直往channel里写入,在缓冲区被填完之前都不会阻塞。

1
2
3
4
c := make(chan int, 1024)
for i := range c { // 循环读取
fmt.Println("Received:", i)
}

select的特点是只要其中一个case已经完成,程序就会继续往下执行,而不会考虑其他case的情况,很方便地解决超时问题。

1
2
3
4
5
6
7
8
9
10
11
12
// 首先,我们实现并执行一个匿名的超时等待函数
timeout := make(chan bool, 1)
go func() {
time.Sleep(1e9) // 等待1秒钟
timeout <- true
}()
// 然后我们把timeout这个channel利用起来
select
{
case <-ch: // 从ch中读取到数据
case <-timeout: // 一直没有从ch中读取到数据,但从timeout中读取到了数据
}

在Go语言中channel本身也是一个原生类型,与map之类的类型地位一样,因此channel本身在定义后也可以通过channel来传递。

1
2
3
4
5
6
7
8
9
10
11
type PipeData struct {
value int
handler func(int) int
next chan int
}

func handle(queue chan *PipeData) { // channel里面传递的是结构体,但结构体里面又包含了channel
for data := range queue {
data.next <- data.handler(data.value)
}
}

单向channel,非const指针具备const指针的所有功能,将一个指针设定为const就是明确告诉函数实现者不要试图对该指针进行修改。单向channel也是起到这样的一种契约作用

1
2
3
4
5
6
7
var ch1 chan int // ch1是一个正常的channel,不是单向的
var ch2 chan<- float64// ch2是单向channel,只用于写float64数据
var ch3 <-chan int // ch3是单向channel,只用于读取int数据

ch4 := make(chan int)
ch5 := <-chan int(ch4) // ch5就是一个单向的读取
channel ch6 := chan<- int(ch4) // ch6 是一个单向的写入channel

关闭channel,close(ch),x, ok := <-ch 这个用法与map中的按键获取value的过程比较类似,只需要看第二个bool返回值即可,如果返回值是false则表示ch已经被关闭。

runtime.GOMAXPROCS(16)设置多核,runtime.NumCPU()查看当前核数。Gosched()可以出让时间片。

Go语言包中的sync包提供了两种锁类型:sync.Mutex和sync.RWMutex。从RWMutex的实现看,RWMutex类型其实组合了Mutex

1
2
3
4
5
6
7
8
9
10
11
12
13
14
type RWMutex struct {
w Mutex
writerSem uint32
readerSem uint32
readerCount int32
readerWait int32
}

var l sync.Mutex
func foo() {
l.Lock()
defer l.Unlock()
//...
}

go语言提供了一个Once类型来保证全局的唯一性操作,sync包中还包含一个atomic子包,func CompareAndSwapUint64(val *uint64, old, new uint64) (swapped bool)

1
2
3
4
5
6
7
8
9
10
11
12
13
var a string
var once sync.Once
func setup() {
a = "hello, world"
}
func doprint() {
once.Do(setup)
print(a)
}
func twoprint() {
go doprint()
go doprint()
}

nephen wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客!
坚持原创技术分享,您的支持将鼓励我继续创作!