golang并发编程

1 协程(Goroutine)

  • 定义:协程(goroutine)是 Go 语言中的一种轻量级线程,可以在单个线程中同时执行多个任务。

  • 使用方法:在调用函数时go function()

  • 在函数中使用协程时,需要注意以下几点:

    1. 协程的执行是异步的,因此需要使用通道等方式进行同步。

    2. 协程共享内存空间,因此需要使用互斥锁等方式进行同步,避免竞态条件(race condition)的出现。

    3. 协程的数量应该根据系统的CPU核心数量和任务的性质等因素进行合理的调度,避免过多的协程导致系统资源的浪费和竞争等问题。

示例1:

package main

import "fmt"

func test()  {
	fmt.Println("hello, world")
}

func main() {
	go test()
}

WaitGroup

  • 定义:WaitGroup是Go语言标准库中的一个结构体,它提供了一种简单的机制,用于同步多个协程的执行。适用于需要并发执行多个任务并等待它们全部完成后才能继续执行后续操作的场景。

  • 首先主协程创建WaitGroup实例,然后在每个协程的开始处,调用Add(1)方法,表示需要等待一个任务执行完成,然后协程在任务执行完成之后,调用Done方法,表示任务已经执行完成了。

    主协程中,需要调用Wait()方法,等待所有协程完成任务,示例如下:

    示例1:

    func main(){
        //首先主协程创建WaitGroup实例
        var wg sync.WaitGroup
        // 开始时调用Add方法表示有个任务开始执行
        wg.Add(1)
        go func() {
            // 开始执行...
            //完成之后,调用Done方法
            wg.Done()
        }()
        // 调用Wait()方法,等待所有协程完成任务
        wg.Wait()
        // 执行后续逻辑
    }
    

协程安全

  • 定义:协程安全是指在并发编程中,使用协程时不会出现数据竞争等问题。Go语言中的协程是通过goroutine实现的,而goroutine是轻量级的线程,因此协程之间的调度和切换都是由操作系统自动完成的。

  • 非常经典的例子,两个协程函数,分别对同一个全局变量进行操作,按照我们预期的结果,应该是200万,但是多运行几次,会发现结果各不相同。

    这就是协程安全问题

示例1:

package main

import (
    "fmt"
    "sync"
)

var w = sync.WaitGroup{}
var num = 0

func AddNum() {
    for i := 0; i < 1000000; i++ {
        num++
    }
    w.Done()
}

func main() {
    w.Add(2)
    go AddNum()
    go AddNum()
    w.Wait()
    fmt.Println(num)

}

  • 这种情况我们可以通过加锁进行解决,go语言中给我们通过了这个方法

  • 互斥锁(Mutex):Go语言提供了互斥锁机制来保护共享资源的访问。互斥锁可以通过Lock()和Unlock()方法实现资源的互斥访问,从而避免多个协程同时访问同一个资源的问题。

示例2:

package main

import (
    "fmt"
    "sync"
)

var lock = sync.Mutex{}
var w = sync.WaitGroup{}
var num = 0

func AddNum() {
    // 上锁之后其他协程就进不来
    lock.Lock()  // 上锁
    for i := 0; i < 1000000; i++ {
        num++
    }
    // 解锁之后,其他协程进入
    lock.Unlock() // 解锁
    w.Done() 
}

func main() {
    w.Add(2)
    go AddNum()
    go AddNum()
    w.Wait()

    fmt.Println(num)
}

获取协程返回值

  • 使用全局变量接收

示例1:

package main

import (
	"fmt"
	"math/rand"
	"strconv"
	"time"
)

var code string
var wg sync.WaitGroup

func main() {
    wg.Add(1)
	// 协程调用生成发送验证码函数,让主线程继续运行
	go sendCode()
    wg.Wait()
	// 用户输入
	userCode := userInput()
	// 判断验证码是否正确
	isRightCode(userCode)
}

// 用户输入
func userInput() string {
	fmt.Print("请输入验证码:")
	var userCode string
	for {
		_, err := fmt.Scanln(&userCode)
		if err != nil {
			fmt.Println("输入错误")
		} else {
			break
		}
	}
	return userCode
}

