Go语言教程之边写边学:了解go中并发工作原理:了解有缓冲channel
正如你所了解的,默认情况下channel是无缓冲行为。 这意味着只有存在接收操作时,它们才接受发送操作。 否则,程序将永久被阻止等待。
有时需要在goroutine之间进行此类同步。 但是,有时你可能只需要实现并发,而不需要限制goroutine之间的通信方式。
有缓冲channel在不阻止程序的情况下发送和接收数据,因为有缓冲channel的行为类似于队列。 创建channel时,可以限制此队列的大小,如下所示:
ch := make(chan string, 10)
每次向channel发送数据时,都会将元素添加到队列中。 然后,接收操作将从队列中删除该元素。 当channel已满时,任何发送操作都将等待,直到有空间保存数据。 相反,如果channel是空的且存在读取操作,程序则会被阻止,直到有数据要读取。
下面是一个理解有缓冲channel的简单示例:
package main

import (
    "fmt"
)

func send(ch chan string, message string) {
    ch <- message
}

func main() {
    size := 4
    ch := make(chan string, size)
    send(ch, "one")
    send(ch, "two")
    send(ch, "three")
    send(ch, "four")
    fmt.Println("All data sent to the channel ...")

    for i := 0; i < size; i++ {
        fmt.Println(<-ch)
    }

    fmt.Println("Done!")
}

运行程序时,将看到以下输出:

All data sent to the channel ...
one
two
three
four
Done!

你可能会说我们在这里没有做任何不同的操作,你是对的。 但是让我们看看当你将size变量更改为一个更小的数字(你甚至可以尝试使用一个更大的数字)时会发生什么情况,如下所示:

size := 2

重新运行程序时,将看到以下错误:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.send(...)
        /Users/developer/go/src/concurrency/main.go:8
main.main()
        /Users/developer/go/src/concurrency/main.go:16 +0xf3
exit status 2
出现此错误是因为对send函数的调用是连续的。 你不是在创建新的goroutine。 因此,没有任何要排队的操作。
channel与goroutine有着紧密的联系。 如果没有另一个goroutine从channel接收数据,则整个程序可能会永久处于被阻止状态。 正如你所见,这种情况确实会发生。
现在让我们进行一些有趣的实践! 我们将为最后两次调用创建goroutine (前两次调用正确适应缓冲区),并运行for循环四次。 代码如下:
func main() {
    size := 2
    ch := make(chan string, size)
    send(ch, "one")
    send(ch, "two")
    go send(ch, "three")
    go send(ch, "four")
    fmt.Println("All data sent to the channel ...")

    for i := 0; i < 4; i++ {
        fmt.Println(<-ch)
    }

    fmt.Println("Done!")
}
运行程序时,它按预期工作。 我们建议在使用channel时始终使用goroutine。
让我们测试一下创建的缓冲通道的元素超出所需量的情况。 我们将使用之前用于检查API的示例,并创建大小为10的缓冲通道:
package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    start := time.Now()

    apis := []string{
        "https://management.azure.com",
        "https://dev.azure.com",
        "https://api.github.com",
        "https://outlook.office.com/",
        "https://api.somewhereintheinternet.com/",
        "https://graph.microsoft.com",
    }

    ch := make(chan string, 10)

    for _, api := range apis {
        go checkAPI(api, ch)
    }

    for i := 0; i < len(apis); i++ {
        fmt.Print(<-ch)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}
运行程序时,将看到与以前相同的输出。 可以更改channel的大小,用更小或更大的数字进行测试,程序仍能正常运行。

无缓冲channel与有缓冲channel

现在,你可能想知道何时使用这两种类型。 这完全取决于你希望goroutine之间的通信如何进行。 无缓冲channel同步通信。 它们保证每次发送数据时,程序都会被阻止,直到有人从channel中读取数据。
相反,有缓冲channel将发送和接收操作解耦。 它们不会阻止程序,但你必须小心使用,因为可能最终会导致死锁(如前文所述)。 使用无缓冲channel时,可以控制可并发运行的goroutine的数量。 例如,你可能要对API进行调用,并且想要控制每秒执行的调用次数。 否则,你可能会被阻止。

Channel方向

Go中的通道具有另一个有趣的功能。 在使用通道作为函数的参数时,可以指定通道是要"发送"数据还是"接收"数据。 随着程序的增长,可能会使用大量的函数,这时候,最好记录每个channel的意图,以便正确使用它们。 或者,你要编写一个库,并希望将channel公开为只读,以保持数据一致性。
要定义channel的方向,可以使用与读取或接收数据时类似的方式进行定义。 但是你在函数参数中声明channel时执行此操作。 将通道类型定义为函数中的参数的语法如下所示:
chan<- int // 仅用于写入数据的chan
<-chan int // 仅用于读取数据的chan
通过仅接收的channel发送数据时,在编译程序时会出现错误。
让我们使用以下程序作为两个函数的示例,一个函数用于读取数据,另一个函数用于发送数据:
package main

import "fmt"

func send(ch chan<- string, message string) {
    fmt.Printf("Sending: %#v\n", message)
    ch <- message
}

func read(ch <-chan string) {
    fmt.Printf("Receiving: %#v\n", <-ch)
}

func main() {
    ch := make(chan string, 1)
    send(ch, "Hello World!")
    read(ch)
}

运行程序时,将看到以下输出:

Sending: "Hello World!"
Receiving: "Hello World!"

程序阐明每个函数中每个channel的意图。 如果试图使用一个channel在一个仅用于接收数据的channel中发送数据,将会出现编译错误。 例如,尝试执行如下所示的操作:

func read(ch <-chan string) {
    fmt.Printf("Receiving: %#v\n", <-ch)
    ch <- "Bye!"
}

运行程序时,将看到以下错误:

# command-line-arguments
./main.go:12:5: invalid operation: ch <- "Bye!" (send to receive-only type <-chan string)

编译错误总比误用channel好。

多路复用

最后,让我们讨论如何使用select关键字与多个通道同时交互。 有时,在使用多个channel时,需要等待事件发生。 例如,当程序正在处理的数据中出现异常时,可以包含一些逻辑来取消操作。
select语句的工作方式类似于switch语句,但它适用于channel。 它会阻止程序的执行,直到它收到要处理的事件。 如果它收到多个事件,则会随机选择一个。
select语句的一个重要方面是,它在处理事件后完成执行。 如果要等待更多事件发生,则可能需要使用循环。
让我们使用以下程序来看看select的运行情况:
package main

import (
    "fmt"
    "time"
)

func process(ch chan string) {
    time.Sleep(3 * time.Second)
    ch <- "Done processing!"
}

func replicate(ch chan string) {
    time.Sleep(1 * time.Second)
    ch <- "Done replicating!"
}

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    go process(ch1)
    go replicate(ch2)

    for i := 0; i < 2; i++ {
        select {
        case process := <-ch1:
            fmt.Println(process)
        case replicate := <-ch2:
            fmt.Println(replicate)
        }
    }
}

