golang 去并发模式:只让一个线程做关键事情,其他线程等待并通过工作。
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang 去并发模式:只让一个线程做关键事情,其他线程等待并通过工作。相关的知识,希望对你有一定的参考价值。
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 去并发模式:只让一个线程做关键事情,其他线程等待并通过工作。的主要内容,如果未能解决你的问题,请参考以下文章