golang学习笔记6——并发

Posted yubo_725

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang学习笔记6——并发相关的知识,希望对你有一定的参考价值。

goroutine

golang里面没有线程的概念,取而代之的是一种叫做goroutine的东西,它是由golang的运行时去调度的,可以完成并发操作。

使用goroutine很简单,直接使用go关键字就行,如下面的代码:

package main

import (
	"fmt"
)

func test() 
	fmt.Println("call test...")


func main() 
	fmt.Println("start main")
	// 使用go关键字创建一个goroutine并在其中执行test函数
	go test()

	var s string
	fmt.Scanln(&s)

	fmt.Println("end main")

test函数中仅仅打印一个字符串,然后在main函数中调用go test()test函数在一个goroutine中执行,另外通过fmt.Scanln()从控制台中接收一条输入,这么做是为了让main函数不立刻结束,如果main函数立刻结束,则go test()这个goroutine不会得到执行,执行上面的代码结果如下:

start main
call test...
hello // 这行为控制台输入hello后回车 
end main

除了使用go 函数名()这种方式创建goroutine之外,还可以使用匿名函数创建goroutine,例如:

func main() 
	go func() 
		fmt.Println("hello golang!")
	()   // 注意这里的括号

	var s string
	fmt.Scanln(&s)

	fmt.Println("end main")

需要注意的是,使用匿名函数创建goroutine,函数末尾要加上一对小括号,表示执行这个匿名函数。

并发和并行

  • 并发:多个任务被分配不同的时间片来调度运行,同一时刻只会有一个任务在执行。

  • 并行:多个任务被不同的CPU执行,同一时刻可以有多个任务同事运行。

golang通道

通道是golang中用于在不同的goroutine中进行数据通信的一种方式,声明一个通道可以使用如下方式:

// 定义一个字符串类型的通道
var ch chan string
// 定义一个任意类型的通道
var ch1 chan interface

可以使用make函数创建一个通道:

// make函数创建一个字符串类型的通道
ch := make(chan string)

可以往通道中发送数据,也可以从通道中获取数据,发送数据使用如下方式:

// 将一个hello字符串发送到ch通道中
ch <- "hello"

从通道中读取数据使用如下方式:

// 从通道ch中读取字符串并赋值给s
var s string = <-ch

下面的代码演示了往通道中写数据,然后在另一个通道中读取数据:

import (
	"fmt"
	"time"
)

func main() 
	// 定义一个字符串类型的通道
	ch := make(chan string)

	// 使用匿名函数创建goroutine并在其中发送字符串
	go func(ch chan string) 
		for i := 0; i < 5; i++ 
			data := fmt.Sprintf("item %d", i)
			// 往通道中写数据
			ch <- data
			time.Sleep(time.Second)
		
		ch <- "done"
	(ch)

	for                      // (1)
		// 从通道中读数据
		str := <-ch
		if str == "done" 
			// 通道中读的是"done"则结束for循环
			break
		 else 
			// 打印从通道中读取的数据
			fmt.Println("read from channel: " + str)
		
	

通道是可以遍历的,所以上面的代码中(1)那个for循环,可以改为如下形式:

for data := range ch 
	if data == "done" 
		break
	 else 
		fmt.Println("read from channel: " + data)
	

单向通道与带缓冲的通道

单向通道

golang中可以定义单向通道,单向通道即只能发送数据或者只能接收数据的通道,声明单向通道的方式如下:

// 声明只能接收数据的字符串通道
var reciveCh <-chan string

// 声明只能发送数据的字符串通道
var sendCh chan<- string

下面的代码展示了单向通道的用法:

// 读函数,接收一个只读的字符串通道
func Reader(ch <-chan string) 
	str := <-ch
	fmt.Println("read data: " + str)


// 写函数,接收一个只写的字符串通道
func Writer(ch chan<- string) 
	ch <- "hello world!"


func main() 
	// 创建字符串通道
	ch := make(chan string)
	
	// 只读通道
	var readOnlyCh <-chan string = ch
	// 只写通道
	var writeOnlyCh chan<- string = ch
	// 开启goroutine
	go Reader(readOnlyCh)
	go Writer(writeOnlyCh)

	var line string
	fmt.Scanln(&line)

如果在开启goroutine时函数参数中的通道类型不对,则编译会报错。

带缓冲的通道

使用make创建通道时,可以指定一个整型数据来创建一个带缓冲的通道,如下代码:

// 创建一个带缓冲的通道,缓冲区大小为2
ch := make(chan string, 2)

下面的代码演示了缓冲通道的使用:

ch := make(chan string, 2)
fmt.Println(len(ch)) // 0
ch <- "hello"
ch <- "world"
fmt.Println(len(ch)) // 2

// 这行报错: fatal error: all goroutines are asleep - deadlock!
ch <- "haha" 

