golang面试经验总结(三)游服务器一二三面 秋招 米哈游

本文介绍了Golang的内存模型、并发控制机制,包括通过channel和sync包中的WaitGroup实现并发控制,以及Context在并发场景中的应用。同时,概述了Golang的垃圾回收原理,并探讨了Redis的底层实现,如字符串、哈希、列表、有序集合的存储结构。此外,讨论了进程、线程和协程的概念及区别,并分析了进程间同步的方式。
摘要由CSDN通过智能技术生成

问题来源于游服务器一二三面 秋招 米哈游,自学习使用,答案搜索来自于网络,若有不对的地方欢迎指正。

1 golang内存模型

原文内容太长,这里就给出原文链接
Golang 内存模型|The Go Memory Model

2 golang并发模型

内容来源

通过channel通知实现并发控制

无缓冲的通道指的是通道的大小为0,也就是说,这种类型的通道在接收前没有能力保存任何值,它要求发送 goroutine 和接收 goroutine 同时准备好,才可以完成发送和接收操作

从上面无缓冲的通道定义来看,**发送 goroutine 和接收 gouroutine 必须是同步的,同时准备后,如果没有同时准备好的话,先执行的操作就会阻塞等待,直到另一个相对应的操作准备好为止。**这种无缓冲的通道我们也称之为同步通道

func main() {
    ch := make(chan struct{})
    go func() {
        fmt.Println("start working")
        time.Sleep(time.Second * 1)
        ch <- struct{}{}
    }()
    <-ch
    fmt.Println("finished")
}

当主 goroutine 运行到<-ch接受channel 的值的时候,如果该 channel 中没有数据,就会一直阻塞等待,直到有值。 这样就可以简单实现并发控制。

通过sync包中的WaitGroup实现并发控制

Goroutine是异步执行的,有的时候为了防止在结束main函数的时候结束掉Goroutine,所以需要同步等待,这个时候就需要用 WaitGroup了,在Sync包中,提供了 WaitGroup,它会等待它收集的所有 goroutine 任务全部完成。
在WaitGroup里主要有三个方法:

  • Add, 可以添加或减少 goroutine的数量.
  • Done, 相当于Add(-1).
  • Wait, 执行后会堵塞主线程,直到WaitGroup 里的值减至0.
func main(){
    var wg sync.WaitGroup
    var urls = []string{
        "http://www.golang.org/",
        "http://www.google.com/",
    }
    for _, url := range urls {
        wg.Add(1)
        go func(url string) {
            defer wg.Done()
            http.Get(url)
        }(url)
    }
    wg.Wait()
}

在主goroutine 中 Add(delta int) 索要等待goroutine 的数量。在每一个goroutine
完成后Done()表示这一个goroutine 已经完成,当所有的 goroutine 都完成后,在主 goroutine 中WaitGroup 返回。

在Golang官网中对于WaitGroup介绍是A WaitGroup must not be copied after first use,在 WaitGroup 第一次使用后,不能被拷贝
eg:

func main(){
 wg := sync.WaitGroup{}
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go func(wg sync.WaitGroup, i int) {
            fmt.Printf("i:%d", i)
            wg.Done()
        }(wg, i)
    }
    wg.Wait()
    fmt.Println("exit")
}

运行结果:

i:1i:3i:2i:0i:4fatal error: all goroutines are asleep - deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc000094018)
        /home/keke/soft/go/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0xc000094010)
        /home/keke/soft/go/src/sync/waitgroup.go:130 +0x64
main.main()
        /home/keke/go/Test/wait.go:17 +0xab
exit status 2

它提示所有的goroutine都已经睡眠了,出现了死锁。这是因为 wg 给拷贝传递到了 goroutine 中,导致只有 Add操作,其实 Done操作是在 wg 的副本执行的。

因此 Wait 就会死锁。

  • 这个第一个修改方式: 将匿名函数中 wg 的传入类型改为*sync.WaitGroup,这样就能引用到正确的WaitGroup了。
  • 这个第二个修改方式: 将匿名函数中的 wg 的传入参数去掉,因为Go支持闭包类型,在匿名函数中可以直接使用外面的 wg 变量.

强大的Context上下文,实现并发控制

在一些简单场景下使用 channel 和 WaitGroup 已经足够了,但是当面临一些复杂多变的网络并发场景下channel和WaitGroup显得有些力不从心了。
所以我们需要一种可以跟踪 goroutine 的方案,才可以达到控制他们的目的,这就是Go语言为我们提供的 Context,称之为上下文非常贴切,它就是goroutine 的上下文。
它是包括一个程序的运行环境现场快照等。每个程序要运行时,都需要知道当前程序的运行状态,通常Go 将这些封装在一个 Context 里,再将它传给要执行的 goroutine 。

