【原创】go语言学习(二十一)Select和线程安全

时间:2024-01-25 19:56:10

目录

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