Golang基础(线程、锁、channel、GC)

it2024-04-17  47

一.线程休眠

Go语言中main()函数为主线程(协程),程序是从上向下执行的

可以通过time包下的Sleep(n)让程序阻塞多少纳秒

  fmt.Println("1")   //单位是纳秒,表示阻塞多长时间   //e9表示10的9次方   time.Sleep(1e9)   fmt.Println("2")

二.延迟执行

延迟指定时间后执行一次,但是需要注意在触发时程序没有结束

 fmt.Println("开始")   //2秒后执行匿名函数   time.AfterFunc(2e9, func() {      fmt.Println("延迟延迟触发")   })   time.Sleep(10e9)//一定要休眠,否则程序结束了   fmt.Println("结束")

三.goroutine简介

Golang中最迷人的一个优点就是从语言层面就支持并发

在Golang中的goroutine(协程)类似于其他语言的线程

并发和并行

并行(parallelism)指不同的代码片段同时在不同的物理处理器上支持

并发(concurrency)指同时管理多个事情,物理处理器上可能运行某个内容一半后就处理其他事情

在一般看来并发的性能要好于并行.因为计算机的物理资源是固定的,较少的,而程序需要执行的内容是很多的.所以并发是”以较少的资源去去做更多事情”

几种主流并发模型

多线程,每个线程只处理一个请求,只有请求结束后,对应的线程才会接收下一个请求.这种模式在高并发下,性能开销极大.

基于回调的异步IO.在程序运行过程中可能产生大量回调导致维护成本加大,程序执行流程也不便于思维

协程.不需要抢占式调用,可以有效提升线程任务的并发性,弥补了多线程模式的缺点;Golang在语言层面就支持,而其他语言很少支持

goroutine的语法

表达式可以是一条语句

表达式也可以是函数,函数返回值即使有,也无效,当函数执行完成此goroutine自动结束

go 表达式

 代码示例

对比多次调用函数和使用goroutine的效果

