go语言并发编程

文章目录[x]
  1. 1:go关键字
  2. 2:runtime包
  3. 3:临界资源
  4. 4:WaitGroup:同步等待组
  5. 5:MUTEX互斥锁
  6. 6:读写锁RWmutex

Go是并发语言,而不是并行语言。欲知后事如何,且听小生。。。

开始之前我们先看一下什么是并发,什么是并行。。

并发小生理解,就是现在干这件事,下一秒干下一件事,两件事切换间隔的时间极短,看起来似乎是同时干的,实则不是。

并行就是同时干两件事事情,比如你在跑步的时候听歌。

再来看一下什么是进程什么是线程什么是协程。。。

进程(Process),线程(Thread),协程(Coroutine,也叫轻量级线程)

进程 进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序”,它是CPU资源分配和调度的独立单位。 进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。 进程的局限是创建、撤销和切换的开销比较大。

线程 线程是在进程之后发展出来的概念。 线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。 线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,缺点是线程没有自己的系统资源,只拥有在运行时必不可少的资源,但同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。不过对于某些独占性资源存在锁机制,处理不当可能会产生“死锁”。

协程 协程是一种用户态的轻量级线程,又称微线程,英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。 子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

协程与多线程相比,其优势体现在:协程的执行效率极高。因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显。

go关键字


相比于其他的语言,比如java,我感觉go的还是比较简练的。这无疑又体现出了go的哲学思想,少就是多。

go语言的的并发:

系统自动创建并启动主goroutine,执行对应的main()
用于自己创建并启动子goroutine,执行对应的函数,需要注意的是子goroutine中执行的函数,往往没有返回值。即使有也会被舍弃。

说着总是感觉玄而又玄,还是要直接看代码

package main

import (
"fmt"
"time"
)

func main() {

//1.先创建并启动子goroutine,执行printNum()
go printNum()

//2.main中打印字母
for i:=1;i<=100;i++{
fmt.Printf("\t主goroutine中打印字母:A %d\n",i)
}

time.Sleep(1*time.Second)
fmt.Println("main...over...")

}

func printNum() {
for i := 1; i <= 100; i++ {
fmt.Printf("子goroutine中打印数字:%d\n", i)
}
}

执行结果是说不准的,每次运行都不一样,电脑不同结果也不同。

runtime包


package main

import (
"fmt"
"runtime"
"time"
)

func init() {
//获取逻辑cpu的数量
fmt.Println("逻辑CPU的数量-->",runtime.NumCPU())

//设置go程序执行的最大的cpu的数量:[1,256]
n := runtime.GOMAXPROCS(runtime.NumCPU())
fmt.Println(n)
}
func main() {
//获取goroot目录
fmt.Println("GOROOT-->",runtime.GOROOT()) //GOROOT--> /usr/local/go
//获取操作系统
fmt.Println("os/platform-->",runtime.GOOS) //os/platform--> linux系统

//gosched
//go func(){
// for i:=0;i<5;i++{
// fmt.Println("goroutine...")
// }
//}()
//
//for i:=0;i<4;i++{
// //让出时间片,先让别的goroutine执行
// runtime.Gosched()
// fmt.Println("main...")
//}

//创建goroutine
go func() {
fmt.Println("goroutine开始。。")
//调用fun
fun()
fmt.Println("goroutine结束。。。")
}()

//睡一会儿
time.Sleep(3*time.Second)

}

func fun(){
defer fmt.Println("derfer...")
//return //终止函数
runtime.Goexit() //终止当前的goroutine
fmt.Println("fun函数。。。")
}

运行结果:

逻辑CPU的数量--> 4
4
GOROOT--> /usr/local/go
os/platform--> linux
goroutine开始。。
derfer...

 

上面的让出时间片的函数,以及终止gorotine的函数需要注意一下

临界资源


临界资源:是指并发编程中共享的资源。(进程、线程、协程)

在并发编程中如果对共享资源处理不好,会导致数据不一样。看下面一个例子:

package main

import (
"fmt"
"time"
)

func main() {
/*
临界资源:
*/
a := 1
go func() {
a = 2
fmt.Println("goroutine中。。",a)
}()

a = 3
time.Sleep(2)
fmt.Println("main goroutine...",a)
}

