Эта серия предназначена для чтения «Язык программирования Go», чтобы понять и задокументировать.
Go поддерживает две модели параллелизма: взаимодействующие последовательные процессы (CSP) и многопоточность с общей памятью.Первая является основой для реализации моделей параллелизма горутин и каналов, а вторая представляет собой традиционный способ совместного использования памяти, который представляет собой многопоточную модель.
Как понять КСП? Проще говоря, это поддержание состояния различных горутин в режиме параллелизма путем передачи значений между разными горутинами, но использование и изменение переменных должно быть ограничено одной горутиной.
определение
Единица действия, которая может выполняться одновременно в Go, называется горутиной. Когда программа Go запускается, создается горутина, которая выполняет основную функцию, называемуюmain goroutine
. Чтобы создать новую горутину, вы можете использовать оператор go, например: go f(), гдеf
является функцией. После использования оператора go для запуска новой горутины вызовы функций после оператора go будут выполняться в новой горутине, не блокируя выполнение текущей программы.
package main
import (
"fmt"
"time"
)
func main() {
go spinner(100 * time.Millisecond)
const (
n = 45
)
fibN := fib(n)
fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN)
}
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)
}
В этом примереgo spinner()
а такжеfib
Выполнение двух функций независимо друг от друга, то есть они могут выполняться одновременно.
Пример: Параллельный сервер часов
Чтобы лучше продемонстрировать использование горутин в параллельных сценариях и его преимущества, давайте завершим сервер часов.Этот сервер очень прост.Каждый раз, когда обрабатывается запрос от клиента, текущее время форматируется и отправляется обратно. на стороне клиента мы сначала реализуем версию, которая не поддерживает горутины, то есть обрабатывает одно соединение за раз.
package main
import (
"io"
"log"
"net"
"time"
)
func main() {
listener, err := net.Listen("tcp", "localhost:8888")
if err != nil {
log.Fatal(err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err)
continue
}
handleConn(conn)
}
}
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)
}
}
Затем мы реализуем клиент для подключения к серверу, клиент отвечает только за подключение к серверу и эхо сообщения сервера.
package main
import (
"io"
"log"
"net"
"os"
)
func main() {
conn, err := net.Dial("tcp", "localhost:8888")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
mustCopy(os.Stdout, conn)
}
func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
log.Fatal(err)
}
}
Затем мы начинаем наш параллельный эксперимент, сначала выполняем программу на стороне сервера, затем открываем терминал для запуска клиента, вы можете видеть, что клиент будет непрерывно выводить текущее время.
go run ch08_03_netcat1.go
13:58:50
13:58:51
13:58:52
13:58:53
13:58:54
13:58:55
13:58:56
13:58:57
13:58:58
Затем открываем новый терминал для выполнения нового клиента и обнаруживаем, что вывода нет, но после закрытия первого клиента время на вывод будет. В этом примере, поскольку сервер может одновременно обрабатывать только одно клиентское соединение, при одновременном подключении нескольких клиентов последующие клиенты должны быть поставлены в очередь.
Использование goroutine может улучшить возможности параллельной обработки сервера для решения этой проблемы.Это очень просто.Вам нужно только добавить ключевое слово go в место, где соединение обрабатывается на стороне сервера.go handleConn(conn)
, после включения новой горутины несколько клиентов будут иметь время для вывода одновременно, а сервер сможет выполнять параллельную обработку.
Пример: эхо-сервер
Эхо-сервер — это пример, который демонстрирует эхо, в этом примере мы отправим сообщение на сервер, а затем сервер ответит эхом в виде эха, например, отправивHello
, сервер будет повторятьHELLO
,Hello
а такжеhello
.
package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
"time"
)
func main() {
listener, err := net.Listen("tcp", "localhost:8888")
if err != nil {
log.Fatal(err)
}
for {
conn, err := listener.Accept()
if err != nil {
log.Print(err)
continue
}
go handleConn(conn)
}
}
func handleConn(conn net.Conn) {
// 连接不断读取数据并转化
input := bufio.NewScanner(conn)
defer conn.Close()
for input.Scan() {
echo(conn, input.Text(), 1*time.Second)
}
}
func echo(c net.Conn, shout string, delay time.Duration) {
fmt.Fprintln(c, "\t", strings.ToUpper(shout))
time.Sleep(delay)
fmt.Fprintln(c, "\t", shout)
time.Sleep(delay)
fmt.Fprintln(c, "\t", strings.ToLower(shout))
}
В приведенном выше коде сервер начинает читать данные клиента и эхо после получения соединения клиента, процесс эхо выполняется с интервальной задержкой.
package main
import (
"io"
"log"
"net"
"os"
)
func main() {
conn, err := net.Dial("tcp", "localhost:8888")
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// 从 conn 中读取数据并且送到标准输出
go mustCopy(os.Stdout, conn)
//从标准输入中读取数据并且送到 conn
mustCopy(conn, os.Stdin)
}
func mustCopy(dst io.Writer, src io.Reader) {
if _, err := io.Copy(dst, src); err != nil {
if err == io.EOF { //check eof ctrl + d
os.Exit(1)
}
}
}
Код клиента прост: он считывает данные со стандартного ввода на сервер и считывает данные с сервера на стандартный вывод.
Запустите сервер и запустите клиент, чтобы начать наш эксперимент.
± % make netcat2
go run ch08_03_netcat2.go
Hello
HELLO
Hello
hello
Me
ME
He Me
llo me
HELLO
Hello
hello
В реальном мире эхо, если есть несколько эхо-сигналов одновременно, должно быть явление шахматного появления, но когда у нашего клиента есть два эха, они не чередуются.Для эха нам также нужна горутина для реализации чередования отображение эха, вот такgo echo(conn, input.Text(), 1*time.Second)
.
Параметры горутины определяются после выполнения оператора go, поэтому значение input.Text() определяется после открытия оператора go., то есть, если input.Text() возвращает сообщение a при выполнении оператора go, даже если то же соединение имеет сообщение b позже, параметр функции goroutine по-прежнему равен a, поэтому, даже если одно и то же клиентское соединение имеет несколько сообщений msg , она последует за Нашей просьбой эхом.
Для приведенной выше программы Go требуется всего два простых ключевых слова go, чтобы реализовать сервер, который обрабатывает несколько соединений одновременно и даже обеспечивает параллелизм в одном и том же соединении.