并发

31 阅读3分钟

一、相关概念

串行

同一时刻,一个任务接着一个任务的执行

并行

同一时刻,执行多个任务

并发

同一时刻内分多个时间片执行多个任务,宏观上像“同一时刻执行多个任务”

image.png

进程

是应用程序运行的抽象,比如启动一个QQ,就是启动一个进程。进程包含两部分

  • 静态部分:程序运行需要的代码和数据
  • 动态部分:程序运行期间的状态

image.png

线程

更轻量级运行的抽象,是进程的一个执行单元,是cpu调度的基本单位。只包含动态部分

“线程由谁来管理”分类:

  • 用户级线程:线程的创建、调度、管理全由应用程序自己通过 “线程库”(比如用户自己写的工具库)完成,操作系统内核完全 “不知道” 这些线程的存在
  • 内核级线程:线程的创建、调度、管理全由操作系统内核负责,内核直接操控这些线程的运行

协程

用户态下的轻量级线程,上下文切换不需要内核参与。go语言的协程叫goroutine

二、实现并发程序

go语言中,使用go关键字就可以开启一个协程。调度器先将goroutine映射到 OS 线程,再由CPU调度,从而实现并发执行,实现高并发程序

image.png

三、协程间的通信

Channel,引用类型,声明:var 变量名 chan 数据类型,初始化make (chan 元素类型 [缓冲大小] ) (缓冲可选)

1. 有无缓冲

没有缓冲,队列大小为0,所以必须要有一个接收者,不然会造成阻塞形成死锁;要不然就是设置缓冲

image.png

image.png

2. 从通道取值

for循环;for range循环

select

在 Go 语言中,select 是用于多路复用通道操作的关键字,它可以同时监听多个通道的发送或接收操作,哪个操作先就绪就执行哪个,若都不就绪则会阻塞(或执行default分支)

func main() {  
    ch := make(chan int) //创建int类型无缓冲通道ch  
    /* go匿名协程 */    go func() {  
       time.Sleep(3 * time.Second) //休眠3s  
       ch <- 1                     //向通道ch发送数据1(无缓冲通道需等待接收方就绪)  
    }()  
    select { //多路复用:同时监听多个通道操作,谁先就绪执行谁  
    case data, ok := <-ch: //尝试从通道ch接收数据,data存接收的值,ok标记通道状态  
       if ok { //ok为true:通道未关闭且接收到数据  
          fmt.Println(data) //打印data  
       } else {  
          fmt.Println("channel closed") //ok为false:通道已关闭且无剩余数据  
       }  
    case <-time.After(2 * time.Second): //创建定时器通道,2秒后自动发送一个时间值(触发超时)  
       fmt.Println("timeout") //打印timeout  
    }  
}

image.png

3. 锁🔒

3.1 互斥锁

同一时间只允许一个协程访问共享资源

package main  
  
import (  
    "fmt"  
    "sync"    "time")  
  
var (  
    mu    sync.Mutex // 互斥锁  
    count int        // 共享变量  
)  
  
func main() {  
    // 启动5个协程并发修改count  
    for i := 0; i < 5; i++ {  
       go func(id int) {  
          mu.Lock()         // 加锁:独占共享资源  
          count++           // 临界区:修改共享变量(必须加锁保护)  
          fmt.Printf("协程%d:count=%d\n", id, count) // 临界区内打印(安全)  
          mu.Unlock()       // 解锁:释放资源  
       }(i)  
    }  
  
    time.Sleep(100 * time.Millisecond) // 等待所有协程执行完  
    fmt.Println("最终count:", count)    // 主协程打印最终结果(安全,无竞争)  
}

image.png

3.2 读写锁

读操作可多个协程同时进行,写操作会独占资源(读与写、写与写互斥)。适用读多写少的场景,提高性能

package main

import (
    "fmt"
    "sync"
    "time"
)

var (
    rw   sync.RWMutex // 定义读写锁
    data = "初始数据"     // 共享数据
)

func main() {
    // 启动5个读协程(并发执行)
    for i := 0; i < 5; i++ {
       go func(readID int) {
          rw.RLock() // 加读锁:允许多个读协程同时加锁
          fmt.Printf("读协程%d:%s\n", readID, data)
          time.Sleep(500 * time.Millisecond) // 模拟读耗时
          rw.RUnlock()                       // 解读锁
       }(i)
    }

    // 启动1个写协程(独占执行)
    go func() {
       time.Sleep(100 * time.Millisecond) // 等读协程先启动
       rw.Lock()                          // 加写锁:阻塞所有读/写操作
       data = "更新后的数据"                    // 写操作(共享资源)
       fmt.Println("写协程:已更新数据")
       rw.Unlock() // 解写锁
    }()

    time.Sleep(2 * time.Second) // 等待所有协程执行完(简化写法)
}

image.png