package main ​ import "fmt" import "time" ​ func main() {   //正常调用,输出3遍1 2 3 4 5(每个数字后换行)   //for i:=1; i<=3; i++ {   // go demo()   //} ​   /*   添加go关键字后发现控制台什么也没有输出   原因:把demo()设置到协程后没等到函数执行,主   线程执行结束    */   for i := 1; i <= 3; i++ {      go demo(i)   } } ​ func demo(index int) {   for i := 1; i <= 5; i++ {      fmt.Printf("第%d次执行,i的值为:%d\n", index, i)   } }

添加休眠等待goroutine执行结束

这种方式很大的问题就是休眠时间,如果休眠时间设置过小,可能goroutine并没有执行完成,如果休眠时间设置过大,影响程序执行执行.找到的本次执行的休眠时间,下次程序执行时这个休眠时间可能”过大”或”过小"

通过程序运行结果发现每次执行结果都不一定是一样的,因为每个demo()都是并发执行

package main ​ import "fmt" import "time" ​ func main() {   //正常调用,输出3遍1 2 3 4 5(每个数字后换行)   //for i:=1; i<=3; i++ {   // go demo()   //} ​   /*   添加go关键字后发现控制台什么也没有输出   原因:把demo()设置到协程后没等到函数执行,主   线程执行结束    */   for i := 1; i <= 3; i++ {      go demo(i)   } ​   /*   添加休眠,让主线程等待协程执行结束.   具体休眠时间需要根据计算机性能去估计   次数没有固定值    */   time.Sleep(3e9)   fmt.Println("程序执行结束") } ​ func demo(index int) {   for i := 1; i <= 5; i++ {      fmt.Printf("第%d次执行,i的值为:%d\n", index, i)   } }

[备注:

四.WaitGroup简介

Golang中sync包提供了基本同步基元,如互斥锁等.除了Once和WaitGroup类型, 大部分都只适用于低水平程序线程,高水平同步线程使用channel通信更好一些

WaitGroup直译为等待组,其实就是计数器,只要计数器中有内容将一直阻塞

在Golang中WaitGroup存在于sync包中,在sync包中类型都是不应该被拷贝的.源码定义如下

// A WaitGroup waits for a collection of goroutines to finish. // The main goroutine calls Add to set the number of // goroutines to wait for. Then each of the goroutines // runs and calls Done when finished. At the same time, // Wait can be used to block until all goroutines have finished. // // A WaitGroup must not be copied after first use. type WaitGroup struct { noCopy noCopy ​ // 64-bit value: high 32 bits are counter, low 32 bits are waiter count. // 64-bit atomic operations require 64-bit alignment, but 32-bit // compilers do not ensure it. So we allocate 12 bytes and then use // the aligned 8 bytes in them as state. state1 [12]byte sema   uint32 }

Go语言标准库中WaitGroup只有三个方法

Add(delta int)表示向内部计数器添加增量(delta),其中参数delta可以是负数

Done()表示减少WaitGroup计数器的值,应当在程序最后执行.相当于Add(-1)

Wait()表示阻塞直到WaitGroup计数器为0

type WaitGroup  func (wg *WaitGroup) Add(delta int)  func (wg *WaitGroup) Done()  func (wg *WaitGroup) Wait()

代码示例

使用WaitGroup可以有效解决goroutine未执行完成主协程执行完成,导致程序结束,goroutine未执行问题

package main ​ import (   "fmt"   "sync" ) ​ var wg sync.WaitGroup ​ func main() { ​   for i := 1; i <= 3; i++ {      wg.Add(1)      go demo(i)   }   //阻塞,知道WaitGroup队列中所有任务执行结束时自动解除阻塞   fmt.Println("开始阻塞")   wg.Wait()   fmt.Println("任务执行结束,解除阻塞") ​ } ​ func demo(index int) {   for i := 1; i <= 5; i++ {      fmt.Printf("第%d次执行,i的值为:%d\n", index, i)   }   wg.Done() }

 

五.互斥锁

Go语言中多个协程操作一个变量时会出现冲突的问题

go run -race 可以查看竞争

可以使用sync.Mutex对内容加锁

互斥锁的使用场景

多个goroutine访问同一个函数(代码段)

这个函数操作一个全局变量

为了保证共享变量安全性,值合法性

未使用锁,程序会对资源num进行抢夺,可能导致数据出现错误

package main import ( "fmt" "sync" ) var ( num=100 wg sync.WaitGroup ) func demo(){ for i:=0;i<10 ;i++ { num=num-1 } wg.Done() } func main() { for i:=0;i<10 ;i++ { wg.Add(1) go demo() } wg.Wait() fmt.Println(num) } //此时未使用锁,可能出现数据错误

添加互斥锁

 

package main import ( "fmt" "sync" ) var ( num=100 wg sync.WaitGroup m sync.Mutex ) func demo(){ m.Lock() for i:=0;i<10 ;i++ { num=num-1 } m.Unlock() wg.Done() } func main() { for i:=0;i<10 ;i++ { wg.Add(1) go demo() } wg.Wait() fmt.Println(num) }

 在idea中运行控制台查询是否有竞争资源

使用互斥锁模拟售票窗口

package main ​ import (   "fmt"   "sync"   "time"   "math/rand" ) ​ var (   //票数   num = 100   wg  sync.WaitGroup   //互斥锁   mu sync.Mutex ) ​ func sellTicker(i int) {   defer wg.Done()   for {      //加锁,多个goroutine互斥      mu.Lock()      if num >= 1 {         fmt.Println("第", i, "个窗口卖了", num)         num = num - 1     }      //解锁      mu.Unlock() ​      if num <= 0 {         break     }      //添加休眠,防止结果可能出现在一个goroutine中      time.Sleep(time.Duration(rand.Int63n(1000) * 1e6))   } ​ } ​ func main() {   //设置随机数种子   rand.Seed(time.Now().UnixNano())   //计算器的起始值和票数相同   wg.Add(4)   go sellTicker(1)   go sellTicker(2)   go sellTicker(3)   go sellTicker(4)   wg.Wait() ​   fmt.Println("所有票卖完") }

 

六.RWMutex读写锁

RWMutex 源码如下

// There is a modified copy of this file in runtime/rwmutex.go. // If you make any changes here, see if you should make them there. ​ // A RWMutex is a reader/writer mutual exclusion lock. // The lock can be held by an arbitrary number of readers or a single writer. // The zero value for a RWMutex is an unlocked mutex. // // A RWMutex must not be copied after first use. // // If a goroutine holds a RWMutex for reading and another goroutine might // call Lock, no goroutine should expect to be able to acquire a read lock // until the initial read lock is released. In particular, this prohibits // recursive read locking. This is to ensure that the lock eventually becomes // available; a blocked Lock call excludes new readers from acquiring the // lock. type RWMutex struct { w           Mutex  // held if there are pending writers writerSem   uint32 // semaphore for writers to wait for completing readers readerSem   uint32 // semaphore for readers to wait for completing writers readerCount int32  // number of pending readers readerWait  int32  // number of departing readers }

Go语言标准库中API如下

type RWMutex  func (rw *RWMutex) Lock()//禁止其他协程读写  func (rw *RWMutex) Unlock()  func (rw *RWMutex) RLock()//禁止其他协程写入,只能读取  func (rw *RWMutex) RUnlock()  func (rw *RWMutex) RLocker() Locker

Go语言中的map不是线程安全的,多个goroutine同时操作会出现错误.

RWMutex可以添加多个读锁或一个写锁.读写锁不能同时存在.

map在并发下读写就需要结合读写锁完成

互斥锁表示锁的代码同一时间只能有一个人goroutine运行,而读写锁表示在锁范围内数据的读写操作

package main ​ import (   "fmt"   "sync"   "strconv" ) ​ func main() {   var rwm sync.RWMutex   m := make(map[string]string)   var wg sync.WaitGroup   wg.Add(10)   for i := 0; i < 10; i++ {      go func(j int) {         //没有锁在map时可能出现问题         rwm.Lock()         m["key"+strconv.Itoa(j)] = "value" + strconv.Itoa(j)         fmt.Println(m)         rwm.Unlock()         wg.Done()     }(i)   }   wg.Wait()   fmt.Println("程序结束") }

 channel

线程通信在每个编程语言中都是重难点,在Golang中提供了语言级别的goroutine之间通信:channel

channel不同的翻译资料叫法不一样.常见的几种叫法

管道

信道

通道

channel是进程内通信方式,每个channel只能传递一个类型的值.这个类型需要在声明channel时指定

channel在Golang中主要的两个作用

同步

通信

Go语言中channel的关键字是chan

声明channel的语法

var 名称 chan 类型 var 名称 chan <- 类型 //只写 var 名称 <- chan 类型//只读 名称:=make(chan int) //无缓存channel 名称:=make(chan int,0)//无缓存channel 名称:=make(chan int,100)//有缓存channel

操作channel的语法:(假设定义一个channel名称为ch)

ch <- 值 //向ch中添加一个值 <- ch //从ch中取出一个值 a:=<-ch //从ch中取出一个值并赋值给a a,b:=<-ch//从ch中取出一个值赋值给a,如果ch已经关闭或ch中没有值,b为false

代码示例

简单无缓存通道代码示例

此代码中如果没有从channel中取值c,d=<-ch语句,程序结束时go func并没有执行

下面代码示例演示了同步操作,类似与WaitGroup功能,保证程序结束时goroutine已经执行完成

向goroutine中添加内容的代码会阻塞goroutine执行,所以要把ch<-1放入到goroutine有效代码最后一行

无论是向channel存数据还是取数据都会阻塞

close(channel)关闭channel,关闭后只读不可写

package main ​ import (   "fmt" ) ​ func main() {   ch := make(chan int)   go func() {      fmt.Println("进入goroutine")      // 添加一个内容后控制台输出:1 true      //ch<-1 ​      //关闭ch控制台输出:0 false      close(ch)   }()   c, d := <-ch   fmt.Println(c, d)   fmt.Println("程序执行结束") }

使用channel实现goroutine之间通信

channel其实就是消息通信机制实现方案,在Golang中没有使用共享内存完成线程通信,而是使用channel实现goroutine之间通信.

package main ​ import (   "fmt" ) ​ func main() {   //用于goroutine之间传递数据   ch := make(chan string)   //用于控制程序执行   ch2 := make(chan string)   go func() {      fmt.Println("执行第一个goroutine,等待第二个goroutine传递数据")      content := <-ch      fmt.Println("接收到的数据为:", content)      ch2 <- "第一个"   }()   go func() {      fmt.Println("进入到第二个,开始传递数据")      ch <- "内容随意"      close(ch)      fmt.Println("发送数据完成")      ch2 <- "第二个"   }()   result1 := <-ch2   fmt.Println(result1, "执行完成")   result2 := <-ch2   fmt.Println(result2, "执行完成")   fmt.Println("程序执行结束") }

可以使用for range获取channel中内容

不需要确定channel中数据个数

func main() {   ch:=make(chan string)   ch2:=make(chan int)   go func() {      for i:=97;i<97+26;i++{         ch <- strconv.Itoa(i)     }      ch2<-1   }() ​   go func() {      for c := range ch{         fmt.Println("取出来的",c)     }   }()   <-ch2   fmt.Println("程序结束") }

channel是安全的.多个goroutine同时操作时,同一时间只能有一个goroutine存取数据

package main ​ import (   "time"   "fmt" ) ​ func main() {   ch := make(chan int) ​   for i := 1; i < 5; i++ {      go func(j int) {         fmt.Println(j, "开始")         ch <- j         fmt.Println(j, "结束")     }(i)   } ​   for j := 1; j < 5; j++ {      time.Sleep(2 * time.Second)      <-ch   } }

 

死锁

在主goroutine中向无缓存channel添加内容或在主goroutine中向channel添加内容且添加内容的个数已经大于channel缓存个数就会产生死锁

fatal error : all goroutines are asleep -deadlock!

死锁:在程序中多个进程(Golang中goroutine)由于相互竞争资源而产生的阻塞(等待)状态,而这种状态一直保持下去,此时称这个线程是死锁状态

在Golang中使用无缓存channel时一定要注意.以下是一个最简单的死锁程序

主协程中有ch<-1,无缓存channel无论添加还是取出数据都会阻塞goroutine,当前程序无其他代码,主goroutine会一直被阻塞下去,此时主goroutine就是死锁状态

func main() {   ch := make(chan int)   ch <- 1 }

而下面代码就不会产生死锁

通过代码示例可以看出,在使用无缓存channel时,特别要注意的是在主协程中有操作channel代码

package main ​ import (   "time"   "fmt" ) ​ func main() {   ch := make(chan int)   go func() {      ch <- 1      fmt.Println("执行goroutine")   }()   time.Sleep(5e9)   fmt.Println("程序执行结束") }

有缓存通道

创建一个有缓存通道

func main() {   ch := make(chan int, 3) //缓存大小3,里面消息个数小于等于3时都不会阻塞goroutine   ch <- 1   ch <- 2   ch <- 3   ch <- 4 //此行出现死锁,超过缓存大小数量 }

在Golang中有缓存channel的缓存大小是不能改变的,但是只要不超过缓存数量大小,都不会出现阻塞状态

package main ​ import "fmt" ​ func main() {   ch := make(chan int, 3) //缓存大小3,里面消息个数小于等于3时都不会阻塞goroutine   ch <- 1   fmt.Println(<-ch)   ch <- 2   fmt.Println(<-ch)   ch <- 3   ch <- 4   fmt.Println(len(ch))//输出2,表示channel中有两个消息   fmt.Println(cap(ch))//输出3,表示缓存大小总量为3 }

select简介

Golang中select和switch结构特别像,但是select中case的条件只能是I/O

select 的语法(condition是条件)

select{  case condition:  case condition:  default: }

select执行过程:

每个case必须是一个IO操作

哪个case可以执行就执行哪个

多个case都可以执行,随机执行一个

所有case都不能执行时,执行default

所有case都不能执行,且没有default,将会阻塞

代码示例

func main() {   runtime.GOMAXPROCS(1)   ch1 := make(chan int, 1)   ch2 := make(chan string, 1)   ch1 <- 1   ch2 <- "hello"   select {   case value := <-ch1:      fmt.Println(value)   case value := <-ch2:      fmt.Println(value)   } }

select多和for循环结合使用,下面例子演示出了一直在接收消息的例子

package main ​ import ( "fmt" ) ​ func main() { ch := make(chan int) for i := 1; i <= 5; i++ { go func(arg int) { ch <- arg }(i) }  //如果是一直接受消息,应该是死循环for{},下面代码中是明确知道消息个数 for i := 1; i <= 5; i++ { select { case c := <-ch: fmt.Println("取出数据", c) default: //没有default会出现死锁 } } fmt.Println("程序执行结束") } ​

 

break可以对select生效,如果for中嵌套select,break选择最近结构

 

 GC

GC英文全称 garbage collector

Go语言GC是相对C/C++语言非常重要的改进

一些常用GC算法

引用计算法.当对象被引用时计算器加一.不被引用计数器减一

PHP和Object-C使用

相互引用无法回收

计数增加消耗

Mark And Sweep 标记和清除算法.停止程序运行,递归遍历对象,进行标记.标记完成后将所有没有引用的对象进行清除

由于标记需要停止程序(Stop the world),当对象特别多时,标记和清除过程比较耗时(可能几百毫秒),很难接受

三色标记法:是Mark And Sweep的改进版.从逻辑上分为白色区(未搜索),灰色区(正搜索),黑色区(已搜索).灰色区内容是子引用没有进行搜索,黑色区表示子引用存在

分代收集.一般情况都有三代,例如java中新生代,老年代,永久代.当新生代中带有阈值时会把对象放入到老年代,相同道理老年代内容达到阈值会放入到永久代

Go语言中的GC

Go语言中采用Stop The World方式

Golang每个版本基本上都会对GC进行优化,从Golang1.5开始支持并发(concurrent )收集,从1.8版本已经把STW时间优化到了100微妙,通常只需要10微妙以下.且在1.10版本时再次优化减少GC对CPU占用

Go语言中GC是自动运行的,在下列情况下会触发GC

当需要申请内存时,发现GC是上次GC两倍时会触发

每2分钟自动运行一次GC

GC调优

小对象复用,局部变量尽量少声明,多个小对象可以放入到结构体,方便GC扫描

少用string的”+”

在runtime包下mgc.go中明确的说明了Golang的GC的解释

// Garbage collector (GC). // // The GC runs concurrently with mutator threads, is type accurate (aka precise), allows multiple // GC thread to run in parallel. It is a concurrent mark and sweep that uses a write barrier. It is // non-generational and non-compacting. Allocation is done using size segregated per P allocation // areas to minimize fragmentation while eliminating locks in the common case. // // The algorithm decomposes into several steps. // This is a high level description of the algorithm being used. For an overview of GC a good // place to start is Richard Jones' gchandbook.org. // // The algorithm's intellectual heritage includes Dijkstra's on-the-fly algorithm, see // Edsger W. Dijkstra, Leslie Lamport, A. J. Martin, C. S. Scholten, and E. F. M. Steffens. 1978. // On-the-fly garbage collection: an exercise in cooperation. Commun. ACM 21, 11 (November 1978), // 966-975. // For journal quality proofs that these steps are complete, correct, and terminate see // Hudson, R., and Moss, J.E.B. Copying Garbage Collection without stopping the world. // Concurrency and Computation: Practice and Experience 15(3-5), 2003. // // 1. GC performs sweep termination. // //   a. Stop the world. This causes all Ps to reach a GC safe-point. // //   b. Sweep any unswept spans. There will only be unswept spans if //   this GC cycle was forced before the expected time. // // 2. GC performs the "mark 1" sub-phase. In this sub-phase, Ps are // allowed to locally cache parts of the work queue. // //   a. Prepare for the mark phase by setting gcphase to _GCmark //   (from _GCoff), enabling the write barrier, enabling mutator //   assists, and enqueueing root mark jobs. No objects may be //   scanned until all Ps have enabled the write barrier, which is //   accomplished using STW. // //   b. Start the world. From this point, GC work is done by mark //   workers started by the scheduler and by assists performed as //   part of allocation. The write barrier shades both the //   overwritten pointer and the new pointer value for any pointer //   writes (see mbarrier.go for details). Newly allocated objects //   are immediately marked black. // //   c. GC performs root marking jobs. This includes scanning all //   stacks, shading all globals, and shading any heap pointers in //   off-heap runtime data structures. Scanning a stack stops a //   goroutine, shades any pointers found on its stack, and then //   resumes the goroutine. // //   d. GC drains the work queue of grey objects, scanning each grey //   object to black and shading all pointers found in the object //   (which in turn may add those pointers to the work queue). // // 3. Once the global work queue is empty (but local work queue caches // may still contain work), GC performs the "mark 2" sub-phase. // //   a. GC stops all workers, disables local work queue caches, //   flushes each P's local work queue cache to the global work queue //   cache, and reenables workers. // //   b. GC again drains the work queue, as in 2d above. // // 4. Once the work queue is empty, GC performs mark termination. // //   a. Stop the world. // //   b. Set gcphase to _GCmarktermination, and disable workers and //   assists. // //   c. Drain any remaining work from the work queue (typically there //   will be none). // //   d. Perform other housekeeping like flushing mcaches. // // 5. GC performs the sweep phase. // //   a. Prepare for the sweep phase by setting gcphase to _GCoff, //   setting up sweep state and disabling the write barrier. // //   b. Start the world. From this point on, newly allocated objects //   are white, and allocating sweeps spans before use if necessary. // //   c. GC does concurrent sweeping in the background and in response //   to allocation. See description below. // // 6. When sufficient allocation has taken place, replay the sequence // starting with 1 above. See discussion of GC rate below.
最新回复(0)