当前位置:   article > 正文

go面试题目收集_gethashitem的第二个参数是啥

gethashitem的第二个参数是啥

go面试

slice

底层数据结构
type slice struct {
	array unsafe.Pointer
	len int
	cap int
}
  • 1
  • 2
  • 3
  • 4
  • 5
slice的创建
1)sliceOne := make([]int, 0, 10)
通过Make创建,可指定创建的切片的长度和容量。如果不指定容量,那么容量就等于长度。
(2)sliceTwo := sliceOne[2:4] 
这种方式是基于其他切片或数据创建容量,长度为创建切片是指定的结束-起始位置, = 4-2=2;容量就等于切片的容量-起始位置=10-2=8.且两个切片共享同一个数据区;
(3)sliceThree := []int{1,2}
这种方式直接字面量创建,长度和容量都等于其元素个数
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
slice的追加

使用append向Slice追加元素时, 如果Slice空间不足, 将会触发Slice扩容, 扩容实际上重新一配一块更大的内存, 将原Slice数据拷贝进新Slice, 然后返回新Slice, 扩容后再将数据追加进去。
扩容容量的选择遵循以下规则:

  • 如果原Slice容量小于1024, 则新Slice容量将扩大为原来的2倍
  • 如果原Slice容量大于等于1024, 则新Slice容量将扩大为原来的1.25倍
slice的拷贝

使用copy()内置函数拷贝两个切片,但是需要注意的是,copy 会将源切片的数据逐个拷贝到目的切片指向的数组中, 拷贝数量取两个切片长度的最小值。copy不会扩容,只有append才会扩容。
基于以上切片特性。编程过程需要注意:

  1. 创建切片时可跟据实际需要预分配容量, 尽量避免追加过程中扩容操作, 有利于提升性能;
  2. 切片拷贝时需要判断实际拷贝的元素个数
  3. 谨慎使用多个切片操作同一个数组, 以防读写冲突

channel

channel是go语言协程间通信的管道。channel可用于协程同步,也可以协程间可以传递各种消息数据。