运行程序时,将看到以下输出:

Done replicating!
Done processing!

请注意,replicate函数首先完成,这就是首先在终端中看到其输出的原因。 main函数存在一个循环,因为select语句在收到事件后立即结束,但我们仍在等待process函数完成。

Go语言教程之边写边学:了解go中并发工作原理:将channel用作通信机制
Go中的channel是goroutine之间的通信机制。 请记住Go的并发方法是:"不是通过共享内存通信;而是通过通信共享内存。"当你需要将值从一个goroutine发送到另一个时,可以使用通道。 让我们看看它们的工作原理,以及如何开始使用它们来编写并发Go程序。

Channel语法

由于channel是发送和接收数据的通信机制,因此它也有类型之分。 这意味着你只能发送channel支持的数据类型。 除使用关键字chan作为channel的数据类型外,还需指定将通过channel传递的数据类型,如int类型。
每次声明一个channel或希望在函数中指定一个channel作为参数时,都需要使用chan <type>,如chan int。 若要创建通道,需使用内置的make() 函数:
ch := make(chan int)
一个channel可以执行两项操作:发送数据和接收数据。 若要指定channel具有的操作类型,需要使用channel运算符 <-。 此外,在channel中发送数据和接收数据属于阻止操作。 你一会儿就会明白为何如此。
如果希望通道仅发送数据,请在通道之后使用 <- 运算符。 如果希望通道接收数据,请在通道之前使用 <- 运算符,如下所示:
ch <- x // 向ch写入数据x = <-ch // 从ch读取数据
<-ch // 从ch读取数据但不做任何处理

可在channel中执行的另一项操作是关闭channel。 若要关闭通道,使用内置的close() 函数:

close(ch)
当你关闭通道时,你希望数据将不再在该通道中发送。 如果试图将数据发送到已关闭的channel,则程序将发生严重错误。 如果试图通过已关闭的channel接收数据,则可以读取发送的所有数据。 随后的每次"读取"都将返回一个零值。
让我们回到之前创建的程序,然后使用通道来删除睡眠功能。 首先,让我们在main函数中创建一个字符串channel,如下所示:
ch := make(chan string)
接下来,删除睡眠行time.Sleep(3 * time.Second)。
现在,我们可以使用channel在goroutine之间进行通信。 应重构代码并通过通道发送该消息,而不是在checkAPI函数中打印结果。 要使用该函数中的channel,需要添加channel作为参数。 checkAPI函数应如下所示:
func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}
请注意,我们必须使用fmt.Sprintf函数,因为我们不想打印任何文本,只需利用通道发送格式化文本。 另请注意,我们在channel变量之后使用 <- 运算符来发送数据。
现在,你需要更改main函数以发送channel变量并接收要打印的数据,如下所示:
ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)
请注意,我们在channel之前使用 <- 运算符来表明我们想要从channel读取数据。
重新运行程序时,会看到如下所示的输出:
ERROR: https://api.somewhereintheinternet.com/ is down!

Done! It took 0.007401217 seconds!
至少它不用调用睡眠函数就可以工作,对吧? 但它仍然没有达到我们的目的。 我们只看到其中一个goroutine的输出,而我们共创建了五个goroutine。 在下一节中,我们来看看这个程序为什么是这样工作的。

无缓冲channel

使用make() 函数创建channel时,会创建一个无缓冲channel,这是默认行为。 无缓冲channel会阻止发送操作,直到有人准备好接收数据。 正如我们之前所说,发送和接收都属于阻止操作。 此阻止操作也是上一节中的程序在收到第一条消息后立即停止的原因。
我们可以说fmt.Print(<-ch) 会阻止程序,因为它从channel读取,并等待一些数据到达。 一旦有任何数据到达,它就会继续下一行,然后程序完成。
其他goroutine发生了什么? 它们仍在运行,但都没有在侦听。 而且,由于程序提前完成,一些goroutine无法发送数据。 为了证明这一点,让我们添加另一个fmt.Print(<-ch),如下所示:
ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)
fmt.Print(<-ch)

重新运行程序时,会看到如下所示的输出:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
Done! It took 0.263611711 seconds!

请注意,现在你会看到两个API的输出。 如果继续添加更多fmt.Print(<-ch) 行,你最终将会读取发送到channel的所有数据。 但是如果你试图读取更多数据,而没有goroutine再发送数据,会发生什么呢? 例如:

ch := make(chan string)

for _, api := range apis {
    go checkAPI(api, ch)
}

fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)

fmt.Print(<-ch)

重新运行程序时,会看到如下所示的输出:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://dev.azure.com is up and running!
它在运行,但程序未完成。 最后一个打印行阻止了程序,因为它需要接收数据。 必须使用类似Ctrl+C的命令关闭程序。
上个示例只是证明了读取数据和接收数据都属于阻止操作。 要解决此问题,可以将代码更改为for循环,并只接收确定要发送的数据,如下所示:
for i := 0; i < len(apis); i++ {
    fmt.Print(<-ch)
}

以下是程序的最终版本,以防你的版本出错:

package main

import (
    "fmt"
    "net/http"
    "time"
)

func main() {
    start := time.Now()

    apis := []string{
        "https://management.azure.com",
        "https://dev.azure.com",
        "https://api.github.com",
        "https://outlook.office.com/",
        "https://api.somewhereintheinternet.com/",
        "https://graph.microsoft.com",
    }

    ch := make(chan string)

    for _, api := range apis {
        go checkAPI(api, ch)
    }

    for i := 0; i < len(apis); i++ {
        fmt.Print(<-ch)
    }

    elapsed := time.Since(start)
    fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}

func checkAPI(api string, ch chan string) {
    _, err := http.Get(api)
    if err != nil {
        ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
        return
    }

    ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}

重新运行程序时,会看到如下所示的输出:

ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
Done! It took 0.602099714 seconds!
程序正在执行应执行的操作。 你不再使用睡眠函数,而是使用通道。 另请注意,在不使用并发时,现在需要约600毫秒完成,而不会耗费近2秒。
最后,我们可以说,无缓冲channel在同步发送和接收操作。 即使使用并发,通信也是同步的。
Go语言教程之边写边学:Goroutines和Channels练习:吸烟者问题

假设一支香烟需要三种成分来制作和吸烟:烟草、纸张和火柴。一张桌子周围有三个吸烟者,每个吸烟者都有三种成分中的一种——一个吸烟者有无限供应的烟草,另一个有纸,第三个有火柴。第四方,拥有无限供应的所有东西,随机选择一个吸烟者,并将香烟所需的用品放在桌子上。被选中的吸烟者吸烟,该过程应无限期重复。

 

示例代码:

package main

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

const (
	paper = iota
	grass
	match
)

var smokeMap = map[int]string{
	paper: "paper",
	grass: "grass",
	match: "match",
}

var names = map[int]string{
	paper: "Sandy",
	grass: "Apple",
	match: "Daisy",
}

type Table struct {
	paper chan int
	grass chan int
	match chan int
}

