多线程会遇到的问题 | 豆包MarsCode AI刷题

78 阅读3分钟

多线程会遇到的问题

临界资源的安全问题

临界资源:指并发环境中多个进程、线程、协程共享的资源

在并发编程中对临界资源的处理不当,往往会导致数据不一致的问题。

 //
 func main() {
    // 临界资源:多个协程共享的变量,会导致程序结果位置
    a := 1
 ​
    go func() {
       a = 2
       fmt.Println("goroutine a:", a)
    }()
 ​
    a = 3
    time.Sleep(3 * time.Second)
    fmt.Println("main a:", a)
 }

售票问题

并发本身并不复杂,但是因为有了资源竞争的问题,就使得我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。

如果多个goroutine在访问同一个数据资源的时候,其中一个线程修改了数据,那么这个数值就被修改了,对于其他的goroutine来讲,这个数值可能是不对的

 package main
 ​
 import (
    "fmt"
    "time"
 )
 ​
 // 定义全局变量 票库存为10张
 var ticket int = 10
 ​
 func main() {
    // 单线程不存在问题,多线程资源争抢就出现了问题
    go saleTickets("张三")
    go saleTickets("李四")
    go saleTickets("王五")
    go saleTickets("赵六")
 ​
    time.Sleep(time.Second * 5)
 }
 ​
 // 售票函数
 func saleTickets(name string) {
    for {
       if ticket > 0 {
          time.Sleep(time.Millisecond * 1)
          fmt.Println(name, "剩余票的数量为:", ticket)
          ticket--
       } else {
          fmt.Println("票已售完")
          break
       }
    }
 }

发现结果和预想的不同,多线程加入之后,原先单线程的逻辑出现了问题。

出现了临界资源安全问题。

sync包 - 锁

要想解决临界资源安全的问题,很多编程语言的解决方案都是同步。通过上锁的方式,某一时间段,只能允许一个goroutine来访问这个共享数据,当前goroutine访问完毕, 解锁后,其他的goroutine才 能来访问

我们可以借助于sync包下的锁操作。 synchronization

但是实际上,在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信:锁,而要以通信的方式去共享内存。

 共享内存的方式
 锁:多个线程拿的是同一个钥匙,go语言不建议使用锁机制来解决。不要以共享内存的方式去通信
 ​
 而要以通信的方式去共享内存 go语言更建议我们使用 chan 来解决安全问题。(后面会学)

在Go语言中并不鼓励用锁保护共享状态的方式,在不同的Goroutine中分享信息(以共享内存的方式去通信)。而是鼓励通过channeI将共享状态或共享状态的变化在各个Goroutine之间传递(以通信的方式去共享内存) ,这样同样能像用锁一样保证在同一的时间只有一个Goroutine访问共享状态。

当然,在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多

 package main
 ​
 import (
     "fmt"
     "sync"
     "time"
 )
 ​
 // 定义全局变量 票库存为10张
 var ticket int = 10
 ​
 // 定义一个锁  Mutex 锁头
 var mutex sync.Mutex
 ​
 func main() {
     // 单线程不存在问题,多线程资源争抢就出现了问题
     go saleTickets("张三")
     go saleTickets("李四")
     go saleTickets("王五")
     go saleTickets("赵六")
 ​
     time.Sleep(time.Second * 5)
 }
 ​
 // 售票函数
 func saleTickets(name string) {
     for {
         // 在拿到共享资源之前先上锁
         mutex.Lock()
         if ticket > 0 {
             time.Sleep(time.Millisecond * 1)
             fmt.Println(name, "剩余票的数量为:", ticket)
             ticket--
         } else {
             // 操作完毕后,解锁
             mutex.Unlock()
             fmt.Println("票已售完")
             break
         }
         // 操作完毕后,解锁
         mutex.Unlock()
     }
 }

同步等待组

 package main
 ​
 import (
    "fmt"
    "sync"
    "time"
 )
 ​
 // waitgroup、
 ​
 var wg sync.WaitGroup
 ​
 func main() {
    // wg.Add(2) 判断还有几个线程、计数  num=2
    // wg.Done() 我告知我已经结束了  -1
    wg.Add(2)
 ​
    go test1()
    go test2()
 ​
    fmt.Println("main等待ing")
    wg.Wait() // 等待 wg 归零,才会继续向下执行
    fmt.Println("end")
 ​
    // 理想状态:所有协程执行完毕之后,自动停止。
    //time.Sleep(3 * time.Second)
 ​
 }
 func test1() {
    for i := 0; i < 10; i++ {
       time.Sleep(3 * time.Second)
       fmt.Println("test1--", i)
    }
    wg.Done()
 }
 func test2() {
    defer wg.Done()
    for i := 0; i < 10; i++ {
       fmt.Println("test2--", i)
    }
 }

不要以共享内存的方式去通信,而要以通信的方式去共享内存。