底层数据结构
type hchan struct {
	qcount uint
	dataqsiz uint
	buf unsafe.Pointer
	elemsize uint16
	closed uint32
	elemtype *_type
	sendx uint
	recvx uint
	recvq waitq
	sendq waitq
 
	lock mutex
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
channel创建
ch := make(chan int, 1)
  • 1
向channel写数据的过程
  1. 如果等待接收队列recvq不为空, 说明缓冲区中没有数据或者没有缓冲区, 此时直接从recvq取出G,并把数据写入, 最后把该G唤醒, 结束发送过程;
  2. 如果接受队列recvq为空,且缓冲区中有空余位置, 将数据写入缓冲区, 结束发送过程;
  3. 如果接受队列recvq为空,缓冲区中没有空余位置, 将待发送数据写入G, 将当前G加入sendq, 进入睡眠, 等待被读goroutine唤醒;
从一个channel读数据简单过程
  1. 如果等待发送队列sendq不为空, 且没有缓冲区, 直接从sendq中取出G, 把G中数据读出, 最后把G唤醒, 结束读取过程;
  2. 如果等待发送队列sendq不为空, 此时说明缓冲区已满, 从缓冲区中首部读出数据, 把G中数据写入缓冲区尾部, 把G唤醒, 结束读取过程;
  3. 如果缓冲区中有数据, 则从缓冲区取出数据, 结束读取过程;
  4. 将当前goroutine加入recvq, 进入睡眠, 等待被写goroutine唤醒;

map

map底层使用哈希表来实现的,哈希过程产生冲突使用的冲突解决办法是链地址法。
除此之外常见的冲突解决办法还有:开放寻址法,链地址法,再次哈希法,创建一个公共溢出区等几种方法。
python中的字典底层依靠哈希表(hash table)实现, 使用开放寻址法解决冲突,java和go都采用链地址法来解决哈希冲突。

底层结构
type hmap struct {
	count int // 当前保存的元素个数
	...
	B uint8 // 指示bucket数组的大小
	...
	buckets unsafe.Pointer // bucket数组指针, 数组的大小为2^B
	...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

go的哈希表实现底层数据结构中有一个count标识当前元素个数,一个B成员标识桶的个数,还有一个buckes是一个指针指向桶数组。

map的创建
1. make方式创建
hash := make(map[string]int, 10)
  • 1
2. 字面量方式创建
hash := map[string]int{
	"1": 2,
	"3": 4,
	"5": 6,
}
  • 1
  • 2
  • 3
  • 4
  • 5
map解决冲突的方法

使用链地址法:当多个键被哈希到了同一个bucket时,也就是产生了哈希冲突。由于每个bucket可以存放8个键值对, 所以同一个bucket存放超过8个键值对时就会创建一个桶, 用链表的方式将bucket关联起来。

map的扩容

因为不能放任它无休止的冲突下去,无休止冲突的话会影响读写性能,于是引入了负载因子的概念,计算方式为:

负载因子=键数量/桶数量,当负载因子达到指定的值就会进行扩容操作。

go语言中的哈希表触发扩容的条件有两个:

  1. 负载因子 > 6.5时, 也即平均每个bucket存储的键值对达到6.5个
  2. overflow数量 > 2^15时, 也即overflow数量超过32768时
第一种情况负载因子过大,使用增量扩容。

当负载因子过大时, 就新建一个bucket, 新的bucket长度是原来的2倍, 然后旧bucket数据搬迁到新的bucket。
考虑到如果map存储了数以亿计的key-value, 一次性搬迁将会造成比较大的延时, Go采用逐步搬迁策略, 即每次访问map时都会触发一次搬迁, 每次搬迁2个键值对。

第二种overflow数量过多,使用等量扩容。

所谓等量扩容, 实际上并不是扩大容量, buckets数量不变, 重新做一遍类似增量扩容的搬迁动作, 把松散的键值对重新排列一次, 以使bucket的使用率更高, 进而保证更快的存取。 在极端场景下, 比如不断的增删, 而键值对正好集中在一小部分的bucket, 这样会造成overflow的bucket数量增多, 但负载因子又不高, 从而无法执行增量搬迁的情况。

map查找过程
  1. 跟据key值算出哈希值
  2. 取哈希值低位与hmpa.B取模确定bucket位置
  3. 取哈希值高位在tophash数组中查询
  4. 如果tophash[i]中存储值也哈希值相等, 则去找到该bucket中的key值进行比较
  5. 当前bucket没有找到, 则继续从下个overflow的bucket中查找。
  6. 如果当前处于搬迁过程, 则优先从oldbuckets查找
map插入过程
  1. 跟据key值算出哈希值
  2. 取哈希值低位与hmap.B取模确定bucket位置
  3. 查找该key是否已经存在, 如果存在则直接更新值
  4. 如果没找到将key, 将key插入
map拓展知识(重要)

go语言的map数据结构并不是并发安全的。想要并发安全的使用map结构。
通常由几种方式:

  1. 为map加读写锁
  2. 使用concurrent-map(开源库)
  3. 使用sync.map

三者的区别在于第一种是为整个map加锁,加锁粒度较大。影响性能。
第二个使用开源库concurrent-map,原理是对map分段加锁。加锁粒度相对减少,性能相对第一个有所提高。
第三种是go1.9引入的官方库,使用了空间换时间策略,通过冗余的两个数据结构(read、dirty),实现加锁对性能的影响。

切片与数组对比

(1). 数组
  1. 数组是具有固定长度且拥有零个或者多个相同数据类型元素的序列。
  2. 数组的长度是数组类型的一部分,所以[3]int 和 [4]int 是两种不同的数组类型。
  3. 数组需要指定大小,不指定也会根据初始化的自动推算出大小,不可改变 ;
  4. 数组是值传递;
  5. 数组是内置(build-in)类型,是一组同类型数据的集合,它是值类型,通过从0开始的下标索引访问元素值。在初始化后长度是固定的,无法修改其长度。当作为方法的参数传入时将复制一份数组而不是引用同一指针。数组的长度也是其类型的一部分,通过内置函数len(array)获取其长度。
(2). 切片
  1. 切片表示一个拥有相同类型元素的可变长度的序列。

  2. 切片是一种轻量级的数据结构,它有三个属性:指针、长度和容量。

  3. 切块结构如下:

 type Slice struct {
        ptr unsafe.Pointer
        len int
        cap int
}
  • 1
  • 2
  • 3
  • 4
  • 5
  1. 切片不需要指定大小;

  2. 切片是地址传递;

  3. 切片可以通过数组来初始化,也可以通过内置函数make()初始化.初始化时len=cap,在追加元素时如果容量cap不足时将按len的2倍扩容

(3). 关系

一个底层数组可以对应多个slice,这些slice可以引用数组的任何位置,彼此之间的元素还可以重叠。

在go语言中,new和make的区别?

New

  1. new的作用是初始化一个指向类型的指针(*T)

  2. new函数是内建函数,函数定义:func new(Type) *Type

  3. 使用new函数来分配空间。传递给new 函数的是一个类型,不是一个值。返回值是 指向这个新分配的零值的指针。

make 的作用是为 slice,map 或 chan 初始化并返回引用(T)。

make函数是内建函数,函数定义:func make(Type, size IntegerType) Type

第一个参数是一个类型,第二个参数是长度,返回值是一个类型

make(T, args)函数的目的与new(T)不同。

它仅仅用于创建 Slice, Map 和 Channel,并且返回类型是 T(不是*T)的一个初始化的(不是零值)的实例。

Printf()、Sprintf()、Fprintf()函数的区别用法是什么?

都是把格式好的字符串输出,只是输出的目标不一样:

Printf(),是把格式字符串输出到标准输出(一般是屏幕,可以重定向)。

Printf()是和标准输出文件(stdout)关联的,Fprintf 则没有这个限制.

Sprintf(),是把格式字符串输出到指定字符串中,所以参数比printf多一个char*。那就是目标字符串地址。

Fprintf(),是把格式字符串输出到指定文件设备中,所以参数比printf多一个文件指针FILE*。主要用于文件操作。Fprintf()是格式化输出到一个stream,通常是到文件。

go语言中协程

  1. 协程和线程都可以实现程序的并发执行;

  2. 通过channel来进行协程间的通信;

  3. 只需要在函数调用前添加go关键字即可实现go的协程,创建并发任务;

  4. 关键字go并非执行并发任务,而是创建一个并发任务单元

map如何顺序读取

map不能顺序读取,是因为他是无序的,想要有序读取,首先的解决的问题就是,把key变为有序,所以可以把key放入切片,对切片进行排序,遍历切片,通过key取值。

go语言的同步锁

(1) 当一个goroutine获得了Mutex后,其他goroutine就只能乖乖的等待,除非该goroutine释放这个Mutex

(2) RWMutex在读锁占用的情况下,会阻止写,但不阻止读

(3) RWMutex在写锁占用情况下,会阻止任何其他goroutine(无论读和写)进来,整个锁相当于由该goroutine独占

channel特性

A. 给一个 nil channel 发送数据,造成永远阻塞

B. 从一个 nil channel 接收数据,造成永远阻塞

C. 给一个已经关闭的 channel 发送数据,引起 panic

D. 从一个已经关闭的 channel 接收数据,如果缓冲区中为空,则返回一个零值

E. 无缓冲的channel是同步的,而有缓冲的channel是非同步的

go语言触发异常的场景有哪些?

A. 空指针解析

B. 下标越界

C. 除数为0

D. 调用panic函数

说说进程、线程、协程之间的区别?

  1. 进程是资源的分配和调度的一个独立单元,而线程是CPU调度的基本单元;

  2. 同一个进程中可以包括多个线程;

  3. 进程结束后它拥有的所有线程都将销毁,而线程的结束不会影响同个进程中的其他线程的结束;

  4. 线程共享整个进程的资源(寄存器、堆栈、上下文),一个进程至少包括一个线程;

  5. 进程的创建调用fork或者vfork,而线程的创建调用pthread_create;

  6. 线程中执行时一般都要进行同步和互斥,因为他们共享同一进程的所有资源;

  7. 进程是资源分配的单位

    线程是操作系统调度的单位

    进程切换需要的资源很最大,效率很低

    线程切换需要的资源一般,效率一般

    协程切换任务资源很小,效率高

    多进程、多线程根据cpu核数不一样可能是并行的 也可能是并发的。协程的本质就是使用当前进程在不同的函数代码中切换执行,可以理解为并行。 协程是一个用户层面的概念,不同协程的模型实现可能是单线程,也可能是多线程。

  8. 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。(全局变量保存在堆中,局部变量及函数保存在栈中)

    线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是这样的)。

    协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。

  9. 一个应用程序一般对应一个进程,一个进程一般有一个主线程,还有若干个辅助线程,线程之间是平行运行的,在线程里面可以开启协程,让程序在特定的时间内运行。

  10. 协程和线程的区别是:协程避免了无意义的调度,由此可以提高性能,但也因此,程序员必须自己承担调度的责任,同时,协程也失去了标准线程使用多CPU的能力。

context包的用途

Context通常被译作上下文,它是一个比较抽象的概念,

其本质,是【上下上下】存在上下层的传递,上会把内容传递给下。

在Go语言中,程序单元也就指的是Goroutine

go语言的select机制

for {
    select {
        case <-chan1:
            //.....
        case chan2<-1:
            //....
        default:
            //都没成功,进入......
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

go里面提供了一个关键字select,通过select可以监听channel上的数据流动

A. select机制用来处理异步IO问题

B. select机制最大的一条限制就是每个case语句里必须是一个IO操作

C. golang在语言级别支持select关键字

防止channel超时机制

有时候会出现协程阻塞的情况,可以使用select来设置超时

func main() {
   c := make(chan int)
   o := make(chan bool)
   go func() {
      for {
         select {
         case v:= <-c:
            fmt.Println(v)
         //5秒钟自动关闭,避免长时间超时
         case <-time.After(5 * time.Second):
            fmt.Println("timeout")
            o<-true
            break
         }
      }
   }()
  //有值就主协程走,主协程走完就都没了
   <-o
   fmt.Println("程序结束")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

select可以用于什么,常用语gorotine的完美退出

golang 的 select 就是监听 IO 操作,当 IO 操作发生时,触发相应的动作

每个case语句里必须是一个IO操作,确切的说,应该是一个面向channel的IO操作

主协程如何等其余协程完再操作

使用channel进行通信,完成同步功能,context,select

waitgroup实现等待

sync.WaitGroup 内部是实现了一个计数器,它有三个方法

  • Add() 用来设置一个计数
  • Done() 用来在操作结束时调用,使计数减1
  • Wait() 用来等待所有的操作结束,即计数变为0。

实现set

//实现set

type inter interface{}
type Set struct {
	m map[inter]bool
	sync.RWMutex
}

func New() *Set {
	return &Set{
		m: map[inter]bool{},
	}
}
func (s *Set) Add(item inter) {
	s.Lock()
	defer s.Unlock()
	s.m[item] = true
}

func main() {
	
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

http get跟head

HEAD和GET本质是一样的,区别在于HEAD不含有呈现数据,而仅仅是HTTP头信息。

想象一个业务情景:欲判断某个资源是否存在,我们通常使用GET,但这里用HEAD则意义更加明确。

http 401,403

400 bad request,请求报文存在语法错误

401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息

403 forbidden,表示对请求资源的访问被服务器拒绝

404 not found,表示在服务器上没有找到请求的资源

http keep-alive

client发出的HTTP请求头需要增加Connection:keep-alive字段;

Web-Server端要能识别Connection:keep-alive字段,并且在http的response里指定Connection:keep-alive字段,告诉client,我能提供keep-alive服务,并且"应允"client我暂时不会关闭socket连接.

http能不能一次连接多次请求,不等后端返回

http本质上市使用socket连接,因此发送请求,接写入tcp缓冲,是可以多次进行的,这也是http是无状态的原因.

TCP 和 UDP 的区别

  • TCP 是面向连接的,UDP 是面向无连接的
  • UDP程序结构较简单
  • TCP 是面向字节流的,UDP 是基于数据报的
  • TCP 保证数据正确性,UDP 可能丢包
  • TCP 保证数据顺序,UDP 不保证

介绍下你平时都是怎么调试 golang 的 bug 以及性能问题的?

panic 调用栈

pprof是Go的性能分析工具,在程序运行过程中,可以记录程序的运行信息,可以是CPU使用情况、内存使用情况、goroutine运行情况等

package main

import (
	"fmt"
	"net/http"
	_ "net/http/pprof" // 引入pprof,调用init方法
)

func main() {

	// 生产环境应仅在本地监听pprof
	go func() {
		ip := "127.0.0.1:9527"
		if err := http.ListenAndServe(ip, nil); err != nil {
			fmt.Println("开启pprof失败", ip, err)
		}
	}()

	// 业务代码运行中
	http.ListenAndServe("0.0.0.0:8081", nil)
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

火焰图(配合压测)

使用go run -race 或者 go build -race 来进行竞争检测

查看系统 磁盘IO/网络IO/内存占用/CPU 占用(配合压测)

select随机性

func main() {
	runtime.GOMAXPROCS(1)
	int_chan := make(chan int, 1)
	string_chan := make(chan string, 1)
	int_chan <- 1
	string_chan <- "hello"
	select {
	case value := <-int_chan:
		fmt.Println(value)
	case value := <-string_chan:
		fmt.Println(value)
		panic("<-string_chan")
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

select会随机选择一个可用通用做收发操作。所以代码是有可能触发异常,也有可能不会。

单个chan如果无缓冲时,将会阻塞。但结合select可以在多个chan间等待执行。有三点原则:

  1. select 中只要有一个case能return,则立刻执行。
  2. 当如果同一时间有多个case均能return则伪随机方式抽取任意一个执行。
  3. 如果没有一个case能return则可以执行”default”块。

介绍下 golang 的 runtime 机制?

Runtime 负责管理任务调度,垃圾收集及运行环境。同时,Go提供了一些高级的功能,如goroutine, channel, 以及Garbage collection。

这些高级功能需要一个runtime的支持. runtime和用户编译后的代码被linker静态链接起来,形成一个可执行文件。这个文件从操作系统角度来说是一个用户态的独立的可执行文件。

从运行的角度来说,这个文件由2部分组成,一部分是用户的代码,另一部分就是runtime。

runtime通过接口函数调用来管理goroutine, channel及其他一些高级的功能。

从用户代码发起的调用操作系统API的调用都会被runtime拦截并处理。

Go runtime的一个重要的组成部分是goroutine scheduler。

他负责追踪,调度每个goroutine运行,实际上是从应用程序的process所属的thread pool中分配一个thread来执行这个goroutine。

因此,和java虚拟机中的Java thread和OS thread映射概念类似,每个goroutine只有分配到一个OS thread才能运行。

Go语言局部变量分配在栈还是堆?

Go语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析,当发现变量的作用域没有跑出函数范围,就可以在栈上,反之则必须分配在堆。

如何获取 go 程序运行时的协程数量, gc 时间, 对象数, 堆栈信息?

调用接口 runtime.ReadMemStats 可以获取以上所有信息,

注意: 调用此接口会触发 STW(Stop The World)

https://golang.org/pkg/runtime/#ReadMemStats

如果需要打入到日志系统, 可以使用 go 封装好的包, 输出 json 格式.

https://golang.org/pkg/expvar/

简述一下你对Go垃圾回收机制的理解?

常见的垃圾回收方法:

**引用计数:**对每个对象维护一个引用计数,当引用该对象的对象被销毁时,引用计数减1,当引用计数器为0是回收该对象。

优点:对象可以很快的被回收,不会出现内存耗尽或达到某个阀值时才回收。

缺点:不能很好的处理循环引用,而且实时维护引用计数,有也一定的代价。

代表语言:Python、PHP

**标记-清除:**从根变量开始遍历所有引用的对象,引用的对象标记为"被引用",没有被标记的进行回收。

优点:解决了引用计数的缺点。

缺点:需要STW,即要暂时停掉程序运行。

代表语言:Golang(其采用三色标记法)

**分代收集:**按照对象生命周期长短划分不同的代空间,生命周期长的放入老年代,而短的放入新生代,不同代有不能的回收算法和回收频率。

优点:回收性能好

缺点:实现复杂

代表语言: JAVA

root

首先标记root根对象,根对象的子对象也是存活的。

根对象包括:全局变量,各个stack上的变量等。

三色标记

灰色:对象已被标记,但这个对象包含的子对象未标记

黑色:对象已被标记,且这个对象包含的子对象也已标记

白色:对象未被标记

GC步骤

初始状态下所有对象都是白色的。(未被标记)

首先标记root对象为灰色,放入待处理队列。

取出待处理队列的灰色对象,将其引用标记为灰色,放入待处理队列,并将本身标记为黑色.

循环第三步,直到待处理队列为空(在标记过程中的新的引用对象,通过写屏障直接标记为灰色),

此时剩下的只有白色和黑色,白色对象则表示不可达,将其清理.

触发GC的机制

在申请内存的时候,检查当前当前已分配的内存是否大于上次GC后的内存的2倍

(可配置GOGC参数,即百分比,默认是100), 若是则触发.

监控线程发现上次GC的时间已经超过两分钟了,触发

手动:runtime.gc()

简述一下golang的协程调度原理?

M(machine): 代表着真正的执行计算资源,可以认为它就是os thread(系统线程)。

P(processor): 表示逻辑processor,是线程M的执行的上下文。

G(goroutine): 调度系统的最基本单位goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等。

G-M-P三者的关系与特点:

P的个数取决于设置的GOMAXPROCS,go新版本默认使用最大内核数,比如你有8核处理器,那么P的数量就是8

M的数量和P不一定匹配,可以设置很多M,M和P绑定后才可运行,多余的M处于休眠状态。

P包含一个LRQ(Local Run Queue)本地运行队列,这里面保存着P需要执行的协程G的队列

除了每个P自身保存的G的队列外,调度器还拥有一个全局的G队列GRQ(Global Run Queue),这个队列存储的是所有未分配的协程G。

互斥锁

当有一个线程要访问共享资源(临界资源)之前会对线程访问的这段代码(临界区)进行加锁。如果在加锁之后没释放锁之前其他线程要对临界资源进行访问,则这些线程会被阻塞睡眠,直到解锁,如果解锁时有一个或者多个线程阻塞,那么这些锁上的线程就会变成就绪状态,然后第一个变为就绪状态的线程就会获取资源的使用权,并且再次加锁,其他线程继续阻塞等待。

读写锁

也叫做共享互斥锁,读模式共享,写模式互斥。有点像数据库负载均衡的读写分离模式。它有三种模式:读加锁状态,写加锁状态和不加锁状态。简单来说就是只有一个线程可以占有写模式的读写锁,但是可以有多个线程占用读模式的读写锁。
当写加锁的模式下,任何线程对其进行加锁操作都会被阻塞,直到解锁。
当在读加锁的模式下,任何线程都可以对其进行读加锁的操作,但所有试图进行写加锁操作的线程都会被阻塞。直到所有读线程解锁。但是当读线程太多时,写线程一直被阻塞显然是不对的,所以一个线程想要对其进行写加锁时,就会阻塞读加锁,先让写加锁线程加锁

自旋锁

自旋锁和互斥锁很像,唯一不同的是自旋锁访问加锁资源时,会一直循环的查看是否释放锁。这样要比互斥锁效率高很多,但是只会占用CPU。所以自旋锁适用于多核的CPU。但是还有一个问题是当自旋锁递归调用的时候会造成死锁现象。所以慎重使用自旋锁。

乐观锁

这其实是一种思想,当线程去拿数据的时候,认为别的线程不会修改数据,就不上锁,但是在更新数据的时候会去判断以下其他线程是否修改了数据。通过版本来判断,如果数据被修改了就拒绝更新,之所以叫乐观锁是因为并没有加锁。

悲观锁

当线程去拿数据的时候,总以为别的线程会去修改数据,所以它每次拿数据的时候都会上锁,别的线程去拿数据的时候就会阻塞。
这两种锁一般用于数据库,当一个数据库的读操作远远大于写的操作次数时,使用乐观锁会加大数据库的吞吐量。

Golang的并发模式

runner

runner可以给一组任务进行顺序分配,然后进行总体的时间限制。该方式确定了任务的顺序后,可以让任务顺序执行,直到时间限制到了或者任务完成或者人为中断。

pool

pool的作用是实现有缓冲通道的资源池,管理在任意数量的goroutine共享以及独立使用的资源。该模式在**需要共享一组静态资源的情况(比如共享数据库链接或者内存缓冲区)**的情况下非常有用。如果goroutine需要从池里获得资源中的一个,可以从池中申请,使用完后归还到池里;如果池里没有资源,那么就返回一个新建立的资源;向池中返回资源时,如果池中资源已经满了,则释放掉这个资源。

work

缓冲队列只有一个容量,所有的线程初始情况都是堵塞的情况,直到添加合适的任务。所有的goroutine控制一组工作。所有外加的任务必须把任务放在自己的Task()函数中,这个函数自己实现。

总结
  • 可以使用通道来控制程序的生命周期。
  • 带 default 分支的 select 语句可以用来尝试向通道发送或者接收数据,而不会阻塞。
  • 有缓冲的通道可以用来管理一组可复用的资源。
  • 语言运行时会处理好通道的协作和同步。
  • 使用无缓冲的通道来创建完成工作的 goroutine 池。
  • 任何时间都可以用无缓冲的通道来让两个 goroutine 交换数据,在通道操作完成时一定保证对方接收到了数据。

io模型

https://blog.csdn.net/chushoufengli/article/details/115231156

阻塞式IO

程序想在缓冲区读数据时,缓冲区并不一定会有数据,这会造成陷入系统调用,只能等待数据可以读取,没有数据读取时则会阻塞住进程,这就是阻塞式IO。当需要为多个客户端提供服务时,可以使用线程方式,每个socket句柄使用一个线程来服务,这样阻塞住的则是某个线程。虽然如此可以解决进程阻塞,但是还是会有相当一部分CPU资源浪费在了等待数据上,同时,使用线程来服务fd有些浪费资源,因为如果要处理的fd较多,则又是一笔资源开销。

非阻塞式IO

与之对应的是非阻塞IO,当程序想要读取数据时,如果缓冲区不存在,则直接返回给用户程序,但是需要用户程序去频繁检查,直到有数据准备好。这同样也会造成空耗CPU。

IO多路复用(包含:select/poll/epoll)

而IO多路复用则不同,他会使用一个线程去管理多个fd,可以将多个fd加入IO多路复用函数中,每次调用该函数,传入要检查的fd,如果有就绪的fd,直接返回就绪的fd,再启动线程处理或者顺序处理就绪的fd。这达到了一个线程管理多个fd任务,相对来说较为高效。常见的IO多路复用函数有select,poll,epoll。select与poll的最大缺点是每次调用时都需要传入所有要监听的fd集合,内核再遍历这个传入的fd集合,当并发量大时候,用户态与内核态之间的数据拷贝以及内核轮询fd又要浪费一波系统资源(关于select与poll这里不展开)。

select/poll/epoll三者的区别

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/小丑西瓜9/article/detail/514445
推荐阅读
相关标签
  

闽ICP备14008679号