func arbitrate(t *Table, smokers [3]chan int) {
	for {
		time.Sleep(time.Millisecond * 500)
		next := rand.Intn(3)
		fmt.Printf("Table chooses %s: %s\n", smokeMap[next], names[next])
		switch next {
		case paper:
			t.grass <- 1
			t.match <- 1
		case grass:
			t.paper <- 1
			t.match <- 1
		case match:
			t.grass <- 1
			t.paper <- 1
		}
		for _, smoker := range smokers {
			smoker <- next
		}
		wg.Add(1)
		wg.Wait()
	}
}

func smoker(t *Table, name string, smokes int, signal chan int) {
	var chosen = -1
	for {
		chosen = <-signal // blocks

		if smokes != chosen {
			continue
		}

		fmt.Printf("Table: %d grass: %d match: %d\n", len(t.paper), len(t.grass), len(t.match))
		select {
		case <-t.paper:
		case <-t.grass:
		case <-t.match:
		}
		fmt.Printf("Table: %d grass: %d match: %d\n", len(t.paper), len(t.grass), len(t.match))
		time.Sleep(10 * time.Millisecond)
		select {
		case <-t.paper:
		case <-t.grass:
		case <-t.match:
		}
		fmt.Printf("Table: %d grass: %d match: %d\n", len(t.paper), len(t.grass), len(t.match))
		fmt.Printf("%s smokes a cigarette\n", name)
		time.Sleep(time.Millisecond * 500)
		wg.Done()
		time.Sleep(time.Millisecond * 100)
	}
}

const LIMIT = 1

var wg *sync.WaitGroup

func main() {
	wg = new(sync.WaitGroup)
	table := new(Table)
	table.match = make(chan int, LIMIT)
	table.paper = make(chan int, LIMIT)
	table.grass = make(chan int, LIMIT)
	var signals [3]chan int
	// three smokers
	for i := 0; i < 3; i++ {
		signal := make(chan int, 1)
		signals[i] = signal
		go smoker(table, names[i], i, signal)
	}
	fmt.Printf("%s, %s, %s, sit with \n%s, %s, %s\n\n", names[0], names[1], names[2], smokeMap[0], smokeMap[1], smokeMap[2])
	arbitrate(table, signals)
}

 

输出:

Sandy, Apple, Daisy, sit with
paper, grass, match

Table chooses match: Daisy
Table: 1 grass: 1 match: 0
Table: 1 grass: 0 match: 0
Table: 0 grass: 0 match: 0
Daisy smokes a cigarette
Table chooses paper: Sandy
Table: 0 grass: 1 match: 1
Table: 0 grass: 1 match: 0
Table: 0 grass: 0 match: 0
Sandy smokes a cigarette
Table chooses match: Daisy
Table: 1 grass: 1 match: 0
Table: 1 grass: 0 match: 0
Table: 0 grass: 0 match: 0
Daisy smokes a cigarette
Go语言教程之边写边学:Goroutines和Channels练习:睡眠理发师问题

一个理发店有N张沙发和一张理发椅。没有顾客要理发时,理发师便去睡觉。当一个顾客走进理发店时,如果所有的沙发都已被占用,他便离开理发店;否则,如果理发师正在为其他顾客理发,则该顾客就找一张空沙发坐下等待;如果理发师因无顾客正在睡觉,则由新到的顾客唤醒理发师为其理发。在理发完成后,顾客必须付费,直到理发师收费后才能离开理发店。

示例代码:

package main

import (
	"fmt"
	"sync"
	"time"
)

const (
	sleeping = iota
	checking
	cutting
)

var stateLog = map[int]string{
	0: "Sleeping",
	1: "Checking",
	2: "Cutting",
}
var wg *sync.WaitGroup // Amount of potentional customers

type Barber struct {
	name string
	sync.Mutex
	state    int // Sleeping/Checking/Cutting
	customer *Customer
}

type Customer struct {
	name string
}

func (c *Customer) String() string {
	return fmt.Sprintf("%p", c)[7:]
}

func NewBarber() (b *Barber) {
	return &Barber{
		name:  "Sam",
		state: sleeping,
	}
}

// Barber goroutine
// Checks for customers
// Sleeps - wait for wakers to wake him up
func barber(b *Barber, wr chan *Customer, wakers chan *Customer) {
	for {
		b.Lock()
		defer b.Unlock()
		b.state = checking
		b.customer = nil

		// checking the waiting room
		fmt.Printf("Checking waiting room: %d\n", len(wr))
		time.Sleep(time.Millisecond * 100)
		select {
		case c := <-wr:
			HairCut(c, b)
			b.Unlock()
		default: // Waiting room is empty
			fmt.Printf("Sleeping Barber - %s\n", b.customer)
			b.state = sleeping
			b.customer = nil
			b.Unlock()
			c := <-wakers
			b.Lock()
			fmt.Printf("Woken by %s\n", c)
			HairCut(c, b)
			b.Unlock()
		}
	}
}

func HairCut(c *Customer, b *Barber) {
	b.state = cutting
	b.customer = c
	b.Unlock()
	fmt.Printf("Cutting  %s hair\n", c)
	time.Sleep(time.Millisecond * 100)
	b.Lock()
	wg.Done()
	b.customer = nil
}

// customer goroutine
// just fizzles out if it's full, otherwise the customer
// is passed along to the channel handling it's haircut etc
func customer(c *Customer, b *Barber, wr chan<- *Customer, wakers chan<- *Customer) {
	// arrive
	time.Sleep(time.Millisecond * 50)
	// Check on barber
	b.Lock()
	fmt.Printf("Customer %s checks %s barber | room: %d, w %d - customer: %s\n",
		c, stateLog[b.state], len(wr), len(wakers), b.customer)
	switch b.state {
	case sleeping:
		select {
		case wakers <- c:
		default:
			select {
			case wr <- c:
			default:
				wg.Done()
			}
		}
	case cutting:
		select {
		case wr <- c:
		default: // Full waiting room, leave shop
			wg.Done()
		}
	case checking:
		panic("Customer shouldn't check for the Barber when Barber is Checking the waiting room")
	}
	b.Unlock()
}

func main() {
	b := NewBarber()
	b.name = "Rocky"
	WaitingRoom := make(chan *Customer, 5) // 5 chairs
	Wakers := make(chan *Customer, 1)      // Only one waker at a time
	go barber(b, WaitingRoom, Wakers)

	time.Sleep(time.Millisecond * 100)
	wg = new(sync.WaitGroup)
	n := 10
	wg.Add(10)
	// Spawn customers
	for i := 0; i < n; i++ {
		time.Sleep(time.Millisecond * 50)
		c := new(Customer)
		go customer(c, b, WaitingRoom, Wakers)
	}

	wg.Wait()
	fmt.Println("No more customers for the day")
}

 

输出:

Checking waiting room: 0
Sleeping Barber - 
Customer 120 checks Sleeping barber | room: 0, w 0 - customer: 
Woken by 120
..............
..............
Checking waiting room: 0
No more customers for the day
Go语言教程之边写边学:Goroutines和Channels练习:生产者消费者问题

