go程序设计语言第八章-goroutine and channel

8.1 goroutines

In Go, each concurrently executing activity is called a goroutine.

If you have used operating system threads or threads in other languages, then you can assume
for now that a goroutine is similar to a thread, and you’ll be able to write correct programs.
The differences between threads and goroutines are essentially quantitative , not qualitative ,
and will be described in Section 9.8.
(threads和goroutines的区别只是数量上的差异,而不止质量上的。)

程序启动时,只有一个调用main函数的main goroutine. 新的goroutine通过go statement来创建。
A go statement causes the function to be called in a newly created goroutine.
The go statement itself completes immediately:
(go statement语句本身是立即执行结束的)

f()    // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait

(样例,一个fib递归程序,由此产生问题:为何递归程序的效率很低?)

func spinner(delay time.Duration) {for {for _, r := range `-\|/` {fmt.Printf("\r%c", r)time.Sleep(delay)}}
}
func fib(x int) int {if x < 2 {return x}return fib(x-1) + fib(x-2)
}
func main() {go spinner(100 * time.Millisecond)const n = 45fibN := fib(n)fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}

(\r参数,回到行首,覆盖掉之前的输出)

当main函数return时,所有的goroutine立即终止,程序退出。
除了main函数的return或程序的退出,没有程序上的方式来使得一个goroutine来结束另一个goroutine,
但是可以有其他方式通知一个goroutine,让它自己来结束自己。
(就是说,一个goroutine不能主动结束另一个goroutine,但goroutine可以自己结束自己,因此我们可以通过信号,
比如A给B发信号,B收到后自己结束自己,这样可以变相理解为“A结束了B”)

8.2 Example: Concurrent Clock Server

(一个简单的tcp服务器程序)

func handleConn(conn net.Conn) {defer conn.Close()for {_, err := io.WriteString(conn, time.Now().Format("15:04:05\n"))if err != nil {return}time.Sleep(1 * time.Second)}
}func main() {listener, err := net.Listen("tcp", "localhost:8000")if err != nil {log.Fatal(err)}for {conn, err := listener.Accept()if err != nil {log.Print(err)continue}handleConn(conn)}
}

一个对应的客户端程序:

func mustCopy(dst io.Writer, src io.Reader) {if _, err := io.Copy(dst, src); err != nil {log.Fatal(err)}
}func main() {conn, err := net.Dial("tcp", "localhost:8000")if err != nil {log.Fatal(err)}defer conn.Close()// 将conn的内容复制到StdoutmustCopy(os.Stdout, conn)
}

样例中server一次只能处理一个请求,在服务程序中简单的将handleConn(conn)前加go关键字,
即可处理多个请求。

8.3 Example: Concurrent Echo Server

一个Echo Server,收到什么回复什么,因此handleConn函数可能是这样:

func handleConn(c net.Conn) {io.Copy(c, c)c.Close()
}

不过这里,我们模仿一个真正的echo服务,对于请求先回应大写的格式,接着是原本格式,最后是小写格式,
中间有一定的时间间隔:

func echo(c net.Conn, shout string, delay time.Duration) {fmt.Fprintf(c, "\t", strings.ToUpper(shout))time.Sleep(delay)fmt.Fprintf(c, "\t", shout)time.Sleep(delay)fmt.Fprintf(c, "\t", strings.ToLower(shout))
}func handleConn(c net.Conn, shout string) {input := bufio.NewScanner(c)for input.Scan() {echo(c, input.Text(), 1*time.Second)}c.Close()
}

因此客户端在接收打印服务端回复的同时,也要有输入了,因此也是一个并发程序:

func main() {conn, err := net.Dial("tcp", "localhost:8000")if err != nil {log.Fatal(err)}defer conn.Close()// 将回复的内容显示到输出go mustCopy(os.Stdout, conn)// 将标准输入的内容发送给服务端mustCopy(conn, os.Stdin)
}

客户端程序中,主goroutine从stdin中读取内容并发送给server,另一个goroutine读取并打印服务的响应。
当主goroutine遇到输入结束,程序终止,即使另一个goroutine还有内容可以读取。
(也就是说,程序没有等到另外一端全部读取完,就结束退出了。可以通过channel来使得两端任务都结束时
再退出程序。)

如下是显示结果:

