golang如何更好地使用channel

最近学习了《GO语言并发之道》这本书,获益匪浅,其中channel方面的知识了解了更多,主要是以下几点:

1. channel在不同条件下读写,会有不同的行为形式,后面会通过实验说明;

2. channel使用完是要close的,而一般由写端创建和关闭,不要在读端关闭,上面的实验结果会说明这样做的原因;

3. channel结合gorouting有很多的实践方式,还可以构造流式处理。

先来看看实验代码,首先看看读channel的代码

package read

import "fmt"

// 阻塞
func Nil() {
	var ch chan interface{}
	<-ch
}

// 阻塞
func Empty() {
	ch := make(chan interface{})
	<-ch
}

// 阻塞
func CloseNotEmpty() {
	ch := make(chan interface{})
	ch <- 10
	close(ch)
	fmt.Println(<-ch)
}

// 读取0值
func CloseEmpty() {
	ch := make(chan interface{})
	close(ch)
	fmt.Println(<-ch)
}

函数主要包括如下内容,实验结果已经在代码中进行了注释:

Nil:读取nil channel

Empty:读取没有数据的channel

CloseNotEmpty:读取被关闭的非空channel

CloseEmpty:读取被关闭的空channel

然后是写channel的代码

package write

// 阻塞
func Nil() {
	var ch chan interface{}
	ch <- 10
}

// 阻塞
func Full() {
	ch := make(chan interface{}, 1)
	ch <- 10
	ch <- 11
}

// 可写入
func NotFull() {
	ch := make(chan interface{}, 1)
	ch <- 10
}

// panic send on closed channel
func Closed() {
	ch := make(chan interface{}, 1)
	close(ch)
	ch <- 10
}

函数说明如下,实验结果也已经在注释中给出:

Nil:向nil channel写入数据

Full:向填满的channel写入数据

NotFull:向未填满的channel写入数据

Closed:向已经关闭的channel写入数据

最后是实验关闭channel的函数:

package close

//panic: close of nil channel
func Nil() {
	var ch chan interface{}
	close(ch)
}

//panic: close of closed channel
func Closed() {
	ch := make(chan interface{})
	close(ch)
	close(ch)
}

函数说明如下,结果注释已给出:

Nil:关闭nil channel

Closed:关闭已经关闭的channel

从上面的实验结果可以看出,如果从读端关闭channel,写端去写关闭的channel时,会出现panic,导致整个程序终止。而从写端关闭channel,读取时,返回0值或阻塞,至少有迹可循,方便我们调试代码。

下面给出了结合channel如何编写比较好的gorouting的一个例子

package main

import (
	"fmt"
	"strings"
	"time"
)

func toUpper(done <-chan interface{}, str string) <-chan string {
	strChan := make(chan string)

	go func() {
		defer close(strChan)

		for {
			select {
			case <-done:
				return
			case strChan <- strings.ToUpper(str):
			}
		}
	}()

	return strChan
}

func main() {
	done := make(chan interface{})
	defer close(done)

	toUpperChan := toUpper(done, "aaBBcc")
	fmt.Println(<-toUpperChan)
}

toUpper函数接受两个参数,第一个参数是一个可读channel,用来终止协程,str是需要转换为大写的字符串。返回值是一个可读的channel,这个channel作为协程返回数据的媒介,如果用过Java,也可以理解为这个channel类似于Future对象,也可以理解为是协程的join点。toUpper函数逻辑比较简单,一个比较大的select,读取done channel并向strChan写入大写转换后的字符串。遵循上面提到的原则,作为strChan的写端,toUpper函数创建strChan,并通过defer延迟执行,在函数return后,关闭strChan。

主函数创建done channel并传递给toUpper,当主函数退出时,通过关闭done channel,toUpper函数中将从done读取到nil,从而退出协程函数。主函数调用toUpper函数,通过返回的channel,读取处理后的字符串。

可以看到,采用这种方式,使得协程的管理更加简单,还能保证channel正常关闭,取得协程返回值,避免了channel和gorouting的泄露。下面这段代码,更加华丽

package main

import (
	"fmt"
	"strings"
	"time"
)

func str(done <-chan interface{}, str string) <-chan string {
	strChan := make(chan string)

	go func() {
		time.Sleep(2 * time.Second)

		defer close(strChan)

		select {
		case <-done:
			return
		case strChan <- str:
		}
	}()

	return strChan
}

func toUpper(done <-chan interface{}, str <-chan string) <-chan string {
	strChan := make(chan string)

	go func() {
		time.Sleep(2 * time.Second)

		defer close(strChan)

		for {
			select {
			case <-done:
				return
			case strChan <- strings.ToUpper(<-str):
			}
		}
	}()

	return strChan
}