运行结果

WaitGroup:同步等待组


直接看例子

package main

import (
"fmt"
"sync"
)

var wg sync.WaitGroup //创建同步等待组的对象
func main() {
/*
WaitGroup:同步等待组
Add(),设置等待组中要执行的子 goroutine的数量

Wait(),让主goroutine出于等待

Done(),让等待组中的counter计数器的值减1,同Add(-1)

*/
wg.Add(2)//counter 3 2 1

go fun1()
go fun2()

fmt.Println("main 进入阻塞状态。。等待wg中的子goroutien结束。。")
wg.Wait() // 表示main goroutine进入等待,意味着阻塞
fmt.Println("main..解除阻塞。。")

}

func fun1(){
for i:=1;i<10;i++{
fmt.Println("fun1()函数中打印。。A ",i)
}

wg.Done() //给wg等待组中的counter数值减1。同 wg.Add(-1)
}

func fun2(){
defer wg.Done()
for j:=1;j<10;j++{
fmt.Println("\tfun2()函数打印。。",j)
}
}

运行结果:

MUTEX互斥锁


我们知道了在并发程序中,会存在临界资源问题。就是当多个协程来访问共享的数据资源,那么这个共享资源是不安全的。为了解决协程同步的问题我们使用了channel,但是Go语言也提供了传统的同步工具。 sync 包提供了两种锁类型:sync.Mutex 和 sync.RWMutex。我们来看一下两个的用法。

Mutex 是最简单的一种锁类型,互斥锁,同时也比较暴力,当一个 goroutine 获得了 Mutex 后,其他 goroutine 就只能乖乖等到这个 goroutine 释放该 Mutex。

说的这么多,还是看例子直观。。。

package main

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

//全局变量,表示票
var ticket = 10 //10张票

var mutex sync.Mutex //创建锁头

var wg sync.WaitGroup //同步等待组对象
func main() {
/*
4个goroutine,模拟4个售票口,

在使用互斥锁的时候,对资源操作完,一定要解锁。否则会出现程序异常,死锁等问题。
defer语句
*/

wg.Add(4)
go saleTickets("售票口1")
go saleTickets("售票口2")
go saleTickets("售票口3")
go saleTickets("售票口4")

wg.Wait() //main要等待
fmt.Println("程序结束了。。。")

//time.Sleep(5*time.Second)
}

func saleTickets(name string){
rand.Seed(time.Now().UnixNano())
defer wg.Done()
for{
//上锁
mutex.Lock() //g2
if ticket > 0{ //ticket 1 g1
time.Sleep(time.Duration(rand.Intn(1000))*time.Millisecond)
fmt.Println(name,"售出:",ticket) // 1
ticket-- // 0
}else{
mutex.Unlock() //条件不满足,也要解锁
fmt.Println(name,"售罄,没有票了。。")
break
}
mutex.Unlock() //解锁
}
}

运行结果

售票口4 售出: 10
售票口4 售出: 9
售票口1 售出: 8
售票口2 售出: 7
售票口3 售出: 6
售票口4 售出: 5
售票口1 售出: 4
售票口2 售出: 3
售票口3 售出: 2
售票口4 售出: 1
售票口3 售罄,没有票了。。
售票口4 售罄,没有票了。。
售票口1 售罄,没有票了。。
售票口2 售罄,没有票了。。
程序结束了。。。

可以看出,票数并不会出现负数。。。

在看一下下面的这种情况,就是在没有互斥锁的情况下,就会出现临界资源被抢占的问题,导致出现负票,看下面的程序

package main

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

//全局变量,表示票
var ticket1 = 10 //10张票
func main() {
/*
4个goroutine,模拟4个售票口,
*/
go saleTickets("售票口1")
go saleTickets("售票口2")
go saleTickets("售票口3")
go saleTickets("售票口4")

time.Sleep(5*time.Second)
}

func saleTickets(name string){
rand.Seed(time.Now().UnixNano())
for{
if ticket1 > 0{
time.Sleep(time.Duration(rand.Intn(1000))*time.Millisecond)
fmt.Println(name,"售出:",ticket1)
ticket1--
}else{
fmt.Println(name,"售罄,没有票了。。")
break
}
}
}

看一下运行结果

