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

  • 相关阅读:
    12.22冲刺总结
    Android远程服务
    短信电话监听
    Android本地服务
    意图
    多线程下载
    异步HTTP请求
    提交数据到服务器
    通过HTTP访问网络资源
    观察者
  • 原文地址:https://www.cnblogs.com/wangshuyang/p/11820488.html
Copyright © 2020-2023  润新知