使用channel控制并发数

 

不带超时的写法

package concurrent_test

import (
    "fmt"
    "math/rand"
    "sync"
    "testing"
    "time"
)

func init() {
    rand.Seed(time.Now().UnixNano())
}

// 使用channel控制并发数

var lst = []string{
    "whw1", "whw2", "whw3", "whw4", "whw5", "whw6", "whw7", "whw8", "whw9",
    "whw10", "whw11", "whw12", "whw13", "whw14", "whw15", "whw16", "whw17", "whw18", "whw19",
}

func TestChanControlGos(t *testing.T) {

    var wg sync.WaitGroup
    limitChan := make(chan struct{}, 3) // 最多同时有3个协裎同时工作

    for _, item := range lst {
        wg.Add(1)
        limitChan <- struct{}{}
        // 注意: 如果要用lst中的元素,一定要定义在func下并当作参数传入!
        go func(s string) {
            defer wg.Done()
            consumeStr(s, limitChan)
        }(item)
    }

    wg.Wait()
    fmt.Println("主协裎结束......")

}

func consumeStr(name string, limitCh chan struct{}) {
    defer func() {
        <-limitCh
    }()
    time.Sleep(time.Duration(rand.Intn(2)) * time.Second) // 模拟延迟
    fmt.Println("name: ", name)
}

带超时机制的写法1 -- 复杂一些

但是上面的代码有问题:如果缓冲区设置的很小,而某几个协裎处理时长又很长,会导致整个程序夯住,这个时候我们需要为每个协裎处理加一个超时的机制。

其实问题就变成了:查看子协裎是否超时,如果超时的话直接停止该子协裎(实际上也可以进行重试,这里就不展开了),避免这个超时的子协裎占用太多的资源:

package concurrent_test

import (
    "context"
    "fmt"
    "math/rand"
    "sync"
    "testing"
    "time"
)

func init() {
    rand.Seed(time.Now().UnixNano())
}

var lst3 = []string{
    "whw1", "whw2", "whw3", "whw4", "whw5", "whw6", "whw7", "whw8", "whw9", "whw10",
}

func consumeName(ctx context.Context, wg *sync.WaitGroup, name string, limitCh chan struct{}, timeLimitCh chan bool) {
    // 没有超时,正常处理完成
    defer func() {
        <-limitCh
        wg.Done()
        <-timeLimitCh
        timeLimitCh <- false // 如果没有超时,需要往channel中放入false,这样外面就不会重复“释放了”
        fmt.Printf("{%s} 处理完成并释放资源! \n", name)
    }()

    // 模拟程序的延迟
    randInt := rand.Intn(5)
    fmt.Printf("{%s} 的处理时间是: %d \n", name, randInt)
    time.Sleep(time.Second * time.Duration(randInt))
}

func TestChanCtlGos(t *testing.T) {

    var wg sync.WaitGroup
    // NOTE 协裎并发限制数
    limitChan := make(chan struct{}, 2)

    for _, item := range lst3 {
        wg.Add(1)
        // 开协裎前往带缓冲的channel中放数据
        // NOTE 如果channel满了会夯住,直到子协裎从channel中踢出数据
        limitChan <- struct{}{}
        go func(name string) {
            // 判断下面的 consumeName 有没有“超时” —— 如果超时了释放limitChan与wg.Done()需要写在select里面,如果没有超时就把他们写在函数里面
            // 注意:释放limitChan与wg.Done() 只能在consumeName函数 “超时”与“不超时”时使用一次!
            // 每个协裎里面再加一个标识函数是否超时的channel: true表示超时,false表示不超时 ———— Go使用channel进行协裎之间的通信!
            timeLimitedChan := make(chan bool, 1) //  需要设置缓冲区,不设置的话程序会夯住
            timeLimitedChan <- true               // 默认超时

            // 在每个协裎中开一个1秒超时的ctx,也就是说每个协裎有1秒的处理超时时间
            ctxTimeOut, cancel := context.WithTimeout(context.Background(), 1*time.Second)
            defer cancel()

            go consumeName(ctxTimeOut, &wg, name, limitChan, timeLimitedChan)

            select {
            // NOTE 实际中根据自己的业务情况来选择Done的时机
            // 到达ctx限制的超时时间后,需要配合Done方法,此时该协裎会终止!我们需要做一些业务的“释放处理”
            // 实际上一定会到ctx.Done的,此时不能盲目释放资源,应该在 consumeName 函数真正超时的时候再去释放
            case <-ctxTimeOut.Done():
                // consumeName 函数真正超时的标志
                bl := <-timeLimitedChan
                if bl {
                    fmt.Printf("%s 超时了! \n", name)
                    <-limitChan // 子协裎被终止了,此时应该在超时后手动从limitChan中释放数据
                    wg.Done()   // 子协裎终止了,手动wg.Done(),否则程序会夯住
                }
            }
        }(item)
    }

    wg.Wait()
    fmt.Println("程序结束......")

}

带超时机制的写法2 —— 使用taskDoneChan&加一条select的case语句进行优化

package concurrent_test

import (
    "context"
    "fmt"
    "math/rand"
    "sync"
    "testing"
    "time"
)

func init() {
    rand.Seed(time.Now().UnixNano())
}

var lst4 = []string{
    "whw1", "whw2", "whw3", "whw4", "whw5", "whw6", "whw7", "whw8", "whw9", "whw10",
}

func consumeName2(ctx context.Context, wg *sync.WaitGroup, name string, limitCh chan struct{}, taskDoneChan chan bool) {
    // 没有超时,正常处理完成
    defer func() {
        // 往taskDoneChan中放入任务已经完成的标志
        taskDoneChan <- true
        fmt.Printf("{%s} 处理完成并释放资源! \n", name)
    }()

    // 模拟程序的延迟
    randInt := rand.Intn(5)
    fmt.Printf("{%s} 的处理时间是: %d \n", name, randInt)
    time.Sleep(time.Second * time.Duration(randInt))
}

func TestChanCtlGos2(t *testing.T) {

    var wg sync.WaitGroup
    // NOTE 协裎并发限制数
    limitChan := make(chan struct{}, 2)

    for _, item := range lst4 {
        wg.Add(1)
        // 开协裎前往带缓冲的channel中放数据
        // NOTE 如果channel满了会夯住,直到子协裎从channel中踢出数据
        limitChan <- struct{}{}
        go func(name string) {

            // 标识任务是否完成的channel
            taskDoneChan := make(chan bool)

            // 在每个协裎中开一个1秒超时的ctx,也就是说每个协裎有1秒的处理超时时间
            ctxTimeOut, cancel := context.WithTimeout(context.Background(), 1*time.Second)
            defer cancel()

            go consumeName2(ctxTimeOut, &wg, name, limitChan, taskDoneChan)

            select {
            // 如果consume超时了就会走这里,需要释放信号量
            case <-ctxTimeOut.Done():
                fmt.Printf("%s 超时了! \n", name)
                <-limitChan
                wg.Done()
            // consume没有超时的情况走这里,同样需要释放信号量
            case taskDone := <-taskDoneChan:
                if taskDone {
                    <-limitChan
                    wg.Done()
                    fmt.Printf("%s 正常完成! \n", name)
                }
            }
        }(item)
    }

    wg.Wait()
    fmt.Println("程序结束......")

}

 

posted on 2022-12-06 16:46  江湖乄夜雨  阅读(172)  评论(0编辑  收藏  举报