该问题描述了两个进程,即生产者和使用者,它们共享一个用作队列的公共固定大小的缓冲区。生产者的工作是生成数据,将其放入缓冲区,然后重新开始。同时,消费者正在消耗数据(即,将其从缓冲区中删除),一次一个片段。问题在于确保生产者不会尝试将数据添加到缓冲区中(如果数据已满),并且使用者不会尝试从空缓冲区中删除数据。生产者的解决方案是,如果缓冲区已满,要么进入睡眠状态,要么丢弃数据。下次使用者从缓冲区中删除项目时,它会通知生产者,生产者再次开始填充缓冲区。同样,如果使用者发现缓冲区为空,则可以进入睡眠状态。下次生产者将数据放入缓冲区时,它会唤醒休眠的消费者。

 

示例代码:

package main

import (
	"flag"
	"fmt"
	"log"
	"os"
	"runtime"
	"runtime/pprof"
)

type Consumer struct {
	msgs *chan int
}

// NewConsumer creates a Consumer
func NewConsumer(msgs *chan int) *Consumer {
	return &Consumer{msgs: msgs}
}

// consume reads the msgs channel
func (c *Consumer) consume() {
	fmt.Println("consume: Started")
	for {
		msg := <-*c.msgs
		fmt.Println("consume: Received:", msg)
	}
}

// Producer definition
type Producer struct {
	msgs *chan int
	done *chan bool
}

// NewProducer creates a Producer
func NewProducer(msgs *chan int, done *chan bool) *Producer {
	return &Producer{msgs: msgs, done: done}
}

// produce creates and sends the message through msgs channel
func (p *Producer) produce(max int) {
	fmt.Println("produce: Started")
	for i := 0; i < max; i++ {
		fmt.Println("produce: Sending ", i)
		*p.msgs <- i
	}
	*p.done <- true // signal when done
	fmt.Println("produce: Done")
}

