Go语言常用的并发模式(上)

Confinement

该模式用于处理数据限制问题,类似于生产者和消费者模式。使用channel的方式通过共享信息的方式进行。有一个协程专门负责生产,另外一个协程负责接收数据。代码中使用随机的时间模拟实际情况中耗时部分。

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	chanOwner := func(n int) <-chan int {
		results := make(chan int, 5)
		go func() {
			defer close(results)
			for i := 0; i < n; i++ {
				results <- i
				fmt.Printf("produce %d\n", i)
				t := rand.Intn(2000)
				time.Sleep(time.Duration(t) * time.Millisecond) // 随机睡眠0-2秒
			}
		}()
		return results
	}

	consumer := func(results <-chan int) {
		for result := range results {
			t := rand.Intn(2000)
			time.Sleep(time.Duration(t) * time.Millisecond) // 随机睡眠0-2秒
			fmt.Printf("Received %d\n", result)
		}
		fmt.Println("Done receiving")
	}
	results := chanOwner(5)
	consumer(results)
}
/*
代码输出:
produce 0
produce 1
Received 0
produce 2
Received 1
produce 3
produce 4
Received 2
Received 3
Received 4
Done receiving
*/

这种模式可以根据实际情况定义生产和消费的方式,不用担心出现数据竞争的问题。

for-select循环

最常规的模式:

for { // 死循环
	select {
	// 在这里采取有关操作
	}
}

通过迭代的方式把数据写入channel

for _, s := range []string{"a", "b", "c"} {
	select {
	case <-done:  // 这里是完成条件的标记
		return
	case stringStream <- s:  // 在这里写入数据
	}
}

死循环等待结束标记。
这种方式会尽可能早的结束工作,只要done信号到达,立刻终止:

for {
	select {
	case <-done:
		return
	default:
	}
	// do something here
}

另一个等价方式

for {
	select {
	case <-done:
		return
	default:
	    // do something here
	}
}

防止goroutine泄露

尽管goroutine是一种轻量级的进程,而且一般不必担心使用太多的协程导致内存的问题,Go语言的有自动回收机制;但是,在某些情况下确实需要考虑出现某些协程一直无法回收的问题,这可还会引发一些其它的不良后果,给出下面的例子:

doWork := func(strings <-chan string) <-chan interface{} {
	completed := make(chan interface{})
	go func() {
		defer fmt.Println("doWork exited")
		defer close(completed)
		for s := range strings {
			// Do something
			fmt.Println(s)
		}
	}()
	return completed
}
doWork(nil)
fmt.Println("Done")

在上述的代码中,doWork会永远阻塞,因为空的string channel不会有任何内容输出。本例子中的开销可能很小,但是在实际的工程中,这可能会引发大的问题。解决方法是通过父协程结束子协程。通过父协程给子协程发射终止的信号,使得子协程自动终止。

给出一般的操作方式:

   doWork := func(
		done <-chan interface{},  // 终止的信号
		strings <-chan string,    // 等待读取的字符串
	) <-chan interface{} {        // 子协程返回自己终止的信号
		terminated := make(chan interface{})
		go func() {
			defer fmt.Println("doWork exited")
			defer close(terminated)
			for {
				select {
				case s := <-strings:
					// do something
					fmt.Println(s)
				case <-done:  // 如果关闭,则直接执行return
					return
				}
			}
		}()
		return terminated
	}

	done := make(chan interface{})
	terminated := doWork(done, nil)  // 接收子协程的终止信号
	go func() {
		// cancel the operation after 1 second
		time.Sleep(time.Second)
		fmt.Println("Canceling doWork goroutine...")
		close(done)  // 关闭后相当于不存在阻塞的情况了。。。
	}()
	<-terminated  // 在这里等待子协程的终止
	fmt.Println("Done.")

上述代码是读数据的例子,下面给出写数据时发生协程泄露的例子:

package main

import (
	"fmt"
	"math/rand"
)

func main() {
	newRandStream := func() <-chan int {
		randStream := make(chan int)
		go func() {
			defer fmt.Println("newRandStream closure exited.")
			defer close(randStream)
			for {
				randStream <- rand.Int()
			}
		}()
		return randStream
	}

	randStream := newRandStream()
	n := 3
	fmt.Printf("%d random ints:\n", n)
	for i := 0; i < n; i++ {
		fmt.Printf("%d: %d\n", i, <-randStream)  // 注意这种使用方式,也是合法的
	}
}
/*
输出结果:
3 random ints:
0: 5577006791947779410
1: 8674665223082153551
2: 6129484611666145821
*/

