Go編程語言之協程與信道

協程信道基礎知識簡單總結

Goroutines go協程

併發go程序活動

  • 一個程序開始時,只有唯一的協程調用main 函數
  • 新的協程通過 go 語句創建
  • 與普通函數地區別,go協程不等待
  •  f() // call f(); wait for it to return
    
  •  go f() // create a new goroutine that calls f(); don't wait
    
  • 結束方式: 從main 返回或從退出程序

Channel 信道

  • 併發go程序活動間的連接 定義:

  • 一種允許一個go協程發送值到另一個go協程的通訊機制

  • 每個channel都是一個特定類型(channel的元素類型)值的信道

  •  創建:ch := make(chan int) // ch has type 'chan int'
    
  • channel是引用傳遞,拷貝一個channel或者作爲參數傳遞給函數,caller和callee涉及的都是同樣的數據結構。nil爲零值

  • 兩個基礎方法+一個關閉方法——通訊方式

    •  send 通過channel從一個go協程傳遞值到另一個執行了對應receive操作的go協程
      
    •  receive
      
    •  close(ch),設置一個標誌表明不再有值會發送到這個channel,否則產生panic。而接受這個信道的,接收完之後會產生這個信道的空類型數據。
      
  • 操作符 <-

    •   ch <- x // 發送語句
      
    •   x = <- ch // 接收語句
      
  • 單向管道聲明

    •   out chan<- int 只寫管道
      
    •   in <-chan int 只讀管道 	    	
      
  • 無緩衝channels

    • 一個無緩衝channel上的發送操作會阻塞發送協程,直到對應的協程在該channel上執行對應的receive操作。
      接受操作先被執行,則阻塞接受協程,直到其它協程執行發送操作。
    • 一個無緩衝channel上的通訊使得發送和接受協程進行同步。所以,無緩衝channel也叫同步channel
  • Pipelines 流水線

    • 流水線:一個go協程的輸出是另一個go協程的輸入。
    • channel關閉後,剩下的值被消費完之後,再消費時會生成一個0值,就好像有一個永不停止的0值流。
    • x, ok := <-naturals ;ok爲True則正確接收,false則是一個關閉且消費完的流
    • 沒必要每個channel都關閉,無論關閉與否,無法到達的channel會被go垃圾回收
    • 在關閉的channel再次使用close會造成panic,因爲在嘗試關閉一個nil值
  • 緩衝channels
    提供一個緩衝隊列,聲明時指定channel容量即可。如ch := make(chan int, 10)

併發的循環

  • Embarrassingly parallel 易平行。問題包含的子問題完全相互獨立。
  • sync.WaitGroup 用於計數。計算爲0可結束。
    • var wg sync.WaitGroup // 聲明
    • wg.Add(1) // 進入+1
    • defer wg.Done() // 與Add數目相同, 釋放
    • wg.Wait() // 所有均結束
  • 計算信號量
    • var tokens = make(chan struct{}, 20) // token是一個計算信號量,限制了最多20個併發請求
    • tokens <- struct{}{} // 獲取一個token
    • list, err := links.Extract(url) // 執行函數
    • <-tokens // release the token // 釋放一個token
  • 爬取沒見過的鏈接的兩種方式
    • 一、外層變量n控制是否繼續。內層沒爬過的鏈接啓動協程去爬
    • 二。啓動20個協程去爬沒爬過的鏈接(unseenLinks)。發現的鏈接送入信道worklist。主程序循環worklist,沒爬過的送入unseenLinks信道

使用select實現多路技術

  • select就像switch一樣,包含若干case情況和一個可選的default
  • 每個case定義了一種通訊(一個發送或接受操作在某種信道channel上)以及一個相關的語句塊。
    • case <-ch1: //…
    • case x := <-ch2: //… use x …
  • 通訊過程
    1. select會等待直到某個case的一個通訊就緒。(多個同時就緒隨機選一個)
    2. 然後執行通訊,和相關語句;其它通訊不會發生。
    • select{} 無case的情況下會一直等待。
    • select是一個非阻塞通訊。當信道沒準備好時,嘗試在信道上接收或發送信息時,不會阻塞信道。
    • 信道的空值爲nil。發送和接受操作在一個nil信道上會永久阻塞。這種情況select語句下的一個case中nil的信道永遠不會選到。這個特性使得我們可以使用nil在打開或關閉select中的case情況。