context 包主要是用来处理多个 goroutine 之间共享数据,及多个 goroutine 的管理。

context 包的核心是 struct Context,接口声明如下:
// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
    // Done returns a channel that is closed when this `Context` is canceled
    // or times out.
    Done() <-chan struct{}

    // Err indicates why this Context was canceled, after the Done channel
    // is closed.
    Err() error

    // Deadline returns the time when this Context will be canceled, if any.
    Deadline() (deadline time.Time, ok bool)

    // Value returns the value associated with key or nil if none.
    Value(key interface{}) interface{}
}
  • Done()返回一个只能接受数据的channel类型,当该context关闭或者超时时间到了的时候,该channel就会有一个取消信号
  • Err()在Done() 之后,返回context 取消的原因。
  • Deadline()设置该context cancel的时间点
  • Value() 方法允许 Context 对象携带request作用域的数据,该数据必须是线程安全的。

Context 对象是线程安全的,你可以把一个 Context 对象传递给任意个数的 gorotuine,对它执行取消操作时,所有 goroutine 都会接收到取消信号。

一个 Context 不能拥有 Cancel 方法,同时我们也只能 Done channel 接收数据。其中的原因是一致的:接收取消信号的函数和发送信号的函数通常不是一个。

典型的场景是:父操作为子操作操作启动 goroutine,子操作也就不能取消父操作。

3 golang gc原理 过程

已经写过这个内容

4 channel用途 原理

用途

在golang中,channel属于较为核心的一个功能,尤其在go协程中,channel功能尤为重要。

  • 作为goroutine之间通信的一种方式,channel跟Linux系统中的管道/消息队列有很多类似之处。
  • 使用channel可以方便地在goroutine之间传递数据
  • channel还关联了数据类型,如int、string等等,可以决定确定channel中的数据单元

原理

原文太长,给出链接
具体原理

5 redis底层实现

string字符串类型实现

redis的字符串类型是由一种叫做简单动态字符串(SDS)的数据类型来实现

struct sdshdr {
  int len;          //buf中已占用空间的长度
  int free;        //buf中剩余空间的长度
  char buf[];    //数据空间
}

SDC和C语言字符串的区别:

  • 1:SDS保存了字符串的长度,而C语言不保存,只能遍历找到第一个\0的结束符才能确定字符串的长度
  • 2:修改SDS,会检查空间是否足够,不足会先扩展空间,防止缓冲区溢出,C字符串不会检查
  • 3:SDS的预分配空间机制,可以减少为字符串重新分配空间的次数

hash哈希类型实现

hash结构里其实是一个字典,有许多的键值对
redis的哈希表是一个dictht结构体:

typedef struct dictht {
  dictEntry ** table;                //哈希表数组
  unsigned long size;            //哈希表大小
  unsigned long sizemask //哈希表大小掩码,用于计算索引值 总是等于 size - 1
  unsigned logn used;          //该哈希表已有节点的数量
}

typedef struct dictEntry{
  void *key;  //键
  union {      //不同键对饮的值得类型可能不同,使用union来处理这个问题
    void *val;
    uint64_tu64;
    int64_ts64;
  }
  struct dictEntry *next;
}

当要将一个新的键值对添加到字典里面时, 程序需要先根据键值对的键计算出哈希值和索引值, 然后再根据索引值, 将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。

list列表类型实现

redis的list列表是使用双向链表来实现的

typedef struct listNode {
struct listNode * pre; //前置节点
struct listNode * next; //后置节点
void * value; //节点的值
}

typedef struct list {
listNode *head; //表头节点
listNode tail; //表尾节点
unsigned long len; //链表所包含的节点数量
void (dup) (void ptr); //节点值赋值函数 这里有问题
void (free) (void ptr); //节点值释放函数
int (match) (void *ptr, void *key) //节点值对比函数
}

zset有序集合

  1. 有序集合的底层实现之一是跳表除此之外跳表它在 Redis 中没有其他应用。
  2. 整数集合(intset)是集合键的底层实现之一: 当一个集合只包含整数值元素, 并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现。
  3. 数据少时,使用ziplist(压缩列表),占用连续内存,每项元素都是(数据+score)的方式连续存储,按照score从小到大排序。ziplist为了节省内存,每个元素占用的空间可以不同,对于大数据(long long),就多用一些字节存储,而对于小的数据(short),就少用一些字节来存储。因此查找的时候需要按顺序遍历。ziplist省内存但是查找效率低。

