1 ) 多线程程序在单核上运行,就是并发
2 ) 多线程程序在多核上运行,就是并行
并发:因为是在一个cpu上,比如有10个线程,每个线程执行10毫秒(进行轮询操作),从人的角度看,好像这10个线程都在运行,但是从微观上看,在某一个时间点看,其实只有一个线程在执行,这就是并发。
并行:因为是在多个cpu上(比如有10个cpu),比如有10个线程,每个线程执行10毫秒(各自在不同cpu上执行),从人的角度看,这10个线程都在运行,但是从微观上看,在某一个时间点看,也同时有10个线程在执行,这就是并行。
Go主线程(有程序员直接称为线程/也可以理解成进程): 一个Go线程上,可以起多个协程,你可以 这样理解,协程是轻量级的线程[编译器做优化]。
Go协程的特点
1 ) 有独立的栈空间
2 ) 共享程序堆空间
3 ) 调度由用户控制
4 ) 协程是轻量级的线程
一个示意图
1 ) 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔 1 秒输出 “hello,world”
2 ) 在主线程中也每隔一秒输出"hello,golang", 输出 10 次后,退出程序
3 ) 要求主线程和goroutine同时执行
4 ) 画出主线程和协程执行流程图
package main
import (
"fmt"
"strconv"
"time"
)
// 在主线程(可以理解成进程)中,开启一个goroutine, 该协程每隔1秒输出 "hello,world"
// 在主线程中也每隔一秒输出"hello,golang", 输出10次后,退出程序
// 要求主线程和goroutine同时执行
//编写一个函数,每隔1秒输出 "hello,world"
func test() {
for i := 1; i <= 10; i++ {
fmt.Println("tesst () hello,world " + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
func main() {
go test() // 开启了一个协程
for i := 1; i <= 10; i++ {
fmt.Println(" main() hello,golang" + strconv.Itoa(i))
time.Sleep(time.Second)
}
}
输出的效果说明,main这个主线程和 test 协程同时执行
main() hello,golang1
tesst () hello,world 1
main() hello,golang2
tesst () hello,world 2
main() hello,golang3
tesst () hello,world 3
main() hello,golang4
tesst () hello,world 4
main() hello,golang5
tesst () hello,world 5
main() hello,golang6
tesst () hello,world 6
main() hello,golang7
tesst () hello,world 7
main() hello,golang8
tesst () hello,world 8
main() hello,golang9
tesst () hello,world 9
main() hello,golang10
tesst () hello,world 10
1 ) 主线程是一个物理线程,直接作用在cpu上的。是重量级的,非常耗费cpu资源。
2 ) 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小。
3 ) Golang的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制是一般基于线程的,开启过多的线程,资源耗费大,这里就突显Golang在并发上的优势了
设置GOLANG运行的CPU数
为了充分了利用多cpu的优势,在Golang程序中,设置运行的cpu数目
package main
import (
"runtime"
"fmt"
)
func main() {
cpuNum := runtime.NumCPU()
fmt.Println("cpuNum=", cpuNum)
//可以自己设置使用多个cpu
runtime.GOMAXPROCS(cpuNum - 1)
fmt.Println("ok")
}
使用goroutine 来完成,效率高,但是会出现并发/并行安全问题
package main
import (
"fmt"
"time"
)
// 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。
// 最后显示出来。要求使用goroutine完成
// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map中.
// 2. 我们启动的协程多个,统计的将结果放入到 map中
// 3. map 应该做出一个全局的.
var (
myMap = make(map[int]int, 10)
)
// test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//这里我们将 res 放入到myMap
myMap[n] = res //concurrent map writes?
}
func main() {
// 我们这里开启多个协程完成这个任务[200个]
for i := 1; i <= 200; i++ {
go test(i)
}
//休眠10秒钟【第二个问题 】
time.Sleep(time.Second * 10)
//这里我们输出结果,变量这个结果
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
}
这里就提出了不同goroutine如何通信的问题
package main
import (
"fmt"
_ "time"
"sync"
)
// 需求:现在要计算 1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。
// 最后显示出来。要求使用goroutine完成
// 思路
// 1. 编写一个函数,来计算各个数的阶乘,并放入到 map中.
// 2. 我们启动的协程多个,统计的将结果放入到 map中
// 3. map 应该做出一个全局的.
var (
myMap = make(map[int]int, 10)
//声明一个全局的互斥锁
//lock 是一个全局的互斥锁,
//sync 是包: synchornized 同步
//Mutex : 是互斥
lock sync.Mutex
)
// test 函数就是计算 n!, 让将这个结果放入到 myMap
func test(n int) {
res := 1
for i := 1; i <= n; i++ {
res *= i
}
//这里我们将 res 放入到myMap
//加锁
lock.Lock()
myMap[n] = res //concurrent map writes?
//解锁
lock.Unlock()
}
func main() {
// 我们这里开启多个协程完成这个任务[200个]
for i := 1; i <= 20; i++ {
go test(i)
}
//休眠10秒钟【第二个问题 】
//time.Sleep(time.Second * 5)
//这里我们输出结果,变量这个结果
lock.Lock()
for i, v := range myMap {
fmt.Printf("map[%d]=%d\n", i, v)
}
lock.Unlock()
}
定义声明channel
var 变量名 chan 数据类型
var intChan chan int(intChan用于存放int数据)
var mapChan chan map[int]string(mapChan用于存放map[int]string类型)
var perChan chan Person
var perChan2 chan *Person
说明
package main
import (
"fmt"
)
func main() {
//演示一下管道的使用
//1. 创建一个可以存放3个int类型的管道
var intChan chan int
intChan = make(chan int, 3)
//2. 看看intChan是什么
fmt.Printf("intChan 的值=%v intChan本身的地址=%p\n", intChan, &intChan)
//3. 向管道写入数据
intChan<- 10
num := 211
intChan<- num
intChan<- 50
// //如果从channel取出数据后,可以继续放入
<-intChan
intChan<- 98//注意点, 当我们给管写入数据时,不能超过其容量
//4. 看看管道的长度和cap(容量)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 3, 3
//5. 从管道中读取数据
var num2 int
num2 = <-intChan
fmt.Println("num2=", num2)
fmt.Printf("channel len= %v cap=%v \n", len(intChan), cap(intChan)) // 2, 3
//6. 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取就会报告 deadlock
num3 := <-intChan
num4 := <-intChan
//num5 := <-intChan
fmt.Println("num3=", num3, "num4=", num4/*, "num5=", num5*/)
}
什么时候阻塞
常见错误
**注意:
从已关闭的通道接收数据时将不会发生阻塞
由此可见通道如果不主动close掉,读出通道全部数据后该协程就会阻塞
从已经关闭的通道接收数据或者正在接收数据时,将会接收到通道类型的零值,然后停止阻塞并返回。
1)创建一个intChan,最多可以存放3个int,演示存3数据到intChan,然后再取出这三个int
package main
import (
"fmt"
)
func main() {
var intChan chan int
intChan = make(chan int, 3)
intChan <- 10
intChan <- 20
intChan <- 10
//因为intChan 的容量为3,再存放会报deadlock
//intChan <- 50
num1 := <-intChan
num2 := <-intChan
num3 := <-intChan
//因为intChan 这时已经没有数据了,再取就会报deadlock
//num3 := <- intChan
fmt.Printf("num1=%v num2=%v num3=%v", num1, num2, num3)
}
2)创建一个mapChan,最多可以存放10个map[string]string的key-val,演示写入和读取
package main
import (
"fmt"
)
func main() {
var mapChan chan map[string]string
mapChan = make(chan map[string]string, 10)
m1 := make(map[string]string, 20)
m1["city1"] = "北京"
m1["city2"] = "天津"
m2 := make(map[string]string, 20)
m2["hero1"] = "宋江"
m2["hero2"] = "武松"
mapChan <- m1
mapChan <- m2
m11 := <-mapChan
m22 := <-mapChan
fmt.Println(m11, m22)
}
3)创建一个catChan,最多可以存放10个Cat结构体变量
package main
import (
"fmt"
)
type Cat struct {
Name string
Age byte
}
func main() {
var catChan chan Cat
catChan = make(chan Cat, 10)
cat1 := Cat{Name: "tom", Age: 18}
cat2 := Cat{Name: "tom~", Age: 180}
catChan <- cat1
catChan <- cat2
//取出
cat11 := <-catChan
cat22 := <-catChan
fmt.Println(cat11, cat22)
}
4)创建一个catChan2,最多可以存放10个*Cat变量,演示写入和读取的方法
package main
import (
"fmt"
)
type Cat struct {
Name string
Age byte
}
func main() {
var catChan chan *Cat
catChan = make(chan *Cat, 10)
cat1 := Cat{Name: "tom", Age: 18}
cat2 := Cat{Name: "tom~", Age: 180}
catChan <- &cat1
catChan <- &cat2
//取出
cat11 := <-catChan
cat22 := <-catChan
fmt.Println(cat11, cat22)
}
5)创建一个allChan,最多可以存放10个任意数据类型变量,演示写入和读取的用法
package main
import (
"fmt"
)
type Cat struct {
Name string
Age byte
}
func main() {
var allChan chan interface{}
allChan = make(chan interface{}, 10)
cat1 := Cat{Name: "tom", Age: 18}
cat2 := Cat{Name: "tom~", Age: 180}
allChan <- cat1
allChan <- cat2
allChan <- 10
allChan <- "jack"
//取出
cat11 := <-allChan
cat22 := <-allChan
v1 := <-allChan
v2 := <-allChan
fmt.Println(cat11, cat22, v1, v2)
}
6)看下面代码,会输出什么?
package main
import (
"fmt"
)
type Cat struct {
Name string
Age byte
}
func main() {
var allChan chan interface{}
allChan = make(chan interface{}, 10)
cat1 := Cat{Name: "tom", Age: 18}
allChan <- cat1
//取出
newCat := <-allChan //从管道中取出的cat是什么?
fmt.Printf("newCat=%T,newCat=%v\n", newCat, newCat)
//下面的写法是错误的!编译不通过
//fmt.Printf("newCat.Name=%v",newCat.Name)
//使用类型断言
a := newCat.(Cat)
fmt.Printf("newCat.Name=%v", a.Name)
}
channel可以声明为只读,或者只写性质
package main
import (
"fmt"
)
func main() {
//管道可以声明为只读或者只写
//1. 在默认情况下下,管道是双向
//var chan1 chan int //可读可写
//2 声明为只写
var chan2 chan<- int
chan2 = make(chan int, 3)
chan2<- 20
//num := <-chan2 //error
fmt.Println("chan2=", chan2)
//3. 声明为只读
var chan3 <-chan int
num2 := <-chan3
//chan3<- 30 //err
fmt.Println("num2", num2)
}
channel只读和只写的最佳实践案例
package main
import (
"fmt"
)
// ch chan<- int,这样ch就只能写操作
func send(ch chan<- int, exitChan chan struct{}) {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
var a struct{}
exitChan <- a
}
// ch <-chan int,这样ch就只能读操作
func recv(ch <-chan int, exitChan chan struct{}) {
for {
v, ok := <-ch
if !ok {
break
}
fmt.Println(v)
}
var a struct{}
exitChan <- a
}
func main() {
var ch chan int
ch = make(chan int, 10)
exitChan := make(chan struct{}, 2)
go send(ch, exitChan)
go recv(ch, exitChan)
var total = 0
for _ = range exitChan {
total++
if total == 2 {
break
}
}
fmt.Println("结束。。。")
}
select是go语言当中提供的一个选择语句。select的语法类似switch语句,也属于控制语句。
那为什么select语句我们没有放在和if、switch语句一起?
因为select是配合channel通道使用的。每个 case 必须是一个通信操作,要么是发送要么是接收。
select特性
select只能用于channel操作,每个case都必须是一个channel;
如果有多个case可以允许(channel没有阻塞),则随机选择一条case语句执行;
如果没有case语句可以执行(channel发生阻塞),切没有default语句,则select语句会阻塞;
如果没有case语句可以执行(channel发生阻塞),有default语句,则执行default语句;
一般使用超时语句代替default语句;
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
select {
case num1 := <-ch1:
fmt.Println("ch1中的数据是:", num1)
case num2 := <-ch2:
fmt.Println("ch2中的数据是:", num2)
case <-time.After(3 * time.Second):
fmt.Println("timeout...")
}
}
如果case语句中的channel为nil,则忽略该分支,相当于从select中删除了该分支;
如果select语句在for循环中,一般不使用default语句,因为会引起CPU占用过高问题。
select语法格式
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
例子:
package main
import (
"fmt"
"time"
)
func main() {
//使用select可以解决从管道取数据的阻塞问题
//1.定义一个管道 10个数据int
intChan := make(chan int, 10)
for i := 0; i < 10; i++ {
intChan<- i
}
//2.定义一个管道 5个数据string
stringChan := make(chan string, 5)
for i := 0; i < 5; i++ {
stringChan <- "hello" + fmt.Sprintf("%d", i)
}
//传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
//问题,在实际开发中,可能我们不好确定什么关闭该管道.
//可以使用select 方式可以解决
//label:
for {
select {
//注意: 这里,如果intChan一直没有关闭,不会一直阻塞而deadlock
//,会自动到下一个case匹配
case v := <-intChan :
fmt.Printf("从intChan读取的数据%d\n", v)
time.Sleep(time.Second)
case v := <-stringChan :
fmt.Printf("从stringChan读取的数据%s\n", v)
time.Sleep(time.Second)
default :
fmt.Printf("都取不到了,不玩了, 程序员可以加入逻辑\n")
time.Sleep(time.Second)
return
//break label
}
}
}
9 ) goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题
package main
import (
"fmt"
"time"
)
//函数
func sayHello() {
for i := 0; i < 10; i++ {
time.Sleep(time.Second)
fmt.Println("hello,world")
}
}
//函数
func test() {
//这里我们可以使用defer + recover
defer func() {
//捕获test抛出的panic
if err := recover(); err != nil {
fmt.Println("test() 发生错误", err)
}
}()
//定义了一个map
var myMap map[int]string
myMap[0] = "golang" //error
}
func main() {
go sayHello()
go test()
for i := 0; i < 10; i++ {
fmt.Println("main() ok=", i)
time.Sleep(time.Second)
}
}
使用内置函数close可以关闭channel, 当channel关闭后,就不能再向channel写数据了,但是仍然 可以从该channel读取数据
案例演示:
package main
import (
"fmt"
)
func main() {
intChan := make(chan int, 3)
intChan<- 100
intChan<- 200
close(intChan) // close
//这是不能够再写入数到channel
//intChan<- 300
fmt.Println("okook~")
//当管道关闭后,读取数据是可以的
n1 := <-intChan
fmt.Println("n1=", n1)
}
channel支持for–range的方式进行遍历,被关闭的信道会禁止数据流入, 是只读的。我们仍然可以从关闭的信道中取出数据,但是不能再写入数据了。
请注意两个细节
1 ) 在遍历时,如果channel没有关闭,则会出现deadlock的错误
2 ) 在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
package main
import (
"fmt"
)
func main() {
intChan := make(chan int, 3)
intChan<- 100
intChan<- 200
close(intChan) // close
//这是不能够再写入数到channel
//intChan<- 300
fmt.Println("okook~")
//当管道关闭后,读取数据是可以的
n1 := <-intChan
fmt.Println("n1=", n1)
//遍历管道
intChan2 := make(chan int, 100)
for i := 0; i < 100; i++ {
intChan2<- i * 2 //放入100个数据到管道
}
//遍历管道不能使用普通的 for 循环
// for i := 0; i < len(intChan2); i++ {
// }
//在遍历时,如果channel没有关闭,则会出现deadlock的错误
//在遍历时,如果channel已经关闭,则会正常遍历数据,遍历完后,就会退出遍历
close(intChan2)
for v := range intChan2 {
fmt.Println("v=", v)
}
}
func main() {
ch := make(chan string, 6)
go func() {
for {
v, ok := <-ch
if !ok {
fmt.Println("结束")
return
}
fmt.Println(v)
}
}()
ch <- "煎鱼还没进锅里..."
ch <- "煎鱼进脑子里了!"
close(ch)
time.Sleep(time.Second)
}
在 Go 语言的 channel 中,channel 接受数据有两种方法:
msg := <-ch
msg, ok := <-ch //通道关闭后ok=false
这两种方式对应着不同的 runtime 方法,我们可以利用其第二个参数进行判别,当关闭 channel 时,就根据其返回结果跳出。
在知道通道的一些阻塞情况后,为了防止deadlock ,可以使用更友好的方式从通道中读取数据
if i, ok := <-ch1 ;ok{
...
}
select如果没有case语句可以执行(channel发生阻塞),有default语句,则执行default语句;
通道如果不主动close掉,读出通道全部数据即通道无数据后该协程就会阻塞
从已经关闭的通道接收数据或者正在接收数据时,将会接收到通道类型的零值,然后停止阻塞并返回。
func main() {
val := make(chan interface{})
i := 0
go func() {
for {
select {
case <-val:
return
default:
i++
fmt.Println(i)
}
}
}()
time.Sleep(time.Second)
close(val)
}
可以借助 Go 语言的上下文(context)来做 goroutine 的控制和关闭。
func main() {
ch := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
ch <- struct{}{}
return
default:
fmt.Println("煎鱼还没到锅里...")
}
time.Sleep(500 * time.Millisecond)
}
}(ctx)
go func() {
time.Sleep(3 * time.Second)
cancel()
}()
<-ch
fmt.Println("结束")
}
在 context 中,我们可以借助 ctx.Done
获取一个只读的 channel,类型为结构体。可用于识别当前 channel 是否已经被关闭,其原因可能是到期,也可能是被取消了。
因此 context 对于跨 goroutine 控制有自己的灵活之处,可以调用 context.WithTimeout
来根据时间控制,也可以自己主动地调用 cancel
方法来手动关闭。
WaitGroup在go语言中,用于线程同步,单从字面意思理解,wait等待的意思,group组、团队的意思,WaitGroup就是指等待一组,等待一个系列执行完成后才会继续向下执行。
如图:WatiGroup的定义
google官方示例:
package main
import (
"fmt"
"sync"
"net/http"
)
func main() {
var wg sync.WaitGroup
var urls = []string{
"http://www.golang.org/",
"http://www.google.com/",
"http://www.baiyuxiong.com/",
}
for _, url := range urls {
// Increment the WaitGroup counter.
wg.Add(1)
// Launch a goroutine to fetch the URL.
go func(url string) {
// Decrement the counter when the goroutine completes.
defer wg.Done()
// Fetch the URL.
http.Get(url)
fmt.Println(url);
}(url)
}
// Wait for all HTTP fetches to complete.
wg.Wait()
fmt.Println("over");
}
代码执行结果为:
http://www.baiyuxiong.com/
http://www.google.com/
http://www.golang.org/
over
从执行结果可看出:
1、取三个网址信息的时候,结果显示顺序与for循环的顺序没有必然关系。
2、三个goroutine全部执行完成后,wg.Wait()才停止等待,继续执行并打印出over字符。
Go程序可以使用通道进行多个goroutine间的数据交换,但是这仅仅是数据同步中的一种方法。Go语言与其他语言如C、Java一样,也提供了同步机制,在某些轻量级的场合,原子访问(sync/atomic包),互斥锁(sync.Mutex)以及等待组(sync.WaitGroup)能最大程度满足需求。
贴士:利用通道优雅的实现了并发通信,但是其内部的实现依然使用了各种锁,因此优雅代码的代价是性能的损失。
互斥锁是传统并发程序进行共享资源访问控制的主要方法。Go中由结构体sync.Mutex表示互斥锁,保证同时只有一个 goroutine 可以访问共享资源。
示例一,普通数据加锁:
package main
import (
"fmt"
"sync"
//"sync"
"time"
)
func main() {
var mutex sync.Mutex
num := 0
// 开启10个协程,每个协程都让共享数据 num + 1
for i := 0; i < 1000; i++ {
go func() {
mutex.Lock() // 加锁,阻塞其他协程获取锁
num += 1
mutex.Unlock() // 解锁
}()
}
// 大致模拟协程结束 等待5秒
time.Sleep(time.Second * 5)
// 输出1000,如果没有加锁,则输出的数据很大可能不是1000
fmt.Println("num = ", num)
}
一旦发生加锁,如果另外一个 goroutine 尝试继续加锁时将会发生阻塞,直到这个 goroutine 被解锁,所以在使用互斥锁时应该注意一些常见情况:
对同一个互斥量的锁定和解锁应该成对出现,对一个已经锁定的互斥量进行重复锁定,会造成goroutine阻塞,直到解锁
对未加锁的互斥锁解锁,会引发运行时崩溃,1.8版本之前可以使用defer可以有效避免该情况,但是重复解锁容易引起goroutine永久阻塞,1.8版本之后无法利用defer+recover恢复
示例二:对象加锁
// 账户对象,对象内置了金额与锁,对象拥有读取金额、添加金额方法
package main
import (
"fmt"
"sync"
"time"
)
type Account struct {
money int
lock *sync.Mutex
}
func (a *Account)Query() {
fmt.Println("当前金额为:", a.money)
}
func (a *Account)Add(num int) {
a.lock.Lock()
a.money += num
a.lock.Unlock()
}
func main() {
a := &Account{
0,
&sync.Mutex{},
}
for i := 0; i < 100; i++ {
go func(num int){
a.Add(num)
}(10)
}
time.Sleep(time.Second * 2)
a.Query() // 不加锁会打印不到1000的数值,加锁后打印 1000
}
在开发场景中,经常遇到多处并发读取,一次并发写入的情况,Go为了方便这些操作,在互斥锁基础上,提供了读写锁操作。
读写锁即针对读写操作的互斥锁,简单来说,就是将数据设定为 写模式(只写)或者读模式(只读)。使用读写锁可以分别针对读操作和写操作进行锁定和解锁操作。
读写锁的访问控制规则与互斥锁有所不同:
写操作与读操作之间也是互斥的
读写锁控制下的多个写操作之间是互斥的,即一路写
多个读操作之间不存在互斥关系,即多路读
在Go中,读写锁由结构体sync.RWMutex表示,包含两对方法:
// 设定为写模式:与互斥锁使用方式一致,一路只写
func (*RWMutex) Lock() // 锁定写
func (*RWMutex) Unlock() // 解锁写
// 设定为读模式:对读执行加锁解锁,即多路只读
func (*RWMutex) RLock()
func (*RWMutex) RUnlock()
注意:
Mutex和RWMutex都不关联goroutine,但RWMutex显然更适用于读多写少的场景。仅针对读的性能来说,RWMutex要高于Mutex,因为rwmutex的多个读可以并存。
所有被读锁定的goroutine会在写解锁时唤醒
读解锁只会在没有任何读锁定时,唤醒一个要进行写锁定而被阻塞的goroutine
对未被锁定的读写锁进行写解锁或读解锁,都会引发运行时崩溃
对同一个读写锁来说,读锁定可以有多个,所以需要进行等量的读解锁,才能让某一个写锁获得机会,否则该goroutine一直处于阻塞,但是sync.RWMutext没有提供获取读锁数量方法,这里需要使用defer避免,如下案例所示。
import (
"fmt"
"sync"
"time"
)
func main() {
var rwm sync.RWMutex
for i := 0; i < 3; i++ {
go func(i int) {
fmt.Println("Try Lock reading i:", i)
rwm.RLock()
fmt.Println("Ready Lock reading i:", i)
time.Sleep(time.Second * 2)
fmt.Println("Try Unlock reading i: ", i)
rwm.RUnlock()
fmt.Println("Ready Unlock reading i:", i)
}(i)
}
time.Sleep(time.Millisecond * 100)
fmt.Println("Try Lock writing ")
rwm.Lock()
fmt.Println("Ready Locked writing ")
}
上述案例中,只有循环结束,才会执行写锁,所以输出如下:
...
Ready Locked writing // 总在最后一行
sync.RWMutex类型还有一个指针方法RLocker:
func (rw *RWMutex) RLocker() Locker
返回值Locker是实现了接口sync.Lokcer的值,该接口同样被 sync.Mutex和sync.RWMutex实现,包含方法:Lock和Unlock。
当调用读写锁的RLocker方法后,获得的结果是读写锁本身,该结果可以调用Lock和Unlock方法,和RLock,RUnlock使用一致。
2.4 最后的说明
读写锁的内部其实使用了互斥锁来实现,他们都使用了同步机制:信号量。
3、死锁
常见会出现死锁的场景:
两个协程互相要求对方先操作,如:AB相互要求对方先发红包,然后自己再发
读写双方相互要求对方先执行,自己后执行
模拟死锁:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var rwm sync.RWMutex
ch := make(chan int)
go func() {
rwm.RLock() // 加读锁
x := <- ch // 如果不写入,则无法读取
fmt.Println("读取到的x:", x)
rwm.RUnlock()
}()
go func() {
rwm.Lock() // 加入写锁
ch <- 10 // 管道无缓存,没人读走,则无法写入
fmt.Println("写入:", 10)
rwm.Unlock()
}()
time.Sleep(time.Second * 5)
}