$ go build gopl.io/ch8/reverb1
$ ./reverb1 &
$ go build gopl.io/ch8/netcat2
$ ./netcat2
Hello?HELLO?Hello?hello?
Is there anybody there?IS THERE ANYBODY THERE?
Yooo-hooo!Is there anybody there?is there anybody there?YOOO-HOOO!Yooo-hooo!yooo-hooo!
^D
$ killall reverb1

可以看到第三个输入Yooo-hooo!发生时,必须等上一个的响应全部结束后,才能收到它自己的响应,因为为了
使得每个input都彼此独立,将服务端的echo调用处前加go变为并发处理:

func handleConn(c net.Conn) {input := bufio.NewScanner(c)for input.Scan() {go echo(c, input.Text(), 1*time.Second)}// NOTE: ignoring potential errors from input.Err()c.Close()
}

The arguments to the function started by go are evaluated when the go statement itself is
executed, thus input.Text() is evaluated in the main goroutine.
(go statement中函数的参数会立即执行,即会在main goroutine中执行,
因此在input.Text()会在main goroutine中执行)

8.4 channels

ch := make(chan int)
ch和map一样,也是引用类型,指向底层的数据结构。
当copy一个chan或将其作为函数参数,复制了一个引用,他们指向相同的数据结构。
零值为nil。
相同类型的chan可用==比较,相等则代表指向同一个数据结构。

chan有两个操作,send和receive。
还有第三个操作close,表示不会再向此chan发数据,使用内置close(ch)来关闭。

make(chan int)创建的是unbuffered chan,
可以make(chan int, 4)创建buffered chan。

8.4.1 unbuffered chan

unbuffered chan会阻塞当前执行send或receive的go routine,直到另一个go routine执行相对的操作。
因此它可以被理解为用于同步的chan。
发送值到unbuffered chan,接收此值的时间要happen before执行此发送操作的go routine的重新唤醒时间。

在讨论同步时,x happend before y不仅表示x的发生早于y,而且意味着这是有保障的(即x一定早于y),这样
对于一些依赖的操作比如更新变量值之类的会得到保证。

当x没有happend before y,或者x after y,则代表x与y并行。这不是说他们是同时发生,而是说他们的顺序不能保证。

在8.3中的客户端程序,在main goroutine中将input传给server,因此一旦input stream终止,
主程序结束,但此时background goroutine可能仍在工作。为了主程序退出前确保background已经完成,
这里使用一个chan来进行同步:

func main(){conn, err := net.Dial("tcp", "192.168.1.93:8000")if err != nil {log.Fatal(err)}done := make(chan struct{})go func() {io.Copy(os.Stdout, conn)log.Println("done")done <- struct{}{}}()mustCopy(conn, os.Stdin)conn.Close()<- done
}

当使用者关闭stdin stream,mustCopy函数返回,主程序执行conn.Close(),
将会关闭连接的两端。关闭写的一端会导致服务器看到end-of-file状态,关闭读的一端会导致
background goroutine中io.Copy返回“read from closed connection”的error。
由于主程序一直等待done中有值,因此只有background goroutine结束后给该chan发送值,才会最终结束程序。

通过chan发送msg有两个重要的方面。每个msg有一个值,但有时我们更在意发生通信这个事实和发生通信的时间。
此时我们将消息为“事件”。当“事件”没有携带额外的信息,也就是说仅仅是为了同步,可以将chan类型定义为struct{},
更常见的是bool或int类型。

8.4.2 pipeline

chan可以用来连接goroutine以便于一个的输出是另一个的输入。这称之为pipeline。
下面是通过两个chan连接三个goroutine。
第一个(counter)生成数字0,1,2。。。,将通过chan发送给第二个goroutine(squater),它收到值,
进行平方计算,发送的另一个(printer)将其打印。

func main() {// 都是unbuffered channaturals := make(chan int)squares := make(chan int)// 无限数字go func() {for x := 0; ; x++ {time.Sleep(1 * time.Second)naturals <- x}}()go func() {for {x := <- naturalssquares <- x * x}}()for {fmt.Println(<-squares)}
}

这里产生的数字是无限的,适用于无终止运行的程序。如果是有限的数字,则在发送完最后一个value到chan后,
需要close此chan。
上述中如果发送100个数字后,关闭maturals,则从naturals取数字的线程最后会一直获取到零值。

从chan中取数采用x, ok := <- chan的形式,可利用ok判断chan是否关闭。
可以使用range形式作用在chan上,直到chan被关闭range才结束。

不必每次处理完chan后执行close操作,只有在需要告诉接收者所有数据都已发送完毕时才有必要执行close操作。
垃圾回收器对于每个不可抵达的chan都会进行资源回收,不管它是否被关闭。(注意不要和打开文件的close操作
混淆,每个打开的文件在结束后都需要关闭。)
关闭chan还可以作为通知机制。

8.4.3 undirectional channel

<-chan int 只receive
chan<- int 只send

8.4.4 buffered channel

是有容量的chan,在初始化时标明容量。
因为有了容量,bufferd chan隔断了发送和接收线程。
cap()函数获取容量,len()函数获取当前元素个数。

ch := make(chan int, 3)
fmt.Println(cap(ch))

不要在一个线程中将buffer chan作为队列使用,这会有程序阻塞的风险,请使用slice。

如下例子是多个线程同时send到一个chan中,函数返回最快写入的那个:

func mirrorQuery() string {responses := make(chan string, 3)go func() {responses <- request("asia.gopl.io")}()go func() {responses <- request("europe.gopl.io")}()go func() {responses <- request("americas.gopl.io")}()return <- responses
}
func request(hostname string) (response string) { /* ... */ }

如果这里不使用buffered chan而使用unbuffered chan,则当一个线程写入后,
另外的线程将写不进去,会造成线程泄露,它将不会被垃圾回收。
因此一定要保证在线程不再被使用时能够终止它自己。

选择buffered 还是unbuffered chan,或者如何选择buffered chan的容量,
将会影响程序的准确度。
unbuffered chan能确保同步性,buffered chan则不能。
当我们知道将要发送给chan 的数据量上限,通常会选择buffered chan。

8.5 并发的循环

如果每个任务都是相互独立的,称为易并行问题。
易并行问题是最容易被实现成并行的一类问题(废话),并且最能够享受到并发带来的好处,能够随着并行的规模线性地扩展。

func ImageFile(filename string) (string, error) {return "", nil
}

版本一

func makeThumbnails(filenames []string) {for _, f := range filenames {if _, err := ImageFile(f); err != nil {log.Println(err)}}
}

版本二,增加go关键字,但主程序未等后台运行结束就终止了

func makeThumbnails2(filenames []string) {// 还没执行就结束了for _, f := range filenames {go ImageFile(f)}
}

版本三,主程序和后台程序用chan进行同步

func makeThumbnails3(filenames []string) {// 这里用buffer还是unbuffer?ch := make(chan struct{})for _, f := range filenames {go func(f string) {ImageFile(f)ch <- struct{}{}}(f)}for range filenames {<- ch}
}

版本四,将后台程序的返回值传给主线程

func makeThumbnails4(filenames []string) error {// 这里用buffer还是unbufferch := make(chan error)for _, f := range filenames {go func(f string) {_, err := ImageFile(f)ch <- err}(f)}for range filenames {// 由于是unbuffer,直接返回会导致线程泄露if err := <- ch; err != nil {return err}}return nil
}

由于是unbuffer chan,这里有个小bug,如果主程序直接return,会导致
后台线程一直阻塞。改进,可以采用buffered chan或者返回时排空channel。

版本五,采用buffered chan

func makeThumbnails5(filenames []string) error {// 这里用bufferch := make(chan error, len(filenames))for _, f := range filenames {go func(f string) {_, err := ImageFile(f)ch <- err}(f)}for range filenames {// 由于是buffer,直接返回并不会导致线程泄露if err := <- ch; err != nil {return err}}return nil
}

版本六, 采用waitGroup进行同步

func makeThumbnails6(filenames <-chan string) int64 {sizes := make(chan int64)var wg sync.WaitGroupfor f := range filenames {wg.Add(1)go func(f string) {defer wg.Done()thumb, err := ImageFile(f)if err != nil {log.Println(err)return}sizes <- int64(len(thumb))}(f)}wg.Wait()close(sizes)var total int64for size := range sizes {total += size}return total
}

8.6 并发的web爬虫

前面的例子中有个爬虫的例子,其中Extract函数返回一个[]string类型:

package mainimport ("fmt""log""os"
)
func Extract(url string) ([]string, error) {s := []string{}return s, nil
}func crawl(url string) []string {fmt.Println(url)list, err := Extract(url)if err != nil {log.Print(err)}return list
}func main() {worklist := make(chan []string)// 把参数列表放进去go func() { worklist <- os.Args[1:] }()seen := make(map[string]bool)for list := range(worklist) {for _, link := range list {if !seen[link] {seen[link] = true// 新开一个线程去执行go func(link string) {// 在此线程内,将结果写入chworklist <- crawl(link)}(link)}}}
}

这里主线程使用range方法从ch中取数据[]string,取到后遍历,
每遍历一个link就开一个线程,且将抓取的结果再放入ch中。
(这里刚开始给ch放入数据时必须新开线程,因为这是一个unbuffered chan,
如果写在主线程中就会阻塞,一直等待造成死锁)

  1. 由于Extract函数是发起tcp连接,系统会有每一个进程的打开文件数限制,
    这里最简单的方法是限制住Extract在同一时间最多不会有超过n次调用,
    n一般小于文件描述符的上限值,比如20。
var tokens = make(chan struct{}, 20)func crawl2(url string) []string {fmt.Println(url)// 每次调用函数则插入一个token// 最多不能超过容量大小20tokens <- struct{}{}list, err := Extract(url)// 调用结束后再取出// 取出和发送成对出现<- tokensif err != nil {log.Print(err)}return list
}
  1. 另外一个问题是这个程序不会终止,
    因为主程序使用range遍历channel,如果channel不关闭则循环不会结束。
    改进型,不用range遍历,用一个变量n表示当前ch中数据的数量,为0时
    表示队列中没有数据需要处理则结束程序:
func main() {worklist := make(chan []string)// 表示ch中的数据量// 每放入一个+1,每取出一个-1var n int// 把参数放进去n++go func() { worklist <- os.Args[1:] }()seen := make(map[string]bool)// 循环取出数据for ; n > 0; n-- {list := <- worklistfor _, link := range list {if !seen[link] {seen[link] = true// 新开一个线程去执行n++go func(link string) {// 在此线程内,将结果写入chworklist <- crawl(link)}(link)}}}
}

注意,n的+1和-1操作都是在主线程中进行的,不然可能会有多线程修改同一变量的问题。

  1. 还有一种避免过度并发的方法,直接开20个常驻的线程处理程序,
    他们统一从一个chan中接收link任务,处理完后放回到worklist中
    (不过同样有函数不能正常终止的问题)
func main() {worklist := make(chan []string)unseenLinks := make(chan string)// 把参数放进去go func() { worklist <- os.Args[1:] }()// 直接创建20个线程for i := 0; i < 20; i++ {go func() {// 从unseenLinks去抢linkfor link := range unseenLinks {foundLinks := crawl(link)// 处理完之后再放入worklistgo func() { worklist <- foundLinks }()}}()}// 主线程从worklist中去取,取出不重复的link放入unseenLinks中seen := make(map[string]bool)for list := range worklist {for _, link := range list {if !seen[link] {seen[link] = trueunseenLinks <- link}}}
}

8.7 基于select的多路复用

首先介绍time.Tick(1 * time.Second)函数,它会创建一个chan,
并且在另外的线程中周期性的往这个chan中发送数据。
一般程序的整个声明周期都需要从它接收时才使用此Ticker函数,
因为一旦不再需要接收,但没办法停止后台线程继续徒劳地仍向此chan发送
数据,会导致goroutine泄露。
因此,我们一般可以采用更加便于控制的:

ticker := time.NewTicker(1 * time.Second)
<- ticker.C
ticker.Stop()

time.After函数会立即返回一个channel,并起一个新的goroutine在经过特定的时间后向该channel发送一个独立的值。

8.8 并发的目录遍历

使用ioutil.ReadDir()访问目录,返回其中的文件列表

// 用来控制同时进行目录操作的个数
var sema = make(chan struct{}, 20)func walkDir(dir string, filesizes chan<- int64, wg *sync.WaitGroup) {// 注意在函数内调用wg.Done()defer wg.Done()for _, entry := range dirents(dir) {if entry.IsDir() {wg.Add(1)subdir := filepath.Join(dir, entry.Name())go walkDir(subdir, filesizes, wg)} else {filesizes <- entry.Size()}}
}func dirents(dir string) []os.FileInfo {sema <- struct{}{}defer func() { <-sema }()entries, err := ioutil.ReadDir(dir)if err != nil {fmt.Fprintf(os.Stderr, "du1: %v\n", err)return nil}return entries
}func main() {dirs := os.Args[1:]fmt.Println(dirs)filesizes := make(chan int64)var wg sync.WaitGrouptick := time.Tick(500 * time.Millisecond)var nfiles, nbytes int64for _, dir := range dirs {wg.Add(1)go walkDir(dir, filesizes, &wg)}// 等待和关闭可以放在主函数中吗?// 这里不行,因为是unbuffered chan,没有线程去消耗会导致死锁go func() {wg.Wait()close(filesizes)}()Loop:for {select {case filesize, ok := <- filesizes:if !ok {fmt.Println("chan closed")break Loop}nfiles++nbytes += filesizecase <- tick:fmt.Printf("%d files  %.1f GB\n", nfiles, float64(nbytes)/1e9)}}fmt.Printf("Final %d files  %v bytes\n", nfiles, nbytes)}

注意嵌套在for循环中的select,使用break并不会跳出for循环。

8.9 并发的退出

goroutine的退出,不能是一个线程终止另一个线程,只能是自己监听某个chan,如果有信号产生则自己退出。
之前我们是向chan中发送某个值,检测到有值时认为需要退出了;如果有多个goroutine需要退出呢,由于
不好确定发送值的数量,因此可以采取监听chan是否关闭的方法:如果关闭了,则认为是需要结束自己了。

对上一节du程序的修改:

package mainimport ("fmt""io/ioutil""log""os""path/filepath""sync""time"
)var tokens = make(chan struct{}, 20)
func Extract(url string) ([]string, error) {s := []string{}return s, nil
}func crawl(url string) []string {fmt.Println(url)list, err := Extract(url)if err != nil {log.Print(err)}return list
}func crawl2(url string) []string {fmt.Println(url)// 每次调用函数则插入一个token// 最多不能超过容量大小20tokens <- struct{}{}list, err := Extract(url)// 调用结束后再取出// 取出和发送成对出现<- tokensif err != nil {log.Print(err)}return list
}// 用来控制同时进行目录操作的个数
var sema = make(chan struct{}, 20)func walkDir(dir string, filesizes chan<- int64, wg *sync.WaitGroup, done chan struct{}) {// 注意在函数内调用wg.Done()defer wg.Done()// 侵入式改造,随时监听done是否关闭// 如果关闭,则walkDir函数返回select {case <-done:returndefault:}// 这里人为加上一个耗时操作,测试done的取消操作time.Sleep(1 * time.Second)for _, entry := range dirents(dir, done) {if entry.IsDir() {wg.Add(1)subdir := filepath.Join(dir, entry.Name())go walkDir(subdir, filesizes, wg, done)} else {filesizes <- entry.Size()}}
}func dirents(dir string, done chan struct{}) []os.FileInfo {// 这里也监听done是否关闭,可以有效地避免因为获取sema的耗时操作// 如果关闭,直接返回,省去了获取semaselect {case <-done:return nilcase sema <- struct{}{}:}// 如果获取到了token,就释放defer func() { <-sema }()entries, err := ioutil.ReadDir(dir)if err != nil {fmt.Fprintf(os.Stderr, "du1: %v\n", err)return nil}return entries
}func main() {dirs := os.Args[1:]fmt.Println(dirs)filesizes := make(chan int64)// 主线程和多个任务线程同步var wg sync.WaitGroup// 每500ms打印一次信息tick := time.Tick(500 * time.Millisecond)var nfiles, nbytes int64// 当从标准输入读取到一个数据时,通过关闭done这个ch来终止所有的程序// 线程1,读取输入done := make(chan struct{})go func() {os.Stdin.Read(make([]byte, 1))fmt.Println("os.Stdin.Read 1 byte")close(done)}()for _, dir := range dirs {wg.Add(1)// 在walkDir函数中就要控制是否要终止go walkDir(dir, filesizes, &wg, done)}// wg.Wait()和close(filesizes)可以放在主函数中吗?// 这里不行,因为filesizes是unbuffered chan,后台线程都只是往里塞数据// 必须主线程里有个取数据的操作,否则会导致死锁go func() {wg.Wait()close(filesizes)}()Loop:for {select {// 主线程也要监听done,为了确保能快速退出// 主线程监听到done关闭时,可能filesizes还未关闭,即wg.Wait()还未执行结束,// 这时可以等待filesizes关闭,即后台所有线程结束后执行到wg.Wait()case <-done:// 这里使用range循环,filesizes关闭后才会range结束for range filesizes {// 什么也不做,也可以取出数据计算nfiles和nbytes,代表到停止那一刻计算出的数量// 同时这是一个排空操作,可以有效避免后台线程因为向chan发送数据而阻塞}//break Loop// 这里直接returnreturncase filesize, ok := <- filesizes:if !ok {fmt.Println("chan closed")break Loop}nfiles++nbytes += filesizecase <- tick:fmt.Printf("%d files  %.1f GB\n", nfiles, float64(nbytes)/1e9)}}fmt.Printf("Final %d files  %v bytes\n", nfiles, nbytes)}

几个注意点:

  1. 监听取消chan的程序有三个:
    (1) 后台walkDir函数,当done关闭时,直接返回
    (2) 后台dirents函数,当done关闭时,直接返回
    (1) 主main中的循环,当done关闭时,排空filesizes,退出程序
  2. 这里监听done的后台程序,无论done是否关闭,仍然是执行了wg的计数操作,
    因此,wg.Wait()肯定能执行到,close(filesizes)也能执行到,即仍然是等待所有的后台
    goroutine都执行完
  3. 这里由于监听done,对程序进行了浸入式改造,会比较麻烦
  4. 现在当取消发生时,所有后台的goroutine都会迅速停止并且主函数会返回。当然,当主函数返回时,一个程序会退出,而我们又无法在主函数退出的时候确认其已经释放了所有的资源(译注:因为程序都退出了,你的代码都没法执行了)。这里有一个方便的窍门我们可以一用:取代掉直接从主函数返回,我们调用一个panic,然后runtime会把每一个goroutine的栈dump下来。如果main goroutine是唯一一个剩下的goroutine的话,他会清理掉自己的一切资源。但是如果还有其它的goroutine没有退出,他们可能没办法被正确地取消掉,也有可能被取消但是取消操作会很花时间;所以这里的一个调研还是很有必要的。我们用panic来获取到足够的信息来验证我们上面的判断,看看最终到底是什么样的情况。
    这里,如果用panic(“error return”)代替return,会看到如下输出:
[root@localhost zjh]# ./main /home
[/home]
0 files  0.0 GB
0 files  0.0 GB
15 files  0.0 GB
a
os.Stdin.Read 1 byte
panic: error returngoroutine 1 [running]:
main.main()/tmp/zjh/main.go:149 +0x629

这是否意味着现在只有主函数这个goroutine了?应该是的。

8.10 聊天服务

package mainimport ("bufio""fmt""log""net"
)// 这里client不是一个conn,是每到一个连接就新生成的chan
type client chan<- stringvar (entering = make(chan client)leaving  = make(chan client)messages = make(chan string)
)
func broadcaster() {// 可以将chan作为map的keyclients := make(map[client]bool)for {select {// 全局messages一旦有内容,就把内容发送给所有的clientscase msg := <- messages:for cli := range clients {cli <- msg}// entering一旦有消息,则注册到clients这个map中case cli := <-entering:clients[cli] = true// leaving一旦有消息,则从clients这个map中删除case cli := <-leaving:delete(clients, cli)close(cli)}}
}func handleConn(conn net.Conn) {ch := make(chan string)// 把ch里的内容写入conn// 一旦ch里有内容,就会被发送给客户端go clientWriter(conn, ch)who := conn.RemoteAddr().String()// 发送给客户端ch <- "You are " + who// 发送给全局变量messages <- who + " has arrived"// 该ch注册到enteringentering <- chinput := bufio.NewScanner(conn)for input.Scan() {messages <- who + ": " + input.Text()}leaving <- chmessages <- who + " has left"conn.Close()}func clientWriter(conn net.Conn, ch <-chan string) {for msg := range ch {fmt.Fprintln(conn, msg) // NOTE: ignoring network errors}
}func main() {listener, err := net.Listen("tcp", "localhost:8100")if err != nil {log.Fatal("err")}// 这个线程干嘛的?go broadcaster()for {conn, err := listener.Accept()if err != nil {log.Print(err)continue}go handleConn(conn)}}

go程序设计语言第八章-goroutine和channel相关推荐

  1. goroutine和channel机制与C#类库功能类比

    版权声明: 本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处.如您有任何疑问或者授权方面的协商,请给我留言. 为 ...

  2. Go程序设计语言翻译问题(goroutine)

    中文:Go程序设计语言 2017.1 英文:The Go Programming Language 2016 8.4.2. Pipelines 8.4.2管道章节 修正: 第一个管道应该改成通道,ca ...

  3. TODO:Go语言goroutine和channel使用

    2019独角兽企业重金招聘Python工程师标准>>> TODO:Go语言goroutine和channel使用 goroutine是Go语言中的轻量级线程实现,由Go语言运行时(r ...

  4. 唤醒手腕 Go 语言 并发编程 (goroutine、channel)详细教程(更新中)

    线程.协程基本概念 协程是单线程下的并发,又称微线程,纤程.它是实现多任务的另一种方式,只不过是比线程更小的执行单元.因为它自带CPU的上下文,这样只要在合适的时机,我们可以把一个协程切换到另一个协程 ...

  5. 程序设计语言原理复习总结 北航计算机专业课

    这门课是上学期收获最大的一门,也是花费时间最多的.有一个大作业,是设计并开发一门新的语言.期末还有考试. 如果大作业被评为优秀,就不用参加期末的考试了.期末考试难度不低,上八十的很少.复习的话要根据重 ...

  6. Go语言---并发编程goroutine

    在Go语言中并发是通过goroutine实现.goroutine类似于线程,属于用户态线程.Go语言也可以通过channel(管道)与多个goroutine进行通信. goroutine gorout ...

  7. 理解Go的Goroutine和channel

    原址 进程,线程的概念在操作系统的书上已经有详细的介绍.进程是内存资源管理和cpu调度的执行单元.为了有效利用多核处理器的优势,将进程进一步细分,允许一个进程里存在多个线程,这多个线程还是共享同一片内 ...

  8. C语言牛牛手里有一个字符串A,程序设计语言C实验卡学生.doc

    程序设计语言C实验卡学生.doc 计算机课程实验卡 课程名称 程序设计语言(C) 班级 顺序号 1(3月4日) 实验名称 实验一 熟悉C语言上机环境 实验目的 1.熟悉C语言的编辑.编译及运行程序的环 ...

  9. 8、程序设计语言与语言处理程序基础

    目录 第八章 程序设计语言与语言处理程序基础 一.汇编.编译.解释系统基础 1. 解释与编译 2. 编译过程 3.语言及文法的概念 4. 词法分析 (1)有限自动机 确定的有限自动机(DFA) 不确定 ...

最新文章

  1. 基于ARM Cortex-M的SoC存储体系结构和实战
  2. android炫酷的自定义view,Android自定义View实现炫酷进度条
  3. OpenCart之在线客服(Google Talk)模块教程
  4. C++实现桶排序(附完整源码)
  5. Statement对象
  6. OSI七层网络模型浅析
  7. [react] 为何说虚拟DOM会提高性能?
  8. kubernetes pv-controller 解析
  9. IPerf——网络测试工具介绍与源码解析(3)
  10. GoCart 分类和产品 测试二
  11. Python3 爬虫之 Scrapy 核心功能实现(二)
  12. react怎么引入jquery_在react里面使用jquery插件
  13. 微信小程序教程、微信小程序开发资源下载汇总(6.16日更新,持续更新中……)...
  14. 量化投资的现状和前景
  15. 三维激光雷达点云处理分类及目标检测综述
  16. 软件研发成本构成中的间接成本包括哪些?
  17. 如何用计算机设计衣服,如何用电脑设计服装
  18. 来了,掏心窝的最重要3条建议
  19. 基于WFP的windows驱动对TCP数据的抓取,修改以及注意事项
  20. 鲁大师2022年度硬件榜单即将出炉,多维度看谁能夺奖?

热门文章

  1. Web中的EasyExcel导出Excel(不创建对象且自定义合并单元格策略)
  2. VideoJS+HLS视频加密播放
  3. Advenced Installer制作C#程序安装包过程.Net和Visual C++采用静默安装配置说明
  4. C++抽奖(随机数+人名的不停闪烁)
  5. 论文解读:ToxinPred2:一种预测蛋白质毒性的改进方法
  6. C语言----结构体及其应用
  7. 基于Visual C#2010开发Windows7应用 多点触摸图片处理应用程序(1)-同时处理多张图片...
  8. 银行排队叫号系统的模拟
  9. 2月19日foremost隐写wp
  10. ai作文批改_AI能批改英语作文了 专业度堪比高考阅卷老师 可自动批改雅思、四六级英语作文...