set无序集合

无序集合可以用整数集合(intset)或者字典实现

6 跳跃表查询插入复杂度

1 时间复杂度
底层包含所有元素(n个),即 2^(h +1)=n ,索引层数 h=logn-1。搜索时,首先在顶层索引中进行查找,然后二分搜索,最多从顶层搜索到底层,最多 O(logn) 层,因此查找的时间复杂度为 O(logn)。

2 空间复杂度
增加索引需要一些辅助空间,那么索引一共有多少个节点呢?从上图中可以看出,每层索引的节点之和都为 2+2^2 +…+2^h =2^(h +1)-2=n-2,因此空间复杂度为 O(n )。实际上,索引节点并不是原节点。

跳跃表详解

7 进程,线程,协程

进程

  • 进程是程序一次动态执行的过程,是程序运行的基本单位。
  • 每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。
  • 进程占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、页表、文件句柄等)比较大,但相对比较稳定安全。
  • 进程是资源分配的最小单位。

线程

  • 线程又叫做轻量级进程,是CPU调度的最小单位。
  • 线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。
  • 多个线程共享所属进程的资源,同时线程也拥有自己的专属资源。
  • 程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
  • 线程从属于进程,是程序的实际执行者。一个进程可以有多个线程,最少有一个线程,但一个线程只能有一个进程。

协程

  • 协程是一种用户态的轻量级线程,协程的调度完全由用户控制。
  • 一个线程可以拥有多个协程,协程不是被操作系统内核所管理,而完全是由程序所控制。
  • 协程最主要的作用是在单线程的条件下实现并发的效果,但实际上还是串行的(像yield一样)一个线程可以拥有多个协程,协程不是被操作系统内核所管理,而完全是由程序所控制。

区别

进程与线程的区别
  1. 根本区别: 进程是操作系统资源分配和独立运行的最小单位;线程是任务调度和系统执行的最小单位。
  2. 地址空间区别: 每个进程都有独立的地址空间,一个进程崩溃不影响其它进程;一个进程中的多个线程共享该 进程的地址空间,一个线程的非法操作会使整个进程崩溃。
  3. 上下文切换开销区别: 每个进程有独立的代码和数据空间,进程之间上下文切换开销较大;线程组共享代码和数据空间,线程之间切换的开销较小。

线程和协程的区别

  1. 一个线程可以多个协程,一个进程也可以单独拥有多个协程。
  2. 线程进程都是同步机制,而协程则是异步。
  3. 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用的状态。
  4. 线程是抢占式,而协程是非抢占式的,所以需要用户自己释放使用权来切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。
  5. 协程并不是取代线程, 而且抽象于线程之上, 线程是被分割的CPU资源, 协程是组织好的代码流程, 协程需要线程来承载运行, 线程是协程的资源, 但协程不会直接使用线程, 协程直接利用的是执行器(Interceptor), 执行器可以关联任意线程或线程池, 可以使当前线程, UI线程, 或新建新程.。
  6. 线程是协程的资源。协程通过Interceptor来间接使用线程这个资源。

8 kill原理

kill 命令通常用于杀死进程。 它在内部发送一个信号,根据您想要做什么,您可以使用此工具发送不同的信号。 以下是命令的语法:

kill [options] < pid > […]

工作原理

kill命令的工作原理是向Linux系统的内核发送一个系统操作信号某个程序的进程标识号,然后系统内核就可以对进程标识号指定的进程进行操作。

kill -9 PID

如果SIGTERM无法正常运行,我们可以使用SIGKILL的苛刻方式。 我们可以使用带有-9选项的kill命令发送SIGKILL信号,该命令指定SIGKILL信号。

要停止所有进程并注销自己,请输入以下命令:

kill -9 0

这将向所有进程组 ID 等于发送方进程组 ID 的进程发送信号 9,即 SIGKILL 信号。 由于 shell 无法忽略 SIGKILL 信号,因此此命令还会停止登录 shell 并将您注销

要停止您拥有的所有进程,请输入以下命令:

kill -9 -1

该命令向有效用户拥有的所有进程发送信号 9,即 SIGKILL 信号,甚至是那些在其他工作站启动并属于其他进程组的进程。 如果正在打印您请求的列表,它也会停止。

9 线程间同步方式

线程同步的方式主要有: 临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphore)、事件(Event)。

他们的主要区别和特点如下:
1)临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。

2)互斥量:采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一应用程序的公共资源安全共享,还能实现不同应用程序的公共资源安全共享。

3)信号量:它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。

4)事 件: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值