func main() {
	// profile flags
	cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file`")
	memprofile := flag.String("memprofile", "", "write memory profile to `file`")

	// get the maximum number of messages from flags
	max := flag.Int("n", 5, "defines the number of messages")

	flag.Parse()

	// utilize the max num of cores available
	runtime.GOMAXPROCS(runtime.NumCPU())

	// CPU Profile
	if *cpuprofile != "" {
		f, err := os.Create(*cpuprofile)
		if err != nil {
			log.Fatal("could not create CPU profile: ", err)
		}
		if err := pprof.StartCPUProfile(f); err != nil {
			log.Fatal("could not start CPU profile: ", err)
		}
		defer pprof.StopCPUProfile()
	}

	var msgs = make(chan int)  // channel to send messages
	var done = make(chan bool) // channel to control when production is done

	// Start a goroutine for Produce.produce
	go NewProducer(&msgs, &done).produce(*max)

	// Start a goroutine for Consumer.consume
	go NewConsumer(&msgs).consume()

	// Finish the program when the production is done
	<-done

	// Memory Profile
	if *memprofile != "" {
		f, err := os.Create(*memprofile)
		if err != nil {
			log.Fatal("could not create memory profile: ", err)
		}
		runtime.GC() // get up-to-date statistics
		if err := pprof.WriteHeapProfile(f); err != nil {
			log.Fatal("could not write memory profile: ", err)
		}
		f.Close()
	}
}

 

输出:

consume: Started
produce: Started
produce: Sending  0
produce: Sending  1
consume: Received: 0
consume: Received: 1
produce: Sending  2
produce: Sending  3
consume: Received: 2
consume: Received: 3
produce: Sending  4
produce: Done
Go语言教程之边写边学:Goroutines和Channels练习:检查点同步问题

检查点同步是同步多个任务的问题。考虑一个车间,几个工人组装一些机制的细节。当他们每个人都完成他的工作时,他们会把细节放在一起。没有商店,所以先完成部分的工人必须等待其他人才能开始另一个。将细节放在一起是任务在路径分开之前自我同步的检查点。

 

示例代码:

package main

import (
	"log"
	"math/rand"
	"sync"
	"time"
)

func worker(part string) {
	log.Println(part, "worker begins part")
	time.Sleep(time.Duration(rand.Int63n(1e6)))
	log.Println(part, "worker completes part")
	wg.Done()
}

var (
	partList    = []string{"A", "B", "C", "D"}
	nAssemblies = 3
	wg          sync.WaitGroup
)

func main() {
	rand.Seed(time.Now().UnixNano())
	for c := 1; c <= nAssemblies; c++ {
		log.Println("begin assembly cycle", c)
		wg.Add(len(partList))
		for _, part := range partList {
			go worker(part)
		}
		wg.Wait()
		log.Println("assemble.  cycle", c, "complete")
	}
}

 

输出:

2019/07/15 16:10:32 begin assembly cycle 1
2019/07/15 16:10:32 D worker begins part
2019/07/15 16:10:32 A worker begins part
2019/07/15 16:10:32 B worker begins part
........
2019/07/15 16:10:32 D worker completes part
2019/07/15 16:10:32 C worker completes part
2019/07/15 16:10:32 assemble.  cycle 3 complete

 

Go语言教程之边写边学:Goroutines和Channels练习:哲学家就餐问题

 

哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。哲学家就餐问题有时也用米饭和筷子而不是意大利面和餐叉来描述,因为很明显,吃米饭必须用两根筷子。

哲学家从来不交谈,这就很危险,可能产生死锁,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。即使没有死锁,也有可能发生资源耗尽。例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试。这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生"活锁"。如果五位哲学家在完全相同的时刻进入餐厅,并同时拿起左边的餐叉,那么这些哲学家就会等待五分钟,同时放下手中的餐叉,再等五分钟,又同时拿起这些餐叉。

在实际的计算机问题中,缺乏餐叉可以类比为缺乏共享资源。一种常用的计算机技术是资源加锁,用来保证在某个时刻,资源只能被一个程序或一段代码访问。当一个程序想要使用的资源已经被另一个程序锁定,它就等待资源解锁。当多个程序涉及到加锁的资源时,在某些情况下就有可能发生死锁。例如,某个程序需要访问两个文件,当两个这样的程序各锁了一个文件,那它们都在等待对方解锁另一个文件,而这永远不会发生。

 

 

 

示例代码:main.go

package main

import (
	"hash/fnv"
	"log"
	"math/rand"
	"os"
	"sync"
	"time"
)

// Number of philosophers is simply the length of this list.
var ph = []string{"Mark", "Russell", "Rocky", "Haris", "Root"}

const hunger = 3                // Number of times each philosopher eats
const think = time.Second / 100 // Mean think time
const eat = time.Second / 100   // Mean eat time

var fmt = log.New(os.Stdout, "", 0)

var dining sync.WaitGroup

func diningProblem(phName string, dominantHand, otherHand *sync.Mutex) {
	fmt.Println(phName, "Seated")
	h := fnv.New64a()
	h.Write([]byte(phName))
	rg := rand.New(rand.NewSource(int64(h.Sum64())))
	rSleep := func(t time.Duration) {
		time.Sleep(t/2 + time.Duration(rg.Int63n(int64(t))))
	}
	for h := hunger; h > 0; h-- {
		fmt.Println(phName, "Hungry")
		dominantHand.Lock() // pick up forks
		otherHand.Lock()
		fmt.Println(phName, "Eating")
		rSleep(eat)
		dominantHand.Unlock() // put down forks
		otherHand.Unlock()
		fmt.Println(phName, "Thinking")
		rSleep(think)
	}
	fmt.Println(phName, "Satisfied")
	dining.Done()
	fmt.Println(phName, "Left the table")
}

func main() {
	fmt.Println("Table empty")
	dining.Add(5)
	fork0 := &sync.Mutex{}
	forkLeft := fork0
	for i := 1; i < len(ph); i++ {
		forkRight := &sync.Mutex{}
		go diningProblem(ph[i], forkLeft, forkRight)
		forkLeft = forkRight
	}
	go diningProblem(ph[0], fork0, forkLeft)
	dining.Wait() // wait for philosphers to finish
	fmt.Println("Table empty")
}

 

输出:

Table empty
Mark seated
Mark Hungry
Mark Eating
..................
..................
Haris Thinking
Haris Satisfied
Haris Left the table
Table empty
Go语言教程之边写边学:Goroutines和Channels练习:查找奇数和偶数

下面的程序启动两个Goroutines。这两个Goroutine现在并发运行。我们创建了两个无缓冲的通道,并将它们传递给goroutines作为通道接收到的值的参数。

 

示例代码:main.go

package main

import (
	"fmt"
)

func main() {
	var intSlice = []int{91, 42, 23, 14, 15, 76, 87, 28, 19, 95}
	chOdd := make(chan int)
	chEven := make(chan int)

	go odd(chOdd)
	go even(chEven)

	for _, value := range intSlice {
		if value%2 != 0 {
			chOdd <- value
		} else {
			chEven <- value
		}
	}

}

func odd(ch <-chan int) {
	for v := range ch {
		fmt.Println("ODD :", v)
	}
}

func even(ch <-chan int) {
	for v := range ch {
		fmt.Println("EVEN:", v)
	}
}

 

输出:

ODD : 91
ODD : 23
EVEN: 42
EVEN: 14
EVEN: 76
ODD : 15
ODD : 87
ODD : 19
ODD : 95
Go语言教程之边写边学:Goroutines和Channels练习:Goroutines通道执行顺序

您在代码中调用go协程,但无法判断协程何时结束,值何时传递到缓冲通道。由于此代码是异步的,因此每当协程完成时,它都会将数据写入通道并在另一端读取。在上面的示例中,您只调用了两个go协程,因此行为是确定的,并且在大多数情况下会以某种方式生成相同的输出,但是当您增加go协程时,输出将不相同,并且顺序将不同,除非您使其同步。

 

示例代码:main.go

package main

import "fmt"

func sum(a []int, c chan int) {
	sum := 0
	for _, v := range a {
		sum += v
	}
	c <- sum
}

func main() {
	a := []int{17, 12, 18, 9, 24, 42, 64, 12, 68, 82, 57, 32, 9, 2, 12, 82, 52, 34, 92, 36}

	c := make(chan int)
	for i := 0; i < len(a); i = i + 5 {
		go sum(a[i:i+5], c)
	}
	output := make([]int, 5)
	for i := 0; i < 4; i++ {
		output[i] = <-c
	}
	close(c)

	fmt.Println(output)
}

 

输出:

[296 80 268 112 0]
Go语言教程之边写边学:Goroutines和Channels练习:将斐波那契数列读写到通道

main函数有两个无缓冲通道ch和quit。在斐波那契函数中,select语句会阻塞,直到其中一个情况准备就绪。

示例代码:main.go

package main

import (
	"fmt"
)

func fibonacci(ch chan int, quit chan bool) {
	x, y := 0, 1
	for {
		select {
		case ch <- x: // write to channel ch
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	ch := make(chan int)
	quit := make(chan bool)
	n := 10

	go func(n int) {
		for i := 0; i < n; i++ {
			fmt.Println(<-ch) // read from channel ch
		}
		quit <- false
	}(n)

	fibonacci(ch, quit)
}

 

输出:

0
1
1
2
3
5
8
13
21
34
Go语言教程之边写边学:Goroutines和Channels练习:从通道发送和接收值

主要功能有两个功能:generator和receiver。我们创建一个c int通道并从生成器函数返回它。在匿名goroutine中运行的for循环将值写入通道c。

 

示例代码:main.go

package main

import (
	"fmt"
)

func main() {
	c := generator()
	receiver(c)
}

func receiver(c <-chan int) {
	for v := range c {
		fmt.Println(v)
	}
}

func generator() <-chan int {
	c := make(chan int)

	go func() {
		for i := 0; i < 10; i++ {
			c <- i
		}
		close(c)
	}()

	return c
}

 

输出:

0
1
2
3
4
5
6
7
8
9
Go语言教程之边写边学:Goroutines和Channels练习:启动多个Goroutine,每个goroutine都向一个频道添加值

该程序从10个Goroutines开始。我们创建了一个ch字符串通道,并通过同时运行10个goroutines将数据写入该通道。箭头相对于通道的方向指定是发送还是接收数据。指向ch的箭头指定我们要写入通道ch。箭头从ch向外指向指定我们正在从通道ch读取。

 

示例代码:main.go

package main

import (
	"fmt"
	"strconv"
)

func main() {
	ch := make(chan string)

	for i := 0; i < 10; i++ {
		go func(i int) {
			for j := 0; j < 10; j++ {
				ch <- "Goroutine : " + strconv.Itoa(i)
			}
		}(i)
	}

	for k := 1; k <= 100; k++ {
		fmt.Println(k, <-ch)
	}
}

 

输出:

1 Goroutine : 9
2 Goroutine : 9
3 Goroutine : 2
4 Goroutine : 0
5 Goroutine : 1
6 Goroutine : 5
7 Goroutine : 3
8 Goroutine : 4
9 Goroutine : 7
10 Goroutine : 6
....................
Go语言教程之边写边学:通道 channel

在Go中,通道是一种内置的数据结构,用于goroutines之间的通信和同步。通道是该语言的基本特征,可实现 goroutine之间的安全高效的通信和同步。

通道本质上是一个管道,允许数据在goroutines之间传递。它具有特定的类型,该类型决定了可以通过通道发送的数据类型。通道是使用内置的make函数创建的,可以缓冲或取消缓冲。

未缓冲的通道会阻止发送goroutine,直到有相应的接收器准备好接收正在发送的值。这意味着保证按发送顺序接收数据,并且同步内置于通道中。

另一方面,缓冲通道可以保存有限数量的值(由缓冲区大小决定),并且仅在缓冲区已满时阻止发送goroutine。这可以允许一些额外的并发性,但需要仔细考虑以避免死锁和其他同步问题。

通道通常用于协调不同goroutines的活动,允许它们共享数据并协同工作,而无需显式锁定或同步。它们是在Golang中构建并发和并行程序的强大工具。

例如,若要创建int类型的通道,可以使用以下代码:

ch := make(chan int)

创建通道后,可以使用<-运算符将值发送到通道,并使用相同的运算符从通道接收值。例如:

ch <- 42 // 发送值42到ch通道
x := <-ch // 从ch通道中读取一个值

通道还可用于通过发送和接收不携带任何数据的值在goroutines之间发出信号。例如,通道可用于发出goroutine终止的信号:

done := make(chan bool)

go func() {
    done <- true // 当前工作完成的信号
}()

// 等待goroutine执行完
<-done

通道是 Go 并发模型的一个重要特性,可用于构建多种类型的并发系统。

 

Golang中的缓冲通道

在 Go 中,缓冲通道是一种具有缓冲区的通道,可以存储一定数量的值。缓冲通道在有多个生产者或使用者的情况下很有用,或者当生产者和使用者以不同的速率运行时。

创建缓冲通道时,将使用容量对其进行初始化,容量是通道缓冲区中可以存储的最大值数。创建通道时,容量被指定为make函数的第二个参数。

下面是创建容量为3的整数缓冲通道的示例:

ch := make(chan int, 3)

在此示例中,通道的容量为3,这意味着它最多可以在其缓冲区中存储3个整数。

若要将值发送到缓冲通道,请使用<-运算符。如果通道的缓冲区未满,则该值将添加到缓冲区中。如果缓冲区已满,则发送方将被阻止,直到缓冲区中有空间。

例如,以下代码将三个值发送到缓冲通道:

ch <- 1
ch <- 2
ch <- 3

此时,通道的缓冲区已满,任何进一步的发送操作都将阻塞,直到从通道接收到值。

若要从缓冲通道接收值,请使用 <- 运算符。如果通道的缓冲区不为空,则将删除并返回缓冲区前面的值。如果缓冲区为空,接收器将被阻塞,直到将值发送到通道。

例如,下面的代码从缓冲通道接收两个值:

x := <-ch // x = 1
y := <-ch // y = 2

此时,通道的缓冲区还剩下一个值,任何进一步的接收操作都将阻塞,直到将值发送到通道。

缓冲通道可用于构建多种类型的并发系统,例如管道和工作线程池,其中多个生产者和使用者以不同的速率运行。但是,请务必选择适当的缓冲区大小以避免死锁或性能问题。

下面是在 Go 中使用缓冲通道生成斐波那契数列的示例:

使用缓冲通道的斐波那契数列

package main

import "fmt"

func fib(n int, ch chan<- int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        ch <- x
        x, y = y, x+y
    }
    close(ch)
}

func main() {
    ch := make(chan int, 10)
    go fib(10, ch)

    // 从ch中读取值并打印,直到通道关闭
    for x := range ch {
        fmt.Println(x)
    }
}

在此示例中,fib函数生成前n个斐波那契数并将它们发送到通道中。main函数创建一个容量为10的缓冲通道,并启动一个单独的goroutine来生成斐波那契数列。然后,主goroutine从通道读取值,直到它关闭。

请注意,fib函数中的ch参数是仅接收通道,这意味着它只能用于接收值。这是由chan<- int类型声明强制执行的。

另请注意,close(ch)语句用于表示序列的结束。这对于防止主goroutine在通道上无限期阻塞是必要的。

当您运行此程序时,它将输出前10个斐波那契数:

0
1
1
2
3
5
8
13
21
34

 

Golang 中的无缓冲通道

无缓冲通道是一种没有缓冲区的通道,用于goroutines之间的同步通信。当在未缓冲的通道上发送值时,发送goroutine会阻塞,直到另一个goroutine从该通道接收到该值。同样,当一个goroutine尝试从未缓冲的通道接收值时,它会阻塞,直到另一个goroutine向通道发送值。

无缓冲通道对于确保goroutines之间的通信同步以及可靠地传输数据非常有用。它们可用于构建多种类型的并发系统,例如消息传递系统和管道。

下面是使用无缓冲通道在两个 goroutines 之间传输值的示例:

package main

import "fmt"

func doSomething(ch chan int) {
    x := 42
    ch <- x
}

func main() {
    ch := make(chan int)

    go doSomething(ch)

    x := <-ch
    fmt.Println(x)
}

在此示例中,doSomething函数在未缓冲的通道通道上发送值42。然后,main函数从通道接收值并打印它。

请注意,<- 运算符用于在通道上发送和接收值。在分配的左侧使用时,<- 运算符在通道上发送一个值。在分配的右侧使用时,它会从通道接收值。

另请注意,接收操作 x  := <-ch 会阻塞,直到发送goroutine在通道上发送值。

当您运行此程序时,它将输出值42。

 

下面是在Go中使用无缓冲通道生成斐波那契数列的示例:

package main

import "fmt"

func fib(n int, ch chan<- int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        ch <- x
        x, y = y, x+y
    }
}

func main() {
    ch := make(chan int)
    go fib(10, ch)

    for i := 0; i < 10; i++ {
        x := <-ch
        fmt.Println(x)
    }
}

在此示例中,fib函数生成前n个斐波那契数并将它们发送到通道中。main函数创建一个无缓冲的通道,并启动一个单独的goroutine来生成斐波那契数列。然后,主goroutine通过使用<-ch执行10个接收操作从通道读取前10个值并打印它们。

请注意,由于通道是无缓冲的,fib函数将阻塞,直到主goroutine准备好从通道接收值。

当您运行此程序时,它将输出前10个斐波那契数:


0
1
1
2
3
5
8
13
21
34

 

如何优雅地关闭 Golang 中的通道?

通道用于 go协程之间的通信,正确关闭通道以避免阻塞和泄漏很重要。以下是在 Go 中优雅关闭频道的步骤:

  • 只有发件人应关闭通道:请务必记住,只有发件人应关闭通道。关闭通道表示不会再在通道上发送值,任何在通道上发送的尝试都会导致panic。
  • 使用范围循环从通道接收值:范围循环可用于从通道接收值,直到通道关闭。当通道关闭时,循环将自动终止,并且将接收通道关闭之前发送的任何值。
  • 在接收值之前检查通道是否关闭:可以使用逗号确定习惯用法在接收值之前检查通道是否关闭。该习惯用法返回两个值,从通道接收的值和一个布尔值,如果通道打开,则为true,如果通道关闭,则为false。
  • 使用select语句从多个通道接收值:如果要从多个通道接收值,则可以使用select语句接收值,直到关闭所有通道。select语句将阻塞,直到从其中一个通道接收到值或关闭所有通道。

下面是如何在 Go 中优雅地关闭通道的示例:

func worker(input chan int, output chan int, done chan bool) {
  for {
    select {
    case n := <-input:
      output <- n * 2
    case <-done:
      close(output)
      return
    }
  }
}

func main() {
  input := make(chan int)
  output := make(chan int)
  done := make(chan bool)

  go worker(input, output, done)

  for i := 0; i < 10; i++ {
    input <- i
  }

  // 关闭input通道
  close(input)

  // 读取output通道
  for n := range output {
    fmt.Println(n)
  }

  // 停止worker
  done <- true
}

在此示例中,worker函数从input通道接收值,执行一些工作,并在output通道上发送结果。done通道用于在处理完所有输入后向工作线程发出退出信号。main函数在input通道上发送一些值,关闭input通道,然后使用范围循环从output通道接收值。最后,done通道用于向工人发出退出信号。

 

如何在Golang中更改选择循环的通道?

可以在for-select循环中更改正在选择的通道。为此,您可以使用default来选择新通道。

下面是如何在Go中的for-select循环中更改所选频道的示例:

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        for i := 0; i < 10; i++ {
            ch1 <- i
        }
        close(ch1)
    }()

    go func() {
        for i := 10; i < 20; i++ {
            ch2 <- i
        }
        close(ch2)
    }()

    for {
        select {
        case x, ok := <-ch1:
            if ok {
                fmt.Println("Received from ch1:", x)
            } else {
                fmt.Println("ch1 closed")
                ch1 = nil // 设置ch1为nil,停止接收
            }
        case x, ok := <-ch2:
            if ok {
                fmt.Println("Received from ch2:", x)
            } else {
                fmt.Println("ch2 closed")
                ch2 = nil // 设置ch2为nil,停止接收
            }
        default:
            // select a new channel to receive from
            if ch1 == nil && ch2 == nil {
                // 两个同都都已关闭
                return
            } else if ch1 == nil {
                fmt.Println("Waiting for ch2")
                <-ch2
            } else if ch2 == nil {
                fmt.Println("Waiting for ch1")
                <-ch1
            } else {
                select {
                case x, ok := <-ch1:
                    if ok {
                        fmt.Println("Received from ch1:", x)
                    } else {
                        fmt.Println("ch1 closed")
                        ch1 = nil
                    }
                case x, ok := <-ch2:
                    if ok {
                        fmt.Println("Received from ch2:", x)
                    } else {
                        fmt.Println("ch2 closed")
                        ch2 = nil
                    }
                }
            }
        }
    }
}

在这个例子中,我们有两个通道ch1ch2。我们生成两个goroutines来将值发送到通道,然后关闭它们。在for-select循环中,我们首先从ch1ch2中进行选择。当一个频道关闭时,我们将其设置为nil 以停止从它接收。然后,我们使用default来选择要从中接收的新通道。如果两个通道均为零,则退出循环。如果一个通道为 nil,我们等待另一个通道发送值。如果两个通道都打开,我们将像往常一样从通道中进行选择。

 

如何使用for/select语法从多个通道接收数据?

您可以使用for-select语法从多个通道接收数据。for-select循环允许您等待来自多个通道的数据,并在数据到达时对其进行处理。

下面是如何使用for-select语法从多个通道接收数据的示例:

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        for i := 0; i < 10; i++ {
            ch1 <- i
        }
        close(ch1)
    }()

    go func() {
        for i := 10; i < 20; i++ {
            ch2 <- i
        }
        close(ch2)
    }()

    for {
        select {
        case x, ok := <-ch1:
            if ok {
                fmt.Println("Received from ch1:", x)
            } else {
                fmt.Println("ch1 closed")
            }
        case x, ok := <-ch2:
            if ok {
                fmt.Println("Received from ch2:", x)
            } else {
                fmt.Println("ch2 closed")
            }
        }
    }
}

在这个例子中,我们有两个通道ch1ch2。我们生成两个goroutines来将值发送到通道,然后关闭它们。在 for-select 循环中,我们首先从ch1和ch2中进行选择。当从通道接收到值时,我们打印该值。如果通道已关闭,我们将打印一条消息,指示该通道已关闭。

请注意,select语句将阻塞,直到从其中一个通道接收到值。如果多个通道有可用数据,则将随机选择其中一个。

此外,请务必注意,如果通道从未关闭并且没有从中接收数据,则select语句将无限期阻塞。因此,在通道上发送所有数据后关闭通道始终是一种很好的做法,以避免阻塞问题。

 

从已关闭通道接收

如果某个通道已关闭,则在该通道上发送值的任何进一步尝试都将导致panic。但是,从已关闭通道接收是一种安全操作,并且可以从已关闭通道接收值,直到读取通道中的所有值为止。

关闭通道时,可以保证在通道关闭之前,将按照发送顺序接收之前在该通道上发送的所有值。收到所有值后,任何后续接收操作都将立即返回通道类型的零值,而不会阻塞。

下面是从已关闭通道接收的示例:

ch := make(chan int)
go func() {
    ch <- 1
    ch <- 2
    close(ch)
}()

for {
    i, ok := <-ch
    if !ok {
        fmt.Println("Channel closed")
        break
    }
    fmt.Println("Received", i)
}

在此示例中,使用make函数创建int类型的通道。启动一个goroutine,在通道上发送两个值,然后关闭它。for循环用于从通道接收值,直到通道关闭。ok 变量用于检查通道是否仍处于打开状态。如果通道关闭,则终止for循环。

请注意,使用ok变量检查通道是否仍处于打开状态非常重要,因为尝试在没有此检查的情况下从关闭的通道接收可能会导致panic。

 

使用通道作为函数参数的示例

通道可以用作函数参数,以允许goroutines之间的通信。这对于在goroutines之间传递数据以及goroutine完成执行时发出信号非常有用。

下面是一个函数示例,该函数将通道作为参数并在该通道上发送值:

func sendData(ch chan<- int) {
    ch <- 1
    ch <- 2
    ch <- 3
    close(ch)
}

在此示例中,sendData函数将chan<- int类型的通道作为参数。这意味着通道只能用于发送值,而不能用于接收值。该函数在通道上发送三个值,然后关闭它。

下面是如何调用sendData函数的示例:

ch := make(chan int)
go sendData(ch)

for i := range ch {
    fmt.Println(i)
}

在此示例中,使用make函数创建int类型的通道。sendData函数在单独的goroutine中调用,将通道作为参数传递。for范围循环用于从通道接收值,直到通道闭合。当通道关闭时,循环将自动终止。

请注意,函数中的通道参数以chan<- int的方向声明,表示它只能用于发送值。这是由Go编译器强制执行的,尝试在函数内部的通道上接收值将导致编译时错误。

 

sync.WaitGroup相对于channel有哪些优势?

sync.WaitGroup和频道是Go中用于管理并发的两种不同工具,它们具有不同的用例和优势。

sync.WaitGroup用于等待一组goroutines完成,然后再继续执行。它提供了一种简单有效的方法来同步goroutines,而无需复杂的通道通信。使用WaitGroup,您只需添加预期运行的goroutines数量,在 WaitGroup上调用 Add(n)然后调用Wait()进行阻止,直到所有goroutines完成。

另一方面,通道用于goroutines之间的通信和同步。它们为goroutines提供了一种发送和接收值并协调其执行的方法。通道可用于协调多个goroutines时、在它们之间共享数据以及发出事件信号。

与channel相比,WaitGroup的一个优点是,它更简单、更易于使用,适用于您只需要等待一组 goroutines 完成的情况。通道功能更强大,但也更复杂,对于简单的同步任务来说可能有点过分。

WaitGroup的另一个优点是,对于简单的同步任务,它比通道更有效。通道在内存使用和性能方面都有一些开销,并且专为更复杂的用例而设计。另一方面,WaitGroup是轻量级的,专为等待一组 goroutines而设计。

话虽如此,通道仍然是许多同步任务的最佳工具,尤其是那些涉及数据共享或goroutines之间更复杂的协调的任务。因此,这最终取决于特定的用例以及您要完成的任务。

 

如何判断缓冲通道是否已满?

您可以使用通道上的len函数检查缓冲通道是否已满。len函数返回通道缓冲区中当前元素的数量。

例如,如果你有一个容量为10的缓冲通道,你可以通过将len(channel)的结果与缓冲区大小进行比较来检查它是否已满:

channel := make(chan int, 10)

// 缓冲通道是否已满
if len(channel) == cap(channel) {
    fmt.Println("Channel is full")
}

在此示例中,len(channel)函数返回通道缓冲区中的当前元素数,cap(channel)返回缓冲区大小。如果len(channel)等于cap(channel)则通道已满。

值得注意的是,len函数仅返回通道缓冲区中的元素数,而不返回通道上活动发送方或接收方的数量。因此,如果通道上有多个goroutines发送或接收,len函数可能无法准确反映通道的当前状态。

为了避免在检查通道长度时出现争用条件,请务必在select语句中使用通道操作(<-或 chan<-),或使用Mutex锁定对通道的访问。

 

如何使用通道广播消息?

你可以使用通道广播消息,方法是创建一个缓冲区大小为1的通道,并从多个goroutines向其发送消息。通道充当消息队列,从通道读取的第一个goroutine接收消息。

下面是使用通道将消息广播到多个goroutines的示例:

package main

import "fmt"

func main() {
    ch := make(chan string, 1)

    // 启动3个goroutine读取ch消息
    for i := 0; i < 3; i++ {
        go func() {
            msg := <-ch
            fmt.Println("Received message:", msg)
        }()
    }

    // 广播消息
    ch <- "Hello, World!"
}

在此示例中,我们创建一个缓冲区大小为1的缓冲通道ch。然后,我们启动三个使用阻塞接收操作(msg := <-ch)从通道读取的goroutine。当一条消息被发送到通道(ch ←"Hello, World!")时,其中一个goroutines将接收该消息并打印出来。

由于通道的缓冲区大小为1,因此一次只能有一个 goroutine 从通道读取。但是,由于通道是缓冲的,因此可以排队等待goroutines在将来读取多个消息。

请注意,在此示例中,goroutines在消息广播之前启动,因此它们都在等待消息到达通道。如果消息是在goroutines启动之前发送到通道的,则某些goroutines可能无法收到消息,因为它们可能在发送消息之前已经完成了接收操作。

 

通道阻塞是如何工作的?

通道使用阻塞操作来同步goroutines之间的通信。当goroutine在通道上发送或接收消息并且没有其他goroutine可用于完成通信时,通道是一种阻塞操作。发生这种情况时,goroutine将阻塞或等待,直到另一个goroutine可用来完成操作。

当goroutine在通道上发送消息时,如果通道已满,该操作将阻塞,这意味着当前没有其他goroutines从通道接收。goroutine将保持阻塞状态,直到通道缓冲区中有空间或另一个goroutine开始从通道接收。

同样,当goroutine在通道上收到消息时,如果通道为空,该操作将阻塞,这意味着当前没有其他goroutines发送到通道。goroutine将保持阻塞状态,直到通道缓冲区中有消息或另一个goroutine开始发送到通道。

通道的阻塞行为允许goroutines同步其执行,从而可以跨多个goroutines协调操作的时间和顺序。通道确保每个通信都是原子的,并且发送方和接收方是同步的,这有助于防止争用条件和其他同步问题。

值得注意的是,在频道上阻止可能是一个强大的功能,但如果使用不当,它也可能是错误和性能问题的根源。重要的是要确保通道在某个时候始终被解锁,方法是让其他goroutines在通道上进行通信,或者使用超时或其他机制来确保被阻止的goroutines不会无限期阻塞。

 

哪种通道类型使用最少的内存量?

通道的大小取决于其类型和容量。Go中的两种主要通道类型是无缓冲通道和缓冲通道。

无缓冲通道的容量为0,这意味着它们只能在发送方和接收方都准备好通信时传输值。由于无缓冲通道专为同步通信而设计,因此它们使用任何通道类型中最少的内存量。

另一方面,缓冲通道的容量大于0,这允许它们传输多个值而无需相应的接收操作。缓冲通道比未缓冲通道使用更多的内存,因为它们必须维护等待传输的值队列。

通道使用的内存量还取决于要传输的值的类型。例如,chan bool 类型的通道比chan string类型的通道使用更少的内存,因为布尔值在内存中的内存小于字符串值。

通常,如果需要优化内存使用情况,使用无缓冲通道和具有较小值类型的通道将有助于最大程度地减少程序的内存占用。但是,请务必记住,最佳通道类型将取决于程序的特定要求,并且在内存使用情况和其他性能特征(如吞吐量和延迟)之间可能存在权衡。

 

通道范围与对通道使用 Select 语句

有两种常见的方法可以从通道接收值:使用范围循环或使用select语句。这两种方法都有自己的优点和缺点,它们之间的选择将取决于您程序的特定要求。

使用范围循环是循环访问通道中的值直到通道关闭的简单方法。例如:

ch := make(chan int)
go func() {
    ch <- 1
    ch <- 2
    ch <- 3
    close(ch)
}()

for val := range ch {
    fmt.Println(val)
}

此代码创建一个通道ch,并启动一个goroutine,该例程在通道上发送三个值,然后关闭它。主goroutine使用范围循环从通道接收值并打印它们。

使用select语句允许您从多个通道接收值,并根据哪个通道首先发送值执行不同的操作。例如:

ch1 := make(chan int)
ch2 := make(chan int)

go func() {
    ch1 <- 1
}()

go func() {
    ch2 <- 2
}()

for i := 0; i < 2; i++ {
    select {
    case val := <-ch1:
        fmt.Println("Received from ch1:", val)
    case val := <-ch2:
        fmt.Println("Received from ch2:", val)
    }
}
  • 当前日期:
  • 北京时间:
  • 时间戳:
  • 今年的第:18周
  • 我的 IP:3.137.154.13
农历
五行
冲煞
彭祖
方位
吉神
凶神
极简任务管理 help
+ 0 0 0
Task Idea Collect