对于缓冲通道,可以这么理解:公司使用指纹识别考勤机打卡,每次只能一个人按指纹,如果有多个人则需要排队按指纹打卡,后来公司改进了打卡方式,可以使用手机连接公司内部Wi-Fi完成打卡,Wi-Fi打卡同一时刻允许20个人一起打,这就类似于缓冲通道。

上面的代码在使用make创建了缓冲通道后,打印出的通道长度为0,然后往通道中写入2个字符串,再次打印出的通道长度为2,如果继续往通道中写入数据,则代码报错。

select从不同的通道中获取数据

例子一:

func Sender1(ch chan string) 
	ch <- "hello"


func Sender2(ch chan int) 
	ch <- 0


func main() 
	ch := make(chan string)
	ch2 := make(chan int)
	go Sender1(ch)
	go Sender2(ch2)
	for i := 0; i < 2; i++ 
		select 
		// 从ch通道中获取数据
		case s := <-ch:
			fmt.Println(s)
		// 从ch2通道中获取数据
		case i := <-ch2:
			fmt.Println(i)
		
	

例子二:

import (
	"fmt"
	"time"
)

func main() 
	// ticker是循环执行的
	ticker := time.NewTicker(time.Millisecond * 500)

	// timer是定时执行的
	timer := time.NewTimer(time.Second * 5)

	counter := 0

	for 
		select 
		case <-ticker.C:
			counter++
			fmt.Printf("counter = %d\\n", counter)
		case <-timer.C:
			fmt.Println("done!")
			// 跳出循环
			goto StopHere
		
	
StopHere:
	fmt.Println("end")

以上代码执行结果:

counter = 1
counter = 2
counter = 3
counter = 4
counter = 5
counter = 6
counter = 7
counter = 8
counter = 9
counter = 10
done!
end

关闭通道

golang中的通道可以通过close函数来关闭,被关闭的通道将无法再写入数据,如下代码所示:

func main() 
	ch := make(chan int, 3)

	ch <- 1

	fmt.Printf("len: %d\\n", len(ch)) // len: 1

	close(ch)

	ch <- 2  // panic: send on closed channel

从已关闭的通道中获取数据,将不会发生阻塞:

func main() 
	ch := make(chan int, 3)

	ch <- 1
	ch <- 2

	fmt.Printf("len: %d, cap: %d\\n", len(ch), cap(ch)) // len: 2, cap: 3

	close(ch)

	l := len(ch) + 1

	for i := 0; i < l; i++ 
		v, ok := <-ch
		fmt.Printf("v: %d, ok: %v\\n", v, ok)
	

以上代码执行结果:

len: 2, cap: 3
v: 1, ok: true
v: 2, ok: true
v: 0, ok: false

在上面的代码中,ch通道里放入了两个整数,然后关闭这个通道,再遍历3次从通道中获取数据,可以看到第三次获取数据时并没有报错,而是返回0值和false,表示从通道中获取数据失败,整型默认值为0,如果ch是字符串类型的通道,则默认值为空字符串。

互斥锁

互斥锁可以保证同一时刻有且只有一个goroutine访问某个资源,比如下面的代码:

var (
	count int
	lock  sync.Mutex
)

// 获取count的值,获取前后加锁和解锁
func GetCount() int 
	lock.Lock()

	defer lock.Unlock()

	fmt.Printf("---get count: %d---\\n", count)

	return count


// 设置count的值,设置前后加锁和解锁
func SetCount(c int) 
	lock.Lock()
	count = c
	fmt.Printf("---set count: %d---\\n", c)
	lock.Unlock()

在读多写少的环境中,可以使用读写互斥锁sync.RWMutex,它比互斥锁的效率更高,如下代码所示:

var (
	count int = 0
	lock  sync.RWMutex
)

func GetCount() int 
	lock.RLock()

	defer lock.RUnlock()

	return count

等待数组

golang中的等待数组也可以完成任务的同步执行,等待数组内部有计数器,开始执行任务前,将计数器值+1,当一个任务完成后,将计数器值-1,当计数器值为0表示所有的任务都完成,下面是示例代码:

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

func main() 
	var wg sync.WaitGroup

	urls := [3]string
		"http://www.baidu.com",
		"http://www.qq.com",
		"http://www.sina.com.cn",
	

	for _, url := range urls 
		// 等待数组+1
		wg.Add(1)
		go func(url string) 
			// 当一个任务完成时,将等待数组值-1
			defer wg.Done()
			res, err := http.Get(url)
			fmt.Printf("res: %v, err = %v\\n\\n", res, err)
		(url)
	

	// 等待所有任务完成
	wg.Wait()

	fmt.Println("all work done!")

上面的代码中,wg.Wait()会等待3个goroutine执行完成,然后才接着后面的执行。

以上是关于golang学习笔记6——并发的主要内容,如果未能解决你的问题,请参考以下文章

白话 Golang 协程池

golang学习九:Go并发编程

Golang学习笔记

学习笔记Golang语法学习笔记

golang学习笔记

深入浅出Golang的协程池设计