售票口1 售出: 10
售票口2 售出: 9
售票口4 售出: 8
售票口3 售出: 7
售票口1 售出: 6
售票口4 售出: 5
售票口2 售出: 4
售票口4 售出: 3
售票口1 售出: 2
售票口3 售出: 1
售票口3 售罄,没有票了。。
售票口2 售出: 0
售票口2 售罄,没有票了。。
售票口4 售出: -1
售票口4 售罄,没有票了。。
售票口1 售出: -2
售票口1 售罄,没有票了。。

 

根据上面的执行结果,我们知道,当其中一个goroutine判断完条件后,准备卖票还没有卖票额时候,其他子进程抢占资源,导致了负票的出现。

读写锁RWmutex


RWMutex是读/写互斥锁。锁可以由任意数量的读取器或单个编写器持有。RWMutex的零值是未锁定的mutex。

  1. 同时只能有一个 goroutine 能够获得写锁定。

  2. 同时可以有任意多个 gorouinte 获得读锁定。

  3. 同时只能存在写锁定或读锁定(读和写互斥)。

说的这么多,感觉还是有点迷,直接看代码例子

这是读操作,可以多个同时

package main

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

var rwMutex *sync.RWMutex
var wg1 *sync.WaitGroup
func main() {
rwMutex = new(sync.RWMutex)
wg1 = new (sync.WaitGroup)

wg1.Add(2)

//多个同时读取
go readData(1)
go readData(2)

wg1.Wait() //主进程等待
fmt.Println("main..over...")
}

func writeData(i int){
defer wg1.Done()
fmt.Println(i,"开始写:write start。。")
rwMutex.Lock()//写操作上锁
fmt.Println(i,"正在写:writing。。。。")
time.Sleep(3*time.Second)
rwMutex.Unlock()
fmt.Println(i,"写结束:write over。。")
}

func readData(i int) {
defer wg1.Done()

fmt.Println(i, "开始读:read start。。")

rwMutex.RLock() //读操作上锁
fmt.Println(i,"正在读取数据:reading。。。")
time.Sleep(3*time.Second)
rwMutex.RUnlock() //读操作解锁
fmt.Println(i,"读结束:read over。。。")
}

执行结果:

2 开始读:read start。。
2 正在读取数据:reading。。。
1 开始读:read start。。
1 正在读取数据:reading。。。
1 读结束:read over。。。
2 读结束:read over。。。
main..over...

看一下下面的代码,下面的代码是写锁的时候,是不允许读或者写操作的。

package main

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

var rwMutex *sync.RWMutex
var wg1 *sync.WaitGroup
func main() {
rwMutex = new(sync.RWMutex)
wg1 = new (sync.WaitGroup)

wg1.Add(3)
go writeData(1)
go readData(2)
go writeData(3)

wg1.Wait() //主进程等待
fmt.Println("main..over...")
}

func writeData(i int){
defer wg1.Done()
fmt.Println(i,"开始写:write start。。")
rwMutex.Lock()//写操作上锁
fmt.Println(i,"正在写:writing。。。。")
time.Sleep(2*time.Second)
rwMutex.Unlock()
fmt.Println(i,"写结束:write over。。")
}

func readData(i int) {
defer wg1.Done()

fmt.Println(i, "开始读:read start。。")

rwMutex.RLock() //读操作上锁
fmt.Println(i,"正在读取数据:reading。。。")
time.Sleep(2*time.Second)
rwMutex.RUnlock() //读操作解锁
fmt.Println(i,"读结束:read over。。。")
}

执行结果:

3 开始写:write start。。
3 正在写:writing。。。。
1 开始写:write start。。
2 开始读:read start。。
3 写结束:write over。。
2 正在读取数据:reading。。。
2 读结束:read over。。。
1 正在写:writing。。。。
1 写结束:write over。。
main..over...
点赞
  1. Firefox Windows 10

    又发现一个好站,收藏了~以后会经常光顾的 (。•ˇ‸ˇ•。)

    1. 洛 尘曦 洛 尘曦说道:
      Google Chrome Windows 10

      谢谢

发表评论

昵称和uid可以选填一个,填邮箱必填(留言回复后将会发邮件给你)
tips:输入uid可以快速获得你的昵称和头像

Title - Artist
0:00