上述代码中,randStream始终没有结束,出现了协程泄露。。

改进方案:和写数据的方式类似,通过父协程给子协程发射结束信号即可。代码方案:

package main

import (
	"fmt"
	"math/rand"
	"time"
)

func main() {
	newRandStream := func(done <-chan interface{}) <-chan int {
		randStream := make(chan int)
		go func() {
			defer fmt.Println("newRandStream closure exited...")
			defer close(randStream)
			for {
				select {
				case randStream <- rand.Int():
				case <-done:
					return
				}
			}
		}()
		return randStream
	}

	done := make(chan interface{})
	randStream := newRandStream(done)
	n := 3
	fmt.Printf("%d random ints\n", n)
	for i := 0; i < 3; i++ {
		fmt.Printf("%d:%d\n", n, <-randStream)
	}
	close(done)
	// 等待同步看效果,不用等待也可以正常结束的,这里仅仅是为了显式说明一下
	time.Sleep(time.Second)
}
/*
输出结果:
3 random ints
0:5577006791947779410
1:8674665223082153551
2:6129484611666145821
newRandStream closure exited...
*/

or-channel方式

这种模式的方式是:把多个channeldone连接到一个done上,如果这些channel中任何至少一个关闭,则关闭这个done。 代码中,如果出现一个任意一个协程结束,那么就出现终止信号。终止信号出现后,如果有协程没有结束,他们会继续执行,代码只是检测是否有协程终止,而不主动结束协程。
给出实例代码:

package main

import (
	"fmt"
	"time"
)

func main() {
	// 从这里传入各个channel的done
	var or func(channels ...<-chan interface{}) <-chan interface{}
	or = func(channels ...<-chan interface{}) <-chan interface{} {
		switch (len(channels)) {
		case 0: // 递归结束的条件
			return nil
		case 1: // 只有一个直接返回
			return channels[0]
		}
		orDone := make(chan interface{}) // 这是自己的标记
		// 可以理解成一棵协程树,父节点需要孩子节点结束才能销毁。。。
		// 在这里进行协程孩子节点的拓展,WTF好难理解。。。。。
		// 在匿名函数中,如果一个channel的任何一个子channel结束,那么匿名函数的阻塞就会立刻结束,
		// 之后会执行内部的defer操作,然后return一个关闭了的channel,相当于解除阻塞
		go func() {
			defer close(orDone) // 结束的时候释放本身的done信号
			switch len(channels) {
			case 2:
				select {
				case <-channels[0]:
				case <-channels[1]:
				}
			default:
				select {
				// 如果case失败,则进行default,需要再判断一下,防止此次突然有结束的信号了
				case <-channels[0]:
				case <-channels[1]:
				case <-channels[2]:
				// 在这里追加父节点的协程终止信号,因为这是一棵或的树,只要有一个节点成功就可以释放掉
				// 因此把父节点一起传入,只要有一个释放掉,父节点的channel就立刻进行释放......好机智的操作
				// 这里追加自己的orDone,是为了`
				case <-or(append(channels[3:], orDone)...): // 注意使用...符号
				}
			}
		}()
		return orDone
	}

	sig := func(after time.Duration) <-chan interface{} {
		c := make(chan interface{})  
		go func() {
			defer close(c) // 所在的goroutine结束后close,使用时间模拟工作时间
			time.Sleep(after)
		}()
		return c
	}

	start := time.Now()
	<-or(
		sig(2*time.Hour),
		sig(5*time.Minute),
		sig(1*time.Second),
		sig(1*time.Hour),
		sig(1*time.Minute),
	)
	fmt.Printf("done after %v\n", time.Since(start))
}
/*
输出结果:
done after 1.000182106s
*/

从上述看出,仅仅执行到结果最短的那个,相当于一个“或”操作。代码采用了尾递归的方式,因为select方式无法预判channel的数量,而循环的方式需要处理大量的阻塞问题,不如尾递归的方式简洁。

上述代码最后的递归中,有一个地方不太理解:代码递归的过程中为什么要加入orDone?希望有明白的同学可以解释一下!

你可能感兴趣的:(Go语言笔记)