// 发送验证码
func sendCode() {
	for i := 3; i >= 1; i-- {
		fmt.Println("还剩", i, "秒!")
		time.Sleep(time.Second)
	}
	rand.Seed(time.Now().UnixNano())
	for i := 0; i < 6; i++ {
		r := rand.Intn(10)
		code += strconv.Itoa(r)
	}
	fmt.Println("验证码已发送")
	fmt.Println("验证码:", code)
    wg.Done()
}

// 判断验证码是否正确
func isRightCode(userCode string) {
	if userCode == code {
		fmt.Println("验证码正确!")
	} else {
		fmt.Println("验证码错误!")
	}
}

2 信道(channel)

  • 定义:Channel 是 Go 语言中的一个重要概念,它是一个类似于管道的数据结构,用于在不同的 goroutine 之间传递数据。Channel 可以看作是一种同步机制,它可以帮助我们实现 goroutine 之间的通信和数据交换。
  • 存入:channel <- value
    取出:value, ok <- channel
    丢弃:<- channel
    • 先进先出,自动阻塞
    • 数据需要保持流动,否则会阻死报错

示例1:

package main

import (
 "fmt"
)

func main() {
 // 创建一个带缓冲区的字符串类型的通道,缓冲区大小为3
 ch := make(chan string, 3)
 
 // 向通道中发送数据
 ch <- "123"
 ch <- "456"
 ch <- "789"

 // 从通道中接收数据并打印
 s := <-ch
 fmt.Println(s)  // 123
 fmt.Println(<-ch)  // 456

 // 从通道中接收数据并检查是否还有数据可用,然后打印结果和ok的值
 ss, ok := <-ch
 fmt.Println(ss, ok)  // 789 true

 // 关闭通道,不再接收数据
 close(ch)
}

搭配协程

示例2:

package main

import "fmt"

func pushNum(c chan int) {
	for i := 0; i < 100; i++ {
		c <- i
	}
	close(c) // 写完必须要关闭,不然会死锁
}

func main() {
	c1 := make(chan int, 2) // 2表示缓冲区大小
	go pushNum(c1)

	for value := range c1 {
		fmt.Println(value)
	}
}

  • 多个协程函数,close就不能写在协程函数里了

示例3:

package main

import (
	"fmt"
	"sync"
)

var ch chan int = make(chan int, 10)
var wg = sync.WaitGroup{}

func pushNum() {
	for i := 0; i < 5; i++ {
		ch <- i
	}
	wg.Done()
}

func main() {
	wg.Add(2)
	go pushNum()
	go pushNum()
	wg.Wait()
	close(ch)
	for {
		res, ok := <-ch
		if !ok {
			break
		}
		fmt.Println(res)
	}
}

close

  • 使用close之后就不能在继续写入了,但是还可以继续从缓冲区读取

    1. close之后,读取的chan是数据类型的默认值

    2. close之后,不能再往chan里面写入数据

    3. for range之前必须要close

  • 可读可写

    • 将一个读写通道 ch 转换为两个单向通道 readChwriteCh

示例4:

package main

import "fmt"

func main() {
    var ch chan int = make(chan int, 2)
    // 可读chan
    var readCh <-chan int = ch
    // 可写chan
    var writeCh chan<- int = ch

    writeCh <- 1
    writeCh <- 2

    fmt.Println(<-readCh)
    fmt.Println(<-readCh)

}

select...case

  • 适用于无法确认合适关闭信道的情况
  • 通常结合for循环使用
  • select ... case会阻塞到某个分支可以继续执行时执行该分支,当没有可执行的分支是执行default分支

示例5:

package main

import "fmt"

func main() {
    var ch1 chan int = make(chan int, 2)
    var ch2 chan int = make(chan int, 2)
    var ch3 chan int = make(chan int, 2)
    ch1 <- 1
    ch2 <- 2
    ch3 <- 3

    select {
        // 监听多个chan的情况,是随机执行
        case v := <-ch1:
        fmt.Println(v)
        case v := <-ch2:
        fmt.Println(v)
        case v := <-ch3:
        fmt.Println(v)
        default:
        fmt.Println("没有数据")
    }
}

posted @ 2023-08-02 12:12  修凡  阅读(28)  评论(0编辑  收藏  举报