func appendStr(done <-chan interface{}, oldStr <-chan string, appendStr string) <-chan string {
	strChan := make(chan string)

	go func() {
		time.Sleep(2 * time.Second)

		defer close(strChan)

		for {
			select {
			case <-done:
				return
			case strChan <- <-oldStr + appendStr:
			}
		}
	}()

	return strChan
}

func main() {
	done := make(chan interface{})

	strChan := str(done, "aaa111bbb222CCC")
	fmt.Println(1)

	upperChan := toUpper(done, strChan)
	fmt.Println(2)

	appendStrChan := appendStr(done, upperChan, "dddd")
	fmt.Println(3)

	fmt.Println(<-appendStrChan)

	close(done)

	done = make(chan interface{})
	fmt.Println(<-appendStr(done, toUpper(done, str(done, "aaa111bbb222CCC")), "dddd"))
	close(done)
}

有三个函数,str,toUpper和appendStr,这三个函数都采用了上面的模式,不同的是toUpper和appendStr这两个函数的第二个输入参数使用了一个可读channel,这样,我们可以构造一个异步的流式处理流程,str作为这个流处理的入口,参数使用原生类型,toUpper和appendStr作为流处理的处理节点,使用通道作为参数。

main函数中给出了两种组装流处理流程的方式,第一种方式,通过调用每一个函数,然后将返回的channel作为下一个处理节点输入进行传参,中间添加了打印,运行时可以看到,函数调用是立刻返回的,在最后获取结果时进行等待;第二种方式更加简洁,基于函数式编程构造了流处理。

关闭done channel可以起到广播的作用,所有等待在读取done的协程,都会读到nil,从而保证流处理过程中使用的所有协程都能退出。

golang的高并发主要靠channel和gorouting实现,深刻理解两者的使用方式,将有利于我们写出更高效的golang代码。

 

<div class="post-text" itemprop="text"> <p><strong>TL;DR</strong> - What is the proper way to close a <code>golang.org/x/crypto/ssh</code> session freeing all resources? </p> <p>My investigation thus far:</p> <p>The <code>golang.org/x/crypto/ssh</code> <code>*Session</code> has a <code>Close()</code> function which calls the <code>*Channel</code> <code>Close()</code> function which sends a message (I'm guessing to the remote server) to close, but I don't see anything about closing other resources like the pipe returned from the <code>*Session</code> <code>StdoutPipe()</code> function. </p> <p>Looking at the <code>*Session</code> <code>Wait()</code> code, I see that the <code>*Session stdinPipeWriter</code> is closed but nothing about the <code>stdoutPipe</code>.</p> <p>This package feels a lot like the <code>os/exec</code> package which guarantees that using the <code>os/exec Wait()</code> function will clean up all the resources. Doing some light digging there shows some similarities in the <code>Wait()</code> functions. Both use the following construct to report errors on <code>io.Copy</code> calls to their stdout, stderr, stdin readers/writers (well if I'm reading this correctly actually only one error) - crypto package shown:</p> <pre><code>var copyError error for _ = range s.copyFuncs { if err := <-s.errors; err != nil && copyError == nil { copyError = err } } </code></pre> <p>But the <code>os/exec</code> <code>Wait()</code> also calls this close descriptor method</p> <pre><code>c.closeDescriptors(c.closeAfterWait) </code></pre> <p>which is just calling the close method on a slice of <code>io.Closer</code> interfaces:</p> <pre><code>func (c *Cmd) closeDescriptors(closers []io.Closer) { for _, fd := range closers { fd.Close() } } </code></pre> <p>when <code>os/exec</code> creates the pipe, it tracks what needs closing:</p> <pre><code>func (c *Cmd) StdoutPipe() (io.ReadCloser, error) { if c.Stdout != nil { return nil, errors.New("exec: Stdout already set") } if c.Process != nil { return nil, errors.New("exec: StdoutPipe after process started") } pr, pw, err := os.Pipe() if err != nil { return nil, err } c.Stdout = pw c.closeAfterStart = append(c.closeAfterStart, pw) c.closeAfterWait = append(c.closeAfterWait, pr) return pr, nil } </code></pre> <p>During this I noticed that <code>x/cyrpto/ssh</code> <code>*Session StdoutPipe()</code> returns an <code>io.Reader</code> and <code>ox/exec</code> returns an <code>io.ReadCloser</code>. And <code>x/crypto/ssh</code> does not track what to close. I can't find a call to <code>os.Pipe()</code> in the library so maybe the implementation is different and I'm missing something and confused by the Pipe name. </p> </div>
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页