【Golang実戦】Golangが無視している問題点を整理する

Golang によって無視される問題を整理する

ここに画像の説明を挿入します

参考文献

https://github.com/aceld/golang
https://zhuanlan.zhihu.com/p/597424646
https://www.cnblogs.com/xxswkl/p/14248560.html

1. WaitGroup と GoRoutine 間のレース

package main

import (
	"sync"
	//"time"
)

const N = 10

var wg = &sync.WaitGroup{}

func main() {

	for i := 0; i < N; i++ {
		go func(i int) {
			wg.Add(1)
			println(i)
			defer wg.Done()
		}(i)
	}
	wg.Wait()

}

結果は一意ではなく、すべての go が実行されない可能性があります。これは、go の実行が速すぎるため、main 関数が実行される前に wg.Add(1) が実行されるためです。

改善点は以下の通りです

package main

import (
	"sync"
)

const N = 10

var wg = &sync.WaitGroup{}

func main() {

    for i:= 0; i< N; i++ {
        wg.Add(1)
        go func(i int) {
            println(i)
            defer wg.Done()
        }(i)
    }

    wg.Wait()
}

2.Mutex ミューテックス ロックおよび RWMutex ミューテックス読み取り/書き込みロック

ミューテックス、絶対ロック (ミューテックス ロック)、RWMutex は 1 つだけ
、読み取り/書き込みロック、同時に RLock() 読み取りロック、複数の読み取りロックが可能、Lock() 書き込みロック、書き込み操作は完全に相互排他的、ゴルーチンが When を書き込むとき、他のゴルーチンは書き込みも読み取りもできません

var count int
var wg sync.WaitGroup
var rw sync.RWMutex
func main() {
    wg.Add(10)
    for i:=0;i<5;i++ {
        go read(i)
    }
    for i:=0;i<5;i++ {
        go write(i);
    }
    wg.Wait()
}
func read(n int) {
    // 读锁是RLock(),
    rw.RLock()
    fmt.Printf("读goroutine %d 正在读取...\n",n)
    v := count
    fmt.Printf("读goroutine %d 读取结束,值为:%d\n", n,v)
    wg.Done()
    rw.RUnlock()
}
func write(n int) {
    // 写锁是Lock()
    rw.Lock()
    fmt.Printf("写goroutine %d 正在写入...\n",n)
    v := rand.Intn(1000)
    count = v
    fmt.Printf("写goroutine %d 写入结束,新值为:%d\n", n,v)
    wg.Done()
    rw.Unlock()
}

Map は、読み取り/書き込みロックに基づいてスレッドセーフな SynchronizedMap に変換できます。

// 安全的Map
type SynchronizedMap struct {
   rw *sync.RWMutex
   data map[interface{}]interface{}
}
// 存储操作
func (sm *SynchronizedMap) Put(k,v interface{}){
   sm.rw.Lock()
   defer sm.rw.Unlock()

   sm.data[k]=v
}
// 获取操作  只有这个加的是读锁,
func (sm *SynchronizedMap) Get(k interface{}) interface{}{
   sm.rw.RLock()
   defer sm.rw.RUnlock()

   return sm.data[k]
}
// 删除操作
func (sm *SynchronizedMap) Delete(k interface{}) {
   sm.rw.Lock()
   defer sm.rw.Unlock()

   delete(sm.data,k)
}
// 遍历Map,并且把遍历的值给回调函数,可以让调用者控制做任何事情
func (sm *SynchronizedMap) Each(cb func (interface{},interface{})){
   sm.rw.RLock()
   defer sm.rw.RUnlock()
   for k, v := range sm.data {
       cb(k,v)
   }
}
// 生成初始化一个SynchronizedMap
func NewSynchronizedMap() *SynchronizedMap{
   return &SynchronizedMap{
       rw:new(sync.RWMutex),
       data:make(map[interface{}]interface{}),
   }
}

3.ポーリング、選択、エポーリング

世論調査

while true {
	for i in 流[] {
		if i has 数据 {
			读 或者 其他处理
		}
	}
}

選択する

while true {
	select(流[]); //阻塞

	//有消息抵达
	for i in 流[] {
		if i has 数据 {
			读 或者 其他处理
		}
	}
}

エポール

