并发与并行

进程、线程、并发、并行

  • 进程:系统进行资源分配和调度的基本单位,是程序在执行过程中分配和管理资源的基本单位。一个进程至少有5种基本态:初始态、执行态、等待态、就绪态以及终止态

  • 线程:程序执行的最小单元,是进程的一个执行实例。一个进程可以创建多个线程,同一个进程中多个线程可以并发执行

  • 并发:多个线程同时竞争一个位置,竞争到的才可以执行,同一个时间段只有一个线程在执行

    • 并发特点:多个任务作用在一个CPU上;同一时间点只能有一个任务执行;同一时间段内执行多个任务
  • 并行:多个线程可以同时执行,每一个时间段,可以有多个线程同时执行

    • 并行特点:多个任务作用在多个CPU上;同一时刻执行多个任务
  • 通俗的讲,多线程程序在单核CPU上面运行就是并发,多线程程序在多核CUP上运行就是并行,如果线程数大于CPU核数,则多线程程序在多个CPU上面运行既有并行又有并发

Goroutine

go
go 函数名(参数列表)
例
go f(x,y,z)
则开启一个新的goroutine(线程)
f(x,y,z)

Go 允许使用go语句开启一个新的运行期线程,即goroutine,以一个不同的,新建的goroutine来执行一个函数
同一个程序中的所有goroutine共享同一个地址空间


package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}
# 执行以上代码,你会看到输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行
输出:
world
hello
hello
world
world
hello
hello
world
world
hello
sync.WaitGroup
// 定义一个协程计数器
var wg sync.WaitGroup
// 开启协程,协程计数器加1
wg.Add(1)
go test2()
// 协程计数器减1
wg.Done()
// 定义一个协程计数器
var wg sync.WaitGroup

func test()  {
    // 这是主进程执行的
    for i := 0; i < 1000; i++ {
        fmt.Println("test1 你好golang", i)
        //time.Sleep(time.Millisecond * 100)
    }
    // 协程计数器减1
    wg.Done()
}

func test2()  {
    // 这是主进程执行的
    for i := 0; i < 1000; i++ {
        fmt.Println("test2 你好golang", i)
        //time.Sleep(time.Millisecond * 100)
    }
    // 协程计数器减1
    wg.Done()
}

func main() {
    // 通过go关键字,就可以直接开启一个协程
    wg.Add(1)
    go test()

    // 协程计数器加1
    wg.Add(1)
    go test2()

    // 这是主进程执行的
    for i := 0; i < 1000; i++ {
        fmt.Println("main 你好golang", i)
        //time.Sleep(time.Millisecond * 100)
    }
    // 等待所有的协程执行完毕
    wg.Wait()
    fmt.Println("主线程退出")
}
GOMAXPROCSruntime.GOMAXPROCS()
func main() {
    // 获取cpu个数
    npmCpu := runtime.NumCPU()
    fmt.Println("cup的个数:", npmCpu)
    // 设置允许使用的CPU数量
    runtime.GOMAXPROCS(runtime.NumCPU() - 1)
}

Channel管道

channelchannelchannelchannel
// 声明一个传递整型的管道
var ch1 chan int
// 声明一个传递布尔类型的管道
var ch2 chan bool
// 声明一个传递int切片的管道
var ch3 chan []int

//初始化channel,声明管道后,需要使用make函数初始化之后才能使用
// 创建一个能存储10个int类型的数据管道
ch1 = make(chan int, 10)
// 创建一个能存储4个bool类型的数据管道
ch2 = make(chan bool, 4)
// 创建一个能存储3个[]int切片类型的管道
ch3 = make(chan []int, 3)

channel操作

管道有发送,接收和关闭的三个功能

<-
// 把10发送到ch中
ch <- 10
//取值操作,从ch中接收值,并赋给x
x := <- ch
//关闭channel
close(ch)
# 声明通道,使用chan关键字,通道在使用前必须先创建
ch := make(chan int)


# 注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // 把 sum 发送到通道 c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // 从通道 c 中接收

    fmt.Println(x, y, x+y)
}

//输出:-5,17,12

channel缓冲区

make
语法格式:
ch := make(chan int,100)

# 带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据

# 注意:如果通道不设置缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞

package main

import "fmt"

func main() {
    // 这里我们定义了一个可以存储整数类型的带缓冲通道
    // 缓冲区大小为2
    ch := make(chan int, 2)

    // 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
    // 而不用立刻需要去同步读取数据
    ch <- 1
    ch <- 2

    // 获取这两个数据
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}

遍历channel并关闭通道

Go通过range关键字来实现遍历读取到的数据,类似于与数组或切片

v,ok := <- ch

如果通道接收不到数据后,ok就位false,这时通道就可以使用close()函数来关闭

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
            c <- x
            x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    /* 
      range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
      数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
      之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
      会结束,从而在接收第 11 个数据的时候就阻塞了。
    */
    for i := range c {
        fmt.Println(i)
    }
}

单向管道

<-
// 定义一种可读可写的管道
var ch = make(chan int, 2)
ch <- 10
<- ch

// 管道声明为只写管道,只能够写入,不能读
var ch2 = make(chan<- int, 2)
ch2 <- 10

// 声明一个只读管道
var ch3 = make(<-chan int, 2)
<- ch3

并发安全和锁

互斥锁

互斥锁是传统并发编程中对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法,Lock和Unlock。Lock锁定当前的共享资源,Unlock 进行解锁

// 定义一个锁
var mutex sync.Mutex
// 加锁
mutex.Lock()
// 解锁
mutex.Unlock()

读写互斥锁

互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。这样在资源同步、避免竞争的同时也降低了程序的并发性能,程序由原来的并行执行变成串行执行

  • 当只进行读操作时,是不存在资源竞争的问题,因此数据不变的,不管如何读取,多少协程同时读取,都是可行的
  • 因此只有当写操作时,才会出现需要加锁的情况
  • 故而,真正的互斥应该是读取和写入、写入和写入之间的,读和读之间时不必要的
  • 因此衍生出一种锁,读写锁
  • 读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作
  • GO中的读写锁由结构体类型sync.RWMutex表示。此类型的方法集合中包含两对方法:
func (rw *RWMutex) Lock()
//写锁,写锁权限高于读锁,有写锁时优先进行写锁定
func (rw *RWMutex) Unlock()
//写锁解锁,如果没有进行写锁定,就会引起一个运行时错误
 
func (rw *RWMutex) RLock() 
//读锁,当有写锁时,无法加载读锁,当只有读锁或者没有锁时,可以加载读锁,读锁可以加载多个,所以适用于“读多写少”的场景
func (rw *RWMutex) RUnlock()
//读锁解锁,RUnlock撤销单次RLock调用,他对于其他同时的读取器则没有效果。若RW并没有为读取而锁定,调用RUnlock就会引发一个运行时错误