記事のディレクトリ
並行プログラミングの概念に行く
ほとんどすべての言語が並行プログラミングをサポートしています。Goも例外ではなく、軽量の並行プログラミング(コルーチン)はGolangの重要な機能です。
基本コンセプト
プロセス:プロセスは、オペレーティングシステムでのプログラムの実行プロセスです。これは、リソースの割り当てとスケジューリングのためのシステムの基本単位です。
スレッド:プロセスの実行インスタンスであり、プログラムの最小の実行単位であり、プロセスよりも小さく、独立して実行できる基本単位です。プロセスは複数のスレッドを作成および破棄できます。プログラムには少なくとも1つのプロセスがあり、プロセスには少なくとも1つのスレッドが含まれます。
同時実行性:マルチスレッドプログラムは単一のコアで実行されます。これは同時実行性です。一度に実行されるスレッドは1つだけです。 、および複数のスレッドが交代で実行されます。実行(各スレッドが順番に実行される時間は非常に短く、巨視的には複数のスレッドが同時に実行されるように見えます)。
並列:マルチスレッドプログラムも複数のコアで並列に実行されており、特定の時間に複数のスレッドが実行されています。
Goスレッドとコルーチン:1つのGOスレッドで複数のコルーチン(Goに固有の軽量スレッド)を起動できます。
コルーチンの機能:独立したスタックスペース、共有プログラムヒープスペース、ユーザーが制御するスケジューリング、
メインスレッドは物理スレッドです。 CPUに作用し、重量があり、CPUリソースを非常に消費します。コルーチンは、メインスレッドから開始される軽量スレッドです。一般的な言語は、より多くのコルーチンスレッドを開くことをサポートしておらず、リソースの消費量は膨大になりますが、golangコルーチンは簡単に数万を開くことができます。
Golangは、プログラムが実行するCPUの数を設定できます(1.8以降のデフォルトではマルチコア操作)
num := runtime.NumCPU()
runtime.GOMAXPROCS(num)
fmt.Println("程序运行cpu数量:",num)
ゴルーチン
サンプルコード
コルーチンを開始して1秒あたり1文を出力し、メインスレッドも1秒あたり1文を出力します
func test01(){
go sayHello()//启动协程执行逻辑
for i:=0;i<10;i++{
fmt.Println("主线程 ====》",i)
time.Sleep(time.Second)
}
}
//定义一个协程方法
//每秒输出一个 hello world 输出 10次
func sayHello(){
for i:=0;i<10;i++{
fmt.Println("协程 hello world!",i)
time.Sleep(time.Second)
}
}
実行結果、2つのロジックが同時に実行されていることがわかります
主线程 ====》 0
协程 hello world! 0
协程 hello world! 1
主线程 ====》 1
主线程 ====》 2
协程 hello world! 2
协程 hello world! 3
主线程 ====》 3
协程 hello world! 4
主线程 ====》 4
主线程 ====》 5
协程 hello world! 5
协程 hello world! 6
主线程 ====》 6
主线程 ====》 7
协程 hello world! 7
协程 hello world! 8
主线程 ====》 8
主线程 ====》 9
协程 hello world! 9
1〜100の整数の次数加算を計算し、結果をマップに出力します
//示例 多协程计算 0-200 每个数字的阶乘,把结果放到map 中
// 执行完成后主线程打印结果
//声明全局变量
//不加锁时 没有出现任何异常
var(
resMap = make(map[int]int,10)
)
func test02(){
for i:=1;i<=100;i++{
go nmuls(i)
}
time.Sleep(time.Second*10)
fmt.Println(resMap)//打印map
//map[85:3656 48:1177 78:3082 35:631 41:862 55:1541 63:2017 66:221...
}
//阶加计算方法
func nmuls(num int)int{
res:=1
for i:=1;i<=num;i++{
res += i
}
resMap[num]=res //将结果放入全局map中
return res
}
チャネル
異なるコルーチン間で通信する方法
は、グローバルミューテックスを使用するか、パイプを使用できます(パイプ自体はスレッドセーフです)
次のような
var変数名chanデータ型を宣言します。varintChanchanintintデータのみを配置するチャネルを宣言します。
注意点
- チャネルの本質はデータ構造キューであり、先入れ先出しです。
- チャネル自体はスレッドセーフです
- チャネルは型指定されており、宣言された型と同じ型のデータのみを格納できます。
- チャネルは参照型であり、データを書き込む前に初期化(make)する必要があります
- チャネル内の要素がいっぱいになった後、要素が配置されたときにデッドロック例外が報告されます
- コルーチンを使用せずに、チャネル内のデータがフェッチされている場合、フェッチ後にデッドロックが報告されます
- トラバーサル中にチャネルが閉じられていない場合、デッドロック例外が報告されます。理由は6と同じである必要があります。
- チャネルは、読み取り専用/書き込み専用として宣言できます。詳細については例を参照してください
サンプルコード
func test03(){
var intChan chan int
intChan = make(chan int,3) //创建一个可以存放 3个int的channel
fmt.Printf("intChan 值 =%v 地址是=%p \n",intChan,&intChan)
//intChan 值 =0xc042076080 地址是=0xc04206e018
//插入数据
intChan<- 10
intChan<- 2
intChan<- 13
//fatal error: all goroutines are asleep - deadlock!
//添加的元素超过 容量时 鲍deadlock 错误
//intChan<- 4
//intChan 长度 len= 3 容量= 3
fmt.Printf("intChan 长度 len= %v 容量= %v \n",len(intChan),cap(intChan))
//读取数据
num2:=<-intChan
fmt.Println("num2=",num2)
fmt.Printf("intChan 长度 len= %v 容量= %v \n",len(intChan),cap(intChan))
num3:=<-intChan
num4:=<-intChan
//没有元素的时候获取元素 会报异常 fatal error: all goroutines are asleep - deadlock!
//num4=<-intChan
fmt.Println(num3,num4)
//创建可以放所有类型的 channel
var aChan chan interface{
}
aChan = make(chan interface{
},5)
aChan<- 1
aChan<- "123"
aChan<- 12.12
o:=<-aChan
fmt.Println("取出元素:",o)//取出元素: 1
//channel 遍历 不能通过普通的for 循环遍历 (数量可能会变化)
//
// for i:=0;i<len(aChan);i++{
// }
//遍历时如果 channel 没有关闭 则会报 deadlock 异常,fatal error: all goroutines are asleep - deadlock!
//如果channel 关闭了,则不会报异常,遍历完 程序退出
fmt.Printf("aChan 长度 len= %v 容量= %v \n",len(aChan),cap(aChan))
close(aChan)
for v:=range aChan{
fmt.Println("v=",v)
}
}
データ同期の例を実現するためのミューテックスロック
//示例 多协程计算 0-200 每个数字的阶乘,把结果放到map 中
// 执行完成后主线程打印结果
//声明全局变量
//不加锁时 没有出现任何异常
var(
resMap = make(map[int]int,10)
lock sync.Mutex //定义全局排他锁
)
func test02(){
for i:=1;i<=20;i++{
go nmuls(i)
}
time.Sleep(time.Second*10)
fmt.Println(resMap)//打印map
//map[85:3656 48:1177 78:3082 35:631 41:862 55:1541 63:2017 66:221
}
//阶加计算方法
func nmuls(num int)int{
res:=1
for i:=1;i<=num;i++{
res += i
}
lock.Lock()//写数据前枷锁
resMap[num]=res //将结果放入全局map中
lock.Unlock()//写数据完成进行解锁
return res
}
チャネルの例
素数である1〜20000の数を数えます
func test04(){
intChan := make(chan int,1000)
primeChan := make(chan int,2000)
exitChan := make(chan bool,4)
//开启协程 插入数据
go putNum(intChan,100)
//开启四个 计算协程
for i:=1;i<=4;i++{
go primeNum(intChan,primeChan,exitChan)
}
//当 exitchannel中取出四个元素后 证明任务执行完毕
go func(){
for i:=1;i<=4;i++{
<-exitChan
}
close(primeChan)
}()
//遍历 primeChan
fmt.Println("素数共",len(primeChan),"个如下:")
for v:=range primeChan{
fmt.Println(v)
}
}
//放指定数量的数到channel中
func putNum(intChan chan int,num int){
for i:=1;i<=num ;i++{
intChan<-i
}
//添加完了 关闭 channel
close(intChan)
}
//从 channel中取数据并判断是否为素数,如果是 则添加到 素数 channel中
//如果取不到数据了(数据一处理完)则 exitChannel中添加元素并 退出程序
func primeNum(intChan chan int,primeChan chan int,exitChan chan bool){
var flag bool
for{
time.Sleep(time.Millisecond*10)//睡10毫秒
num,ok:=<-intChan
if !ok{
break; //取不到数据 则 退出
}
flag =true
for i:=2;i<num;i++{
if num%i ==0{
//证明是不是素数
flag = false
break
}
}
if flag{
//如果是素数则加到channel中
primeChan<-num
}
}
fmt.Println("一个协程由于取不到数据而退出")
exitChan<-true//添加标识 到 退出 channel
}
読み取り専用および書き込み専用のコルーチンの例
func test05(){
//默认情况下 channel是可读可写
//声明为只写
var chan2 chan<- int
chan2 = make(chan int,3)
chan2<- 1
chan2<- 2
//num:=<-chan2 //error invalid operation:
//声明只读channel
//var chan3 <-chan int
//chan3 = make(chan int,3)
//chan3<- 1 //invalid operation:
//num:=<-chan3
//fmt.Println("num=",num) // deadlock!
//协程测试 只读 和只写
var ch chan int
ch = make(chan int,10)
exitChan := make(chan bool,2)
go send(ch,exitChan)
go recv(ch,exitChan)
// go func(){
// for i:=1;i<=2;i++ {
// val :=<-exitChan
// fmt.Println("执行结束")
// }
// close(exitChan)
// }()
total:=0
for v := range exitChan{
fmt.Println("读取内容v=",v)
total ++
if(total>= 2){
break
}
}
fmt.Println("执行结束")
}
//该方法只写数据到 channel
func send(ch chan<- int,exitChan chan bool){
fmt.Println("send 开始")
for i:=1;i<=10;i++{
ch<-i
}
close(ch)
fmt.Println("send 结束")
exitChan<- true
}
//该方法只读数据
func recv(ch <-chan int,exitChan chan bool){
fmt.Println("recv 开始")
for {
v,ok:=<-ch
if !ok{
fmt.Println("读取值错误",ok)
break
}
fmt.Println("读取到值==》:",v)
}
fmt.Println("recv 结束")
exitChan<-true
}
デッドロックの問題を解決するために選択する
func test06(){
//select 解决从 管道 取数据的阻塞问题
intChan :=make(chan int,10)
for i:=0;i<10;i++{
intChan<-i
}
strChan :=make(chan string,5)
for i:=0;i<5;i++{
strChan<-"hello"+fmt.Sprintf("%d",i)
}
//传统方式 遍历管道时 如果 没有关闭会导致阻塞 而报错 deadlock
//实际开发过程中,可能不确实什么时候该关闭管道
//可以使用select 解决报错的问题
for{
select {
//这里 虽然intChan一直没有关闭,但是不会一直阻塞而deadlock,而是自动到写一个case匹配
case v:=<-intChan:
fmt.Printf("从intChan取值 %d \n",v)
time.Sleep(time.Second)
case v:=<-strChan:
fmt.Printf("从strChan取值 %s \n",v)
time.Sleep(time.Second)
default:
fmt.Printf("都没有取到值\n")
return
}
}
}
コルーチン例外処理
func test07(){
go sayHello2()
go testPanic()
for i:=0;i<11;i++{
fmt.Println("main ok~",i)
time.Sleep(time.Second)
}
}
func sayHello2(){
for i:=0;i<10;i++{
time.Sleep(time.Second)
fmt.Println("helloWorld~")
}
}
func testPanic(){
//捕获异常
//如果不捕获这个异常,那么主线程一起其他协程都会因异常而退出执行
//捕获之后,该协程的异常不会影响其他协程和主线程
defer func(){
if err:=recover();err!=nil{
fmt.Println("方法发生错误,",err)
}
}()
var mapp map[int]string
mapp[1]="golang"//没有执行make所以肯定报错
}