while true {
	可处理的流[] = epoll_wait(epoll_fd); //阻塞

	//有消息抵达,全部放在 “可处理的流[]”中
	for i in 可处理的流[] {
		读 或者 其他处理
	}
}

4. スタックとヒープはいつ使用されますか?

コンパイラはエスケープ解析を行い、変数のスコープが関数のスコープを超えていないことが判明した場合はスタック上に置くことができますが、それ以外の場合はヒープ上に割り当てる必要があります。

5. GoRoutine の合理的な使用

GoRoutine は約 2.5KB を消費します

推奨事項 1: チャネルと同期同期の組み合わせ方法

package main

import (
    "fmt"
    "math"
    "sync"
    "runtime"
)

var wg = sync.WaitGroup{}

func busi(ch chan bool, i int) {

    fmt.Println("go func ", i, " goroutine count = ", runtime.NumGoroutine())

    <-ch

    wg.Done()
}

func main() {
    //模拟用户需求go业务的数量
    task_cnt := math.MaxInt64

    ch := make(chan bool, 3)

    for i := 0; i < task_cnt; i++ {
		wg.Add(1)

        ch <- true

        go busi(ch, i)
    }

	  wg.Wait()
}

推奨事項 2: バッファなしチャネルとタスク送信/実行分離方法を使用する

package main

import (
    "fmt"
    "math"
    "sync"
    "runtime"
)

var wg = sync.WaitGroup{}

func busi(ch chan int) {

    for t := range ch {
        fmt.Println("go task = ", t, ", goroutine count = ", runtime.NumGoroutine())
        wg.Done()
    }
}

func sendTask(task int, ch chan int) {
    wg.Add(1)
    ch <- task
}

func main() {

    ch := make(chan int)   //无buffer channel

    goCnt := 3              //启动goroutine的数量
    for i := 0; i < goCnt; i++ {
        //启动go
        go busi(ch)
    }

    taskCnt := math.MaxInt64 //模拟用户需求业务的数量
    for t := 0; t < taskCnt; t++ {
        //发送任务
        sendTask(t, ch)
    }

	  wg.Wait()
}

6.GoRoutine が正常に終了する

6.1データチャネルクローズ通知出口

単純なタスクに適しています。複雑なタスクの場合は、個別のコンテキスト通知を推奨します。

// cancelFn 数据通道关闭通知退出
func cancelFn(dataChan chan int) {
    for {
        select {
        case val, ok := <-dataChan:
            // 关闭data通道时,通知退出
            // 一个可选是判断data=指定值时退出
            if !ok {
                log.Printf("Channel closed !!!")
                return
            }

            log.Printf("Revice dataChan %d\n", val)
        }
    }
}

6.2出口チャネル終了通知出口

いくつかの単純なシナリオに適用可能

// exitChannelFn 单独退出通道关闭通知退出
func exitChannelFn(wg *sync.WaitGroup, taskNo int, dataChan chan int, exitChan chan struct{}) {
    defer wg.Done()

    for {
        select {
        case val, ok := <-dataChan:
            if !ok {
                log.Printf("Task %d channel closed !!!", taskNo)
                return
            }

            log.Printf("Task %d  revice dataChan %d\n", taskNo, val)

            // 关闭exit通道时,通知退出
        case <-exitChan:
            log.Printf("Task %d  revice exitChan signal!\n", taskNo)
            return
        }
    }

}

6.3コンテキストのタイムアウトまたはキャンセル通知の終了

主流の推奨事項

// contextCancelFn context取消或超时通知退出
func contextCancelFn(wg *sync.WaitGroup, taskNo int, dataChan chan int, ctx context.Context) {
    defer wg.Done()

    for {
        select {
        case val, ok := <-dataChan:
            if !ok {
                log.Printf("Task %d channel closed !!!", taskNo)
                return
            }

            log.Printf("Task %d  revice dataChan %d\n", taskNo, val)

        // ctx取消或超时,通知退出
        case <-ctx.Done():
            log.Printf("Task %d  revice exit signal!\n", taskNo)
            return
        }
    }

}

6.4WaitGroup/ErrGroupは全てのコルーチンが閉じたと判断して終了する

最も一般的に使用されるものは、以下を参照してください

