【原创】go语言学习(二十一)Select和线程安全
目录
- select语义介绍和使用
- 线程安全介绍
- 互斥锁介绍和实战
- 读写锁介绍和实战
- 原子操作介绍
select语义介绍和使用
1、多channel场景
A. 多个channel同时需要读取或写入,怎么办?
B. 串行操作?
package main
import (
"fmt"
"time"
)
func server1(ch chan string) {
time.Sleep(6 * time.Second)
ch <- "from server1"
}
func server2(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
s1 := <-output1
fmt.Println(s1)
s2 := <-output2
fmt.Println(s2)
}
2、select登场
A. 同时监听一个或多个channel,直到其中一个channel ready
B. 如果其中多个channel同时ready,随机选择一个进行操作。
C. 语法和switch case有点类似,代码可读性更好。
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
package main
import (
"fmt"
"time"
)
func server1(ch chan string) {
time.Sleep(6 * time.Second)
ch <- "from server1"
}
func server2(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}
3、default分支,当case分支的channel都没有ready的话,执行default
A. 用来判断channel是否满了
B. 用来判断channel是否是空的
package make
import (
"fmt"
"time"
)
// select 管道参数并行
func server1(ch chan string) {
time.Sleep(time.Second * 6)
ch <- "response from server1"
}
func server2(ch chan string){
time.Sleep(time.Second * 3)
ch <- "response from server2"
}
func main(){
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
/*
s1 := <-output1
fmt.Println("s1:", s1)
s2 := <-output2
fmt.Println("s2:", s2)
*/
// 管道同时ready,select随机执行
// time.Sleep(time.Second)
select{
case s1 := <-output1:
fmt.Println("s1:", s1)
case s2 := <-output2:
fmt.Println("s2:", s2)
default:
fmt.Println("run default")
}
}
4、select case分支随机策略验证
package main
import (
"fmt"
"time"
)
func server1(ch chan string) {
ch <- "from server1"
}
func server2(ch chan string) {
ch <- "from server2"
}
func main() {
output1 := make(chan string)
output2 := make(chan string)
go server1(output1)
go server2(output2)
time.Sleep(1 * time.Second)
select {
case s1 := <-output1:
fmt.Println(s1)
case s2 := <-output2:
fmt.Println(s2)
}
}
5、empty select
package main
func main() {
// 代码阻塞
// select{}
select{}
}
线程安全介绍
1、现实例子
A. 多个goroutine同时操作一个资源,这个资源又叫临界区
B. 现实生活中的十字路口,通过红路灯实现线程安全
C. 火车上的厕所,通过互斥锁来实现线程安全
2、实际例子, x = x +1
A. 先从内存中取出x的值
B. CPU进行计算,x+1
C. 然后把x+1的结果存储在内存中

互斥锁介绍和实战
1、互斥锁介绍
A. 同时有且只有一个线程进入临界区,其他的线程则在等待锁
B. 当互斥锁释放之后,等待锁的线程才可以获取锁进入临界区
C. 多个线程同时等待同一个锁,唤醒的策略是随机的
package main
import (
"fmt"
"sync"
)
//有问题的代码! !
var x = 0
func increment(wg *sync.WaitGroup) {
x = x + 1
wg.Done()
}
func main() {
var w sync.WaitGroup
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w)
}
w.Wait()
fmt.Println("final value of x", x)
}
package main
import (
"fmt"
"sync"
)
//使用互斥锁
var x = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
m.Lock()
x = x + 1
m.Unlock()
wg.Done()
}
func main() {
var w sync.WaitGroup
var m sync.Mutex
for i := 0; i < 1000; i++ {
w.Add(1)
go increment(&w, &m)
}
w.Wait()
fmt.Println("final value of x", x)
}
读写锁介绍和实战
1、读写锁使用场景
A. 读多写少的场景
B. 分为两种角色,读锁和写锁
C. 当一个goroutine获取写锁之后,其他的goroutine获取写锁或读锁都会等待读写锁介绍
D. 当一个goroutine获取读锁之后,其他的goroutine获取写锁都会等待, 但其他goroutine获取读锁时,都会继续获得锁.
package main
import (
"fmt"
"sync"
"time"
)
// 读写锁
var rwlock sync.RWMutex
var x int
var wg sync.WaitGroup
func write(){
fmt.Println("wait for rlock")
//获得写锁
rwlock.Lock()
fmt.Println("write lock")
x = x + 1
fmt.Println(10 * time.Second)
fmt.Println("write unlock")
rwlock.Unlock()
wg.Done()
}
func read(i int){
//获取一个读锁
rwlock.RLock()
fmt.Printf("goroutine:%d x=%d", i, x)
// time.Sleep(time.Second)
rwlock.RUnlock()
wg.Done()
}
func main(){
for i := 0 ; i < 10; i++{
wg.Add(1)
go read(i)
}
wg.Add(1)
go write()
wg.Wait()
}
2、读写锁和互斥锁性能比较
package main
import (
"fmt"
"sync"
"time"
)
// 读写锁 比较 互斥锁
// 读写锁在读多写少的情况下比互斥锁效率高15倍
var rwlock sync.RWMutex
var x int
var wg sync.WaitGroup
var mutex sync.Mutex
func write(){
for i := 0; i <1000;i++{
//获得写锁
// rwlock.Lock()
mutex.Lock()
x = x + 1
time.Sleep(10 * time.Microsecond)
// rwlock.Unlock()
mutex.Unlock()
wg.Done()
}
}
func read(i int){
for i := 0; i <10000;i++ {
//获取一个读锁
// rwlock.RLock()
mutex.Lock()
fmt.Printf("goroutine:%d x=%d", i, x)
// time.Sleep(time.Second)
time.Sleep(10 * time.Microsecond)
// rwlock.RUnlock()
mutex.Unlock()
}
wg.Done()
}
func main() {
start := time.Now().UnixNano()
wg.Add(1)
go write()
for i := 0; i < 100; i++ {
wg.Add(1)
go read(i)
}
wg.Wait()
end := time.Now().UnixNano()
cost := (end - start) / 1000 / 1000
fmt.Println("cost:", cost, "ms")
}
原子操作介绍
1、原子操作
A. 加锁代价比较耗时,需要上下文切换
B. 针对基本数据类型,可以使用原子操作保证线程安全
C. 原子操作在用户态就可以完成,因此性能比互斥锁要高


浙公网安备 33010602011771号