进阶-1

作者: hellomyshadow | 来源:发表于2019-09-26 06:53 被阅读0次

    复习

    1. Go语言保留了C语言中的指针,但又有所不同;
      1. 默认值为 nil
      2. 操作符 & 取变量地址,* 通过指针访问目标对象;
      3. 不支持指针运算,不支持 -> 运算符,直接使用 . 访问目标成员。
    2. 32为系统:4G
      1. 3G - 4Gkernel占用
      2. 0 - 3G:用户使用,可分为代码区、只读数据区、数据区、未初始化数据区、堆、栈、其他
      3. :默认大小为1M,Windows系统上可扩展到8M,考虑到栈区内存很小,所以由操作系统管理栈的释放;
      4. :通常内存空间在 1G 以上,在程序合理的情况下,GC机制能有效控制释放;
    3. 切片并不是数组或数组指针,它是通过内部指针和相关属性引用数组片段,以实现变长放案;
      1. 切片并不是真正意义上的动态数组,而是一个引用类型,数组是值传递;
      2. 切片是一种数据结构体,总是指向一个底层array
      3. 切片创建的多种方式
      slice := []int{1, 2, 3}
      slice := make([]int, 长度, 容量)
      slice := make([]int, 长度)  // 省略容量时,容量==长度
      
    4. make()函数只能用于创建 切片、Map、Channel
    5. 相同结构体类型:成员变量的类型、个数、顺序都完全一致;相同结构体变量之间可以直接赋值;
      1. 结构体变量的地址 == 结构体首个元素的地址
      2. 结构体是值传递,内存消耗大,效率低下;
    6. unsave.Sizeof(xxx):查看占用的字节数;
      1. Go语言的布尔类型是用 0/1 模拟的,所以占用 1 个字节;
      2. 不管何种类型的指针,在64位操作系统下,占用字节数恒为8,所以在传递结构体时,通常使用 指针 实现地址传递。
    7. 函数不能返回局部变量的地址值(指针)!因为局部变量保存在栈中,栈区释放之后,局部变量不受系统保护,随时可能把内存分配给其他程序;
    8. 虚拟地址映射:我们常说的内存地址其实虚拟地址,因为操作系统并不是希望直接把物理内存地址暴露给用户;
      1. 堆区的地址是连续的,而真正的物理存储却并不是连续的内存区间,目的是为了让内存得到充分的利用;
      2. 磁盘的最小单位是扇区,内存的最小单位是
    9. 当一个进程启动时,系统会自动打开三个文件:标准输入、标准输出、标准错误 --- stdin、stdout、stderr ,进程结束,系统会自动关闭它们。

    并发

    1. 并行与并发
      1. 并行:借助多核CPU实现,是真并行;
      2. 并发:宏观的用户体验上,程序是并行的;微观上,其实是轮换使用CPU时间轮片,飞快地切换,是假并行。
    2. 程序与进程
      1. 程序:编译成功得到的二进制文件,占用磁盘空间;
      2. 进程:运行起来地程序,占用系统资源;
    3. 进程状态
      1. 5种基本状态:初始态、就绪态、运行态、挂起/阻塞态、终止/停止态
      2. 初始态为进程准备阶段,常与就绪态结合考虑;
    进程状态.png
    1. 进程并发
      1. 系统开销比较大,占用资源比较多,开启进程数量比较少;
      2. 父进程通过fork创建子进程,子进程再创建新的进程,且父进程永远无法预测子进程什么时候结束;
      3. 在 unix/linux 系统下,还会产生孤儿进程和僵尸进程;
      4. 孤儿进程:父进程先于子进程结束,则子进程成为孤儿进程,子进程地父进程成为init进程,称为init进程领养孤儿进程;
      5. 僵尸进程:进程终止,父进程尚未回收,子进程残留资源存放于内核种,变成僵尸进程;
      6. Windows系统地进程和Linux地进程有所不同,它从不执行任何东西,只是为线程提供执行环境,由线程负责执行包含在进程地址空间中的代码;在创建一个进程时,系统也会自动为其创建第一个线程,称为主线程。
    2. 线程并发
      1. 进程是最小的系统资源分配单位
      2. 线程:LWP 轻量级的进程,最小的执行单位,CPU分配时间轮片的对象
    3. 线程同步
      1. 线程同步机制:锁
      2. 互斥锁:建议锁,拿到锁以后才能访问数据,没有拿到锁的线程阻塞等待;
      3. 读写锁:一把具有读属性和写属性的锁,写独占,读共享,且写的优先级最高。
    4. 协程并发:轻量级线程,占用系统资源最少
      1. 协程最大的优势在于轻量级,可以轻松创建数万个而不导致系统资源衰竭,而线程和进程通常很难达到1万个;
      2. 一个线程可以有任意多个协程,但某一个时刻只能有一个协程在运行,多个协程共享该线程分配到的计算机资源;
      3. 在协程中,调用一个任务就像调用一个函数一样,消耗的系统资源最少,但能达到进程/线程并发的相同效果。

    Goroutine

    1. Go语言为并发编程而内置的上层API基于顺序通信进程模型CSP,这就意味着显式锁是可以避免的,因为Go通过相对安全的通道发送和接收数据以实现同步,大大简化了并发程序的编写;
    2. Go语言中的并发程序主要通过两种手段实现:GoroutineChannel
    3. Goroutine是Go语言并行设计的核心,也成为Go程,其本质就是协程,十几个Goroutine在底层可能只有五六个线程,Go语言内部实现了Goroutine之间的内存共享;
    4. 执行Goroutine只需极少的栈内存(4-5KB,会根据相应的数据伸缩),可同时运行成千上万个并发任务;
    5. 创建:只需要在函数调用语句前添加 go 关键字,就可以创建并发执行单元,调度器会自动将其安排到合适的系统线程上执行;
        func sing()  {
            for i:=0; i<10; i++ {
                fmt.Println("sing: ", i)
                time.Sleep(10)
            }
        }
        func dance()  {
            for i:=0; i<10; i++ {
                fmt.Println("dance: ", i)
                time.Sleep(10)
            }
        }
        func main() {
            go sing()
            go dance()
            for  {
                ;
            }
        }
    
    1. 当一个程序启动时,主函数main()在一个单独的Goroutine中运行,又称为main Goroutine(主Go程),在主Go程中开启的Go程称为子Go程;
    2. 特性:主Go程一旦结束,进程也就结束了,子Go程也就随之退出了!

    runtime包

    1. runtime.Gosched():用于让出CPU时间片,让出当前Goroutine的执行权限,调度器安排其他等待的任务执行,并在下次重新获得CPU时间轮片时,从之前出让CPU的位置继续向下执行!
      1. 虽然runtime.Gosched()time.Sleep()都能让出时间片,但它们的量级是不同的;
      2. runtime.Gosched()让时间片之后,会立即加入等待区,而time.Sleep()则必须等待休眠时间到了之后才会加入等待区。
    2. runtime.Goexit():立即终止当前 Goroutine(非主Go程)的执行,调度器确保所有已注册 defer 延迟调用被执行;
      1. return 只是针对一个函数的结束,其后面注册的 defer 也不会再执行,因为还没来得及注册;
      2. Goexit()针对的是整个当前的Go程,一旦执行,当前所在的Go程立即终止,其后的代码不在执行,包括尚未注册的defer
    3. GOMAXPROCS(n):设置可同时执行(并行)的CPU核数的最大值,并返回之前的值;
      n := runtime.GOMAXPROCS(1)  // 将CPU设置为单核
      
      1. 首次调用返回默认值,如果 n < 1 ,则设置失败,不会更改当前值;
      2. 默认会使用CPU的全核工作,但也会受电源等元器件的影响,为了保护系统服务正常工作,而选择降频工作,即不启用全核。
    4. NumCPU():查询本地机器的逻辑CPU个数;
    5. GC():手动执行一次垃圾回收;

    Channel

    1. Channel是Go语言中的一个核心类型,可以看成管道,分为两个端:写端(传入端)、读端(传出端)
      1. 并发核心单元通过Channel就可以发送/接收数据进行通讯,在一定程度上进一步降低编程难度;
      2. Channel是一个数据类型,主要用于解决协程的同步问题以及协程之间的数据共享(数据传递)问题;
      3. Goroutine运行在相同的地址空间,因此访问共享内存必须做好同步,Goroutine奉行通过通信来共享内存,而不是共享内存来通信;
      4. 引用类型Channel可用于多个Goroutine的通讯,其内部实现了同步,确保并发安全。
    2. ChannelMap类似,由 make() 创建底层数据结构,且它是引用类型,默认值为nil,遵循地址传递;
      1. 声明
      var channel chan int   // 声明一个Channel通道,默认可读可写
      var read <-chan int   // 声明一个只读的Channel通道
      var write chan<- int   // 声明一个只写的Channel通道
      
      1. 创建
      channel := make(chan Type, capacity)  //创建一个Channel
      
      1. chan是声明Channel的关键字,Type 表示Channel收发数据的类型;
      2. capacity可省略,默认值为0,此时Channel是无缓冲阻塞读写的;当capacity > 0时,Channel是有缓冲非阻塞的,直到写满capacity个元素才阻塞写入。
    3. Channel通过操作符 <- 来收发数据
      channel <- value  // 写端,发送 value 到 channel,value 的数据类型与定义类型保持一致
      <-channel     // 接收并将其丢弃,读端
      x := <-channel   // 从 channel 中接收数据,并赋值给x,读端
      x, ok := <-channel  // ok 可以检查通道是否已关闭,或者是否为空,读端
      
      1. len()获取的是Channel中剩余未读取的数据个数;
      2. cap()获取Channel的容量。
    4. For example
      var channel = make(chan int)  // 创建 Channel 通道
      func sing()  {
          printer("hello")
          channel <- 8   // 执行完打印操作之后,再向Channel中发送数据
      }
      func dance()  {
          <- channel   // 阻塞、等待接收Channel中的数据
          printer("world")
      }
      func printer(s string) {  // 打印操作,每次睡眠 1s
          for _,ch:=range s {
              fmt.Printf("%c", ch)
              time.Sleep(1000*time.Millisecond)
          }
      }
      func main() {
          go sing()
          go dance()
          for  {
              ;
          }
      }
      // helloworld
      
    5. 无缓冲阻塞Channel:同步通信
      1. 无缓冲通道是指在接收前没有能力保存任何值的通道,通道容量为0
      2. 它要求写端和读端同时准备好,才能完成收发操作,否则先执行的一端Go程就会阻塞等待,即读写同步!比如打电话
          func main() {
              ch := make(chan string)
              go func() {
                  for i:=0; i<5; i++ {
                      fmt.Println(i)
                      time.Sleep(1000*time.Millisecond)
                  }
                  <- ch  // 循环结束才会消费通道中的数据
              }()
              ch <- "hello"  // 写端-主Go程陷入阻塞,等待读端消费数据,然后才能重新加入CPU时间轮片
              fmt.Println("main end")
          }
          // 0 1 2 3 4    main end
      
      1. 这种对通道进行发送和接收的交互行为本身就是同步的,其中任意一个操作都无法离开另一个操作单独存在,否则就会造成死锁;
      2. 阻塞:由于某种原因导致数据没有到达,当前协程/线程持续处于等待状态,直到条件满足,才能解除阻塞;
      3. 同步:在两个或多个协程/线程间,保持数据内容的一致性。
    6. 有缓冲Channel:异步通信
      1. 缓冲区可以进行数据存储,达到容量上限之后才会阻塞,具备异步能力!比如发短信
          func main() {
              ch := make(chan int, 3)
              go func() {
                  for i:=0; i<8; i++ {
                      fmt.Println("func: ", i)
                      ch <- i
                  }
              }()
              for i := 0; i < 8; i++ {
                  n := <-ch
                  fmt.Println("main: ", n)
                  time.Sleep(1*time.Second)  // 睡眠 1s
              }
              fmt.Println("main end")
          }
      
    7. close(ch):内置函数,关闭Channel
      1. 一端关闭了通道,另一端是可以判断通道是否已经关闭的;
          if n, ok := <-ch; ok {
      
          }
      
      1. 如果对端已经关闭了通道,okfalse,主要也是关闭发送端;
      2. 数据未发送完,不应该关闭通道;已关闭的通道不能再发送数据,否则报异常:send on closed channel
      3. 写端已关闭了通道,对于无缓冲的Channel,读端还可以从中读取到数据,结果为数据类型的默认值;对于有缓冲的Channel,如果缓冲区内还有数据,则先读数据,读完之后还可以继续读,结果也是数据类型的默认值;
    8. for-range 可以遍历通道,获取其中的数据
      for n := range channel {
      }
      

    单向Channel

    1. 默认的Channel是双向的
    2. 单向写Channelvar send chan <- int
    3. 单向读Channelvar recv <-chan int
    4. 双向Channel可以隐式转换为任意一种同类型的单向Channel,反之则不行!
      ch := make(chan int)
      var send chan <- int = ch
      send <- 89
      
      1. Channel一定是成对出现的,只有单向Channel会造成死锁;
      2. 不能对单向写Channel进行读取操作。
    5. 单向Channel传参
      func send(s chan <- int)  {  // 单向写Channel
          s <- 89
          close(s)
      }
      func recv(r <- chan int)  {  // 单向读Channel
          n := <- r
          fmt.Println("recv: ", n)
      }
      func main() {
          ch := make(chan int)  // 双向Channel
          go func() {
              send(ch)
          }()
          recv(ch)  // 89
      }
      

    生产者消费者模型

    生产者 -> 缓冲区 -> 消费者

    1. 缓冲区的作用
      1. 解耦:降低生产者和消费者之间的耦合度;
      2. 并发:生产者和消费者的数量不对等时,保持正常通信;
      3. 缓存:生产者和消费者的数据处理速度不一致时,暂存数据;
    2. Channel可以作为缓冲区,实现生产者(Go程-1)与消费者(Go程-2)的通信

    定时器

    1. time.Timer 是一个定时器,代表未来的一个单一事件,可以设置等待时间;
      type Timer struct {
          C <-chan Time
          r runtimeTimer
      }
      
      1. 它提供一个Channel,在定时时间到达之前,没有数据写入通道 C 就会一直阻塞;定时时间到了之后,系统会自动向 C 通道中写入当前时间,阻塞即刻被解除;
      2. time.NewTimer(d):创建定时器,传入定时时长,返回值为*Timer
      t := time.NewTimer(time.Second*2)  //定时2s
      now := <- t.C  //从 Timer 中读通道C 的数据,也就是当前时间
      
      1. 设置了定时器后,当前Go程会阻塞,等待定时时间到,返回Timer指针;
      2. 应从通道中读出数据,否则会一直阻塞;
      3. t.Stop():停止定时器;
      4. t.Reset(d):重置定时时长;
    2. timer.Sleep() 属于单纯的等待;
    3. timer.After() 也可以实现定时等待,返回一个Time类型的Channel
      now <- timer.After(time.Second*2)  // 从通道中取出当前时间
      
    4. 周期定时:timer.NewTicker(d),系统每隔 d 时间自动向通道中写入一次当前系统时间,返回*Ticker
      type Ticker struct {
          C <-chan Time
          r runtimeTimer
      }
      
      func main() {
          t := time.NewTicker(time.Second)  // 设置周期为 1s
          go func() {
              for {
                  now := <-t.C
                  fmt.Println(now)
              }
          }()
          for {
              ;
          }
      }
      

    相关文章

      网友评论

          本文标题:进阶-1

          本文链接:https://www.haomeiwen.com/subject/cbheuctx.html