// 多个任务并行控制,等待所有任务完成
func TestTaskControl(t *testing.T) {
    dataChan := make(chan int)

    taskNum := 3

    wg := sync.WaitGroup{}
    wg.Add(taskNum)

    // 起多个协程,data关闭时退出
    for i := 0; i < taskNum; i++ {
        go func(taskNo int) {
            defer wg.Done()
            t.Logf("Task %d run\n", taskNo)

            for {
                select {
                case _, ok := <-dataChan:
                    if !ok {
                        t.Logf("Task %d notify to stop\n", taskNo)
                        return
                    }
                }
            }
        }(i)
    }

    // 通知退出
    go func() {
        time.Sleep(3 * time.Second)
        close(dataChan)
    }()

    // 等待退出完成
    wg.Wait()
}

7.メーカーと新品の違い

同じ
ヒープ領域の割り当て

異なる
make: スライス、マップ、チャネルの初期化にのみ使用され、置き換え不可能
new: タイプ メモリの割り当てに使用され (初期値は 0)、一般的には使用されず、
new は一般的に使用されない
ため、組み込み関数 new があります。私たちはそれを使用しますが、実際のコーディングでは一般的には使用されません。私たちは通常、目標を達成するために、次のような短いステートメント宣言と構造リテラルを使用します:
i : =0
u := user{}
make は置き換えられません。
スライス、マップ、チャネルを使用する場合でも、make を使用する必要があります。それらを使用する前に初期化してください。それらを操作することができます。

8. 動的キープアライブ ワーカー プール

WorkerManager はメインのゴルーチンとして機能し、ワーカーは子ゴルーチンとして機能します。

WorkerManager.go

type WorkerManager struct {
   //用来监控Worker是否已经死亡的缓冲Channel
   workerChan chan *worker
   // 一共要监控的worker数量
   nWorkers int
}

//创建一个WorkerManager对象
func NewWorkerManager(nworkers int) *WorkerManager {
   return &WorkerManager{
      nWorkers:nworkers,
      workerChan: make(chan *worker, nworkers),
   }
}

//启动worker池,并为每个Worker分配一个ID,让每个Worker进行工作
func (wm *WorkerManager)StartWorkerPool() {
   //开启一定数量的Worker
   for i := 0; i < wm.nWorkers; i++ {
      i := i
      wk := &worker{id: i}
      go wk.work(wm.workerChan)
   }

  //启动保活监控
   wm.KeepLiveWorkers()
}

//保活监控workers
func (wm *WorkerManager) KeepLiveWorkers() {
   //如果有worker已经死亡 workChan会得到具体死亡的worker然后 打出异常,然后重启
   for wk := range wm.workerChan {
      // log the error
      fmt.Printf("Worker %d stopped with err: [%v] \n", wk.id, wk.err)
      // reset err
      wk.err = nil
      // 当前这个wk已经死亡了,需要重新启动他的业务
      go wk.work(wm.workerChan)
   }
}

ワーカーゴー

type worker struct {
   id  int
   err error
}

func (wk *worker) work(workerChan chan<- *worker) (err error) {
   // 任何Goroutine只要异常退出或者正常退出 都会调用defer 函数,所以在defer中想WorkerManager的WorkChan发送通知
   defer func() {
      //捕获异常信息,防止panic直接退出
      if r := recover(); r != nil {
         if err, ok := r.(error); ok {
            wk.err = err
         } else {
            wk.err = fmt.Errorf("Panic happened with [%v]", r)
         }
      } else {
         wk.err = err
      }
 
     //通知 主 Goroutine,当前子Goroutine已经死亡
      workerChan <- wk
   }()

   // do something
   fmt.Println("Start Worker...ID = ", wk.id)

   // 每个worker睡眠一定时间之后,panic退出或者 Goexit()退出
   for i := 0; i < 5; i++ {
      time.Sleep(time.Second*1)
   }

   panic("worker panic..")
   //runtime.Goexit()

   return err
}

3.
main.goをテストする

func main() {
   wm := NewWorkerManager(10)

   wm.StartWorkerPool()
}

子ゴルーチンが、panic() 例外によって終了するか、Goexit() 終了によって終了するかに関係なく、メインのゴルーチンによって監視され、再起動されることがわかります。主にキープアライブ機能を実行できますが、もちろん、スレッドが停止した場合はどうなるでしょうか? プロセスが死んでしまった?どうすればこれを保証できるでしょうか? 心配しないでください、私たちの Go での開発は実際には Go のスケジューラに基づいています。プロセスとスレッド レベルの死はスケジューラの死を引き起こし、基本的なフレームワーク全体が崩壊します。次に、スレッドとプロセスがどのように維持されるかに依存しますが、これは Go 開発の範囲を超えています。

9.バグデバッグ/パフォーマンス分析

9.1 シェル組み込み時間コマンド

test2.go を実行してみましょう

9.2 トップと GODEBUG/gctrace

package main

import (
    "log"
    "runtime"
    "time"
)

func test() {
    //slice 会动态扩容,用slice来做堆内存申请
    container := make([]int, 8)

    log.Println(" ===> loop begin.")
    for i := 0; i < 32*1000*1000; i++ {
        container = append(container, i)
    }
    log.Println(" ===> loop end.")
}

func main() {
    log.Println("Start.")

    test()

    log.Println("force gc.")
    runtime.GC() //强制调用gc回收

    log.Println("Done.")

    time.Sleep(3600 * time.Second) //睡眠,保持程序不退出
}

$go build -osnippet_mem && ./snippet_mem
$top -p $(pidofsnippet_mem)

GODEBUG='gctrace=1' ./snippet_mem
gc # @#s #%: #+#+# ミリ秒クロック、#+#/#/#+# ミリ秒 CPU、#->#-># MB、# MB 目標, # P
gc # GC 回数 (
GC ごとに @#s ずつ増分) プログラムの実行開始からの時間
#% 実行時間のうち GC が占める割合
#+…+# GC が使用した時間
#- >#-># MB GC の開始、終了、および現在のアクティブなヒープ メモリ サイズ、単位 M
# MB の目標グローバル ヒープ メモリ サイズ
# P 使用されるプロセッサの数

9.3プロフ

import(
	"net/http"
	_ "net/http/pprof"
)

go func() {
	log.Println(http.ListenAndServe("0.0.0.0:10000", nil))
}()

アドレスを入力します: http://127.0.0.1:10000/debug/pprof/heap?debug=1

10.GCマーク/スイープ

GoV1.3 - 通常のマークとクリアの方法では、プロセス全体で STW を開始する必要があり、非常に非効率です。
GoV1.5-3 色マーキング方式、ヒープ領域は書き込みバリアを開始しますが、スタック領域は開始しません、すべてのスキャン後、スタックを再度スキャンする必要があります (STW が必要)、効率は通常の GoV1.8- 3色マーキング方式、ハイブリッドライトバリア機構
、スタック領域は活性化されませんが、ヒープ領域は活性化されます。プロセス全体では STW がほとんど必要なく、非常に効率的です。

11. メモリオーバーフロー

11.1 たとえば、map[string]*ObjectA は特定の []*ObjectB 内にあり、リサイクルされていません。

map[string]*ObjectA = nil を使用すると、メモリが再利用されない問題を回避できます。

11.2 新しいエラータイプ

创建一个新的类型
type ErrNegativeSqrt float64
并为其实现
func (e ErrNegativeSqrt) Error() string在 Error 方法内调用 fmt.Sprint(e) 会让程序陷入死循环。可以通过先转换 e 来避免这个问题:fmt.Sprint(float64(e))。

Error メソッド内で fmt.Sprint(e) を呼び出すと、プログラムが無限ループに陥ります。この問題は、最初に e を変換することで回避できます: fmt.Sprint(float64(e))。

12. メモリリーク

12.1部分文字列によるメモリリーク

var s0 string // a package-level variable

// A demo purpose function.
func f(s1 string) {
    s0 = s1[:50]
    // Now, s0 shares the same underlying memory block
    // with s1. Although s1 is not alive now, but s0
    // is still alive, so the memory block they share
    // couldn't be collected, though there are only 50
    // bytes used in the block and all other bytes in
    // the block become unavailable.
}

func demo() {
    s := createStringWithLengthOnHeap(1 << 20) // 1M bytes
    f(s)
}

解決:

func f(s1 string) {
    s0 = string([]byte(s1[:50]))
}

func f(s1 string) {
    s0 = (" " + s1[:50])[1:]
}

import "strings"

func f(s1 string) {
    var b strings.Builder
    b.Grow(50)
    b.WriteString(s1[:50])
    s0 = b.String()
}

3 番目の方法の欠点は、少し冗長であることです。良いニュースとして、go1.12 以降では、count パラメーターを 1 に指定して strings.Repeat 関数を呼び出して、文字列のクローンを作成できるようになりました。go1.12 以降、strings.Repeat 関数の基礎となる実装では、不要なコピーを避けるために strings.Builder が使用されます。

12.2 サブスライスによるメモリリーク

var s0 []int

func g(s1 []int) {
    // Assume the length of s1 is much larger than 30.
    s0 = s1[len(s1)-30:]
}

このメモリ リークを回避したい場合は、s0 の活性によって s1 要素のメモリ ブロックの収集が妨げられないように、s0 の 30 個の要素をコピーする必要があります。

func g(s1 []int) {
    s0 = append([]int(nil), s1[len(s1)-30:]...)
    // Now, the memory block hosting the elements
    // of s1 can be collected if no other values
    // are referencing the memory block.
}

12.3 欠落したスライス要素のポインタをリセットしないことによるメモリ リーク

以下のコードでは、 h 関数を呼び出した後、スライス s の最初と最後の要素に割り当てられたメモリ ブロックが失われます。

func h() []*int {
    s := []*int{new(int), new(int), new(int), new(int)}
    // do something with s ...

    return s[1:3:3]
}

返されたスライスがまだ存在する限り、 s の要素は収集されなくなります。これにより、 s の最初と最後の要素によって参照される 2 つの int 値に割り当てられた 2 つのメモリ ブロックが収集されなくなります。
このタイプのメモリ リークを回避したい場合は、欠落している要素に格納されているポインタをリセットする必要があります。

func h() []*int {
    s := []*int{new(int), new(int), new(int), new(int)}
    // do something with s ...

    // Reset pointer values.
    s[0], s[len(s)-1] = nil, nil
    return s[1:3:3]
}

12.4 ゴルーチンのスタックによるメモリリーク

場合によっては、Go プログラム内の一部のゴルーチンが永久にブロックされることがあります。このようなゴルーチンはスタック ゴルーチンと呼ばれます。Go ランタイムは中断されたゴルーチンを強制終了しないため、中断されたゴルーチン (および参照されるメモリ ブロック) に割り当てられたリソースがガベージ コレクションされることはありません。
Go ランタイムは 2 つの理由により、ハングしたゴルーチンを強制終了しません。1 つは、ブロックしているゴルーチンが永続的にブロックされるかどうかを Go ランタイムが判断するのが難しい場合があるということです。もう 1 つは、ゴルーチンを意図的にハングさせる場合があるということです。たとえば、プログラムの終了を避けるために、Go プログラムのメイン goroutine をハングさせることがあります。

12.5 使用されなくなったが停止しない time.Ticker によるメモリ リーク

time.Timer 値が使用されなくなった場合、しばらくしてからガベージ コレクションが行われます。しかし、これは time.Ticker 値には当てはまりません。しばらく立ち止まるべきです。使用されなくなったときのタグの値。

12.6 ファイナライザの誤った使用によって引き起こされるメモリ リーク

循環参照グループのメンバーである値にファイナライザーを設定すると、循環参照グループに割り当てられたすべてのメモリ ブロックの収集が防止されます。これは実際のメモリ リークです。
たとえば、次の関数が呼び出されて終了した後、x と y に割り当てられたメモリ ブロックは、今後のガベージ コレクションでガベージ コレクションされることは保証されません。

func memoryLeaking() {
    type T struct {
        v [1<<20]int
        t *T
    }

    var finalizer = func(t *T) {
         fmt.Println("finalizer called")
    }

    var x, y T

    // The SetFinalizer call makes x escape to heap.
    runtime.SetFinalizer(&x, finalizer)

    // The following line forms a cyclic reference
    // group with two members, x and y.
    // This causes x and y are not collectable.
    x.t, y.t = &y, &x // y also escapes to heap.
}

したがって、循環参照グループ内の値にファイナライザーを設定することは避けてください。
ところで、ファイナライザーをオブジェクト デストラクターとして使用すべきではありません。

13.チャンネル

まずはChannelの特徴についておさらいしてみましょう。
データを nil チャネルに送信し、永続的なブロックを
引き起こす nil チャネルからデータを受信し、永続的なブロックを引き起こす
閉じたチャネルにデータを送信し、パニックを引き起こす
閉じたチャネルからデータを受信する バッファが空の場合、次の A 値を返すゼロは、
バッファされていないチャネルが同期であるのに対し、バッファされたチャネルは非同期であることを意味します。

上記の 5 つの特徴は死んだものですが、公式を通じて記憶することもできます。

「Null の読み取りと書き込みはブロックされ、書き込みは例外でクローズされ、読み取りは null ゼロでクローズされます。」

package main

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

func main() {
	RightExample()
	ErrorExample()
}

func ErrorExample() {
	fmt.Println("ErrorExample")
	ch := make(chan int, 1000)
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
		}
	}()
	go func() {
		for {
			a, ok := <-ch
			if !ok {
				fmt.Println("close")
				return
			}
			fmt.Println("a: ", a)
		}
	}()
	close(ch)
	fmt.Println("ok")
	time.Sleep(time.Second * 100)
}

var wg sync.WaitGroup = sync.WaitGroup{}

func RightExample() {
	fmt.Println("RightExample")
	ch := make(chan int, 1000)
	wg.Add(10)
	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
		}
	}()
	go func() {
		for {
			a, ok := <-ch
			if !ok {
				fmt.Println("close")
				return
			}
			fmt.Println("a: ", a)
			wg.Done()
		}
	}()

	wg.Wait()
	close(ch)
	fmt.Println("ok")
}

14.条件

1. はじめに
sync.Cond は、ミューテックス ロック/読み書きロックに基づいて実装された条件変数であり、共有リソースにアクセスするゴルーチンを調整するために使用されます。共有リソースの状態が変化した場合、sync.Cond を使用して、待機状態の発生によりブロックされた Goroutine に通知できます。

sync.Cond はミューテックス ロック/読み書きロックに基づいていますが、ミューテックス ロックとの違いは何ですか?

ミューテックス ロック sync.Mutex は通常、共有の重要なリソースを保護するために使用され、条件変数 sync.Cond は共有リソースにアクセスするゴルーチンを調整するために使用されます。共有リソースの状態が変化した場合、sync.Cond を使用してブロックされた Goroutine に通知できます。

2. 利用シナリオ
sync.Cond は、複数の Goroutine が待機していて、1 つの Goroutine が通知される (イベントが発生する) シナリオでよく使用されます。通知または待機の場合は、ミューテックスまたはチャネルを使用して実行できます。

非常に単純なシナリオを想像してみましょう。

1 つのコルーチンはデータを非同期で受信しており、残りのコルーチンは正しいデータを読み取る前に、このコルーチンがデータを受信するまで待機する必要があります。この場合、単に chan またはミューテックス ロックを使用した場合、データを待機して読み取ることができるのは 1 つのコルーチンだけであり、他のコルーチンにもデータを読み取るように通知する方法はありません。

このとき、最初のコルーチンがデータの受け入れを完了したかどうかを示すグローバル変数が必要となり、残りのコルーチンは要件が満たされるまで変数の値を繰り返しチェックします。または、複数のチャネルを作成し、各コルーチンがチャネル上でブロックし、データを受信したコルーチンがデータを受信した後に 1 つずつ通知します。つまり、これを達成するにはさらに複雑さが必要になります。

Go 言語には、この種の問題を解決するための標準ライブラリ sync に sync.Cond が組み込まれています。

package main

import (
	"fmt"
	"sync"
)

func main() {
	var mu sync.Mutex
	// 创建 cond
	cond := sync.NewCond(&mu)

	// 计数
	var count uint64

	// 报名表
	var stuSlice []int

	// 模拟学生报名参加课外活动
	for i := 0; i < 30; i++ {
		go func(i int) {
			cond.L.Lock()
			stuSlice = append(stuSlice, i)
			count++
			cond.L.Unlock()

			// Broadcast 唤醒所有等待此 cond 的 goroutine, Signal 只唤醒一个
			cond.Broadcast()
		}(i)
	}

	// 调用 Wait方法前, 调用者必须持有锁
	cond.L.Lock()
	for count != 30 {
		// 调用者被阻塞,并被放入 cond 的等待队列中
		cond.Wait()
	}
	cond.L.Unlock()

	fmt.Println(len(stuSlice), stuSlice)
}

おすすめ

転載: blog.csdn.net/aaaadong/article/details/128835981