【原创】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. 原子操作在用户态就可以完成,因此性能比互斥锁要高