實現取消協程的方式

類似廣播。

  • 實現方式
    • select方式輪詢一個不接收參數的信道。
    • 關閉信道後會產生nil。var done = make(chan struct{})
    • 一個函數cancelled 輪詢到done返回true,否則爲false。
    • 用於子函數用來關閉子協程 if cancelled() {return}
    • 主函數輪詢到done時,獲取拋棄所有信道數據後返回。

練習8.15 及詳細註釋

練習 8.15: 如果一個客戶端沒有及時地讀取數據可能會導致所有的客戶端被阻塞。修改 broadcaster來跳過一條消息,而不是等待這個客戶端一直到其準備好寫。或者爲每一個客戶 端的消息發出channel建立緩衝區,這樣大部分的消息便不會被丟掉;broadcaster應該用一個非阻塞的send向這個channel中發消息。

注:代碼來源 https://github.com/kdama/gopl

package main

import (
	"bufio"
	"fmt"
	"log"
	"net"
	"strings"
	"time"
)

const timeout = 5 * time.Minute // 超時時間
const outbuffer = 64 // 發送信息信道緩衝

type client struct {
	name  string
	inch  chan<- string // 接收信道
	outch chan<- string // 發送信道
var (
	entering = make(chan client)
	leaving  = make(chan client)
	messages = make(chan string) // all incoming client messages
)

func broadcaster() {
	clients := make(map[client]bool) // all connected clients
	for { // 廣播服務,常駐:發消息,進出記錄
		select {
		case msg := <-messages:
			// Broadcast incoming message to all
			// clients' outgoing message channels.
			for cli := range clients {
				cli.outch <- msg
			}

		case cli := <-entering:
			clients[cli] = true

			// 新用戶進入,告知該新用戶現在已有的所有用戶
			var onlines []string
			for c := range clients {
				onlines = append(onlines, c.name)
			}
			cli.outch <- fmt.Sprintf("%d clients: %s", len(clients), strings.Join(onlines, ", "))

		case cli := <-leaving:
			delete(clients, cli)
			close(cli.outch)
		}
	}
}

func handleConn(conn net.Conn) {
	inch := make(chan string) // 無緩衝信道
	outch := make(chan string, outbuffer) // 帶緩衝信道

	go clientReader(conn, inch)
	go clientWriter(conn, outch)

	// 當前連接的用戶是誰
	var who string

	outch <- "Input your name:" // 發送一條消息,詢問當前上線用戶名

	// 建立連接後接收用戶名
	select {
	case in, ok := <-inch:
		if !ok {
			conn.Close()
			return
		}
		who = in
	case <-time.After(timeout): // 超時則退出
		conn.Close()
		return
	}

	messages <- who + " has arrived" // 告知其他用戶誰來了
	entering <- client{who, inch, outch}

	for { // 一直處理該連接,維持上線狀態
		select {
		case in, ok := <-inch:
			if ok {
				messages <- who + ": " + in
			} else { // 信道已關,超時或服務關閉
				leaving <- client{who, inch, outch}
				messages <- who + " has left"
				conn.Close()
				return
			}
		case <-time.After(timeout): // 超時關閉連接
			leaving <- client{who, inch, outch}
			messages <- who + " has left"
			conn.Close()
			return
		}
	}
}

func clientReader(conn net.Conn, ch chan<- string) {
	input := bufio.NewScanner(conn) //讀取信息
	for input.Scan() {
		ch <- input.Text()
	}
	close(ch)
	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:8000")
	if err != nil {
		log.Fatal(err)
	}

	go broadcaster()
	for { //用來接收處理連接的服務
		conn, err := listener.Accept()
		if err != nil {
			log.Print(err)
			continue
		}
		go handleConn(conn)
	}
}

  • 主函數
    • 建立廣播
    • 監聽端口,創建連接
  • 客戶端讀、寫操作
    • 讀完需要關閉寫信道
  • 廣播服務
    • 廣播信息
    • 進入提示
    • 離開提示
  • 連接處理
    • 建立讀寫協程
    • 通過讀寫協程輸出提示後獲取用戶名
    • 通訊服務
      • 發消息(讀取界面輸入信道,nil則說明已離開,否則輸出到廣播信道)
      • 超時斷開(發送廣播離開信息)

運行截圖(注:每個用戶連接後會要求輸入用戶名,後續信息格式爲 用戶名:信息)
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章