1、Golang中除了加Mutex锁以外还有哪些方式安全读写共享变量。
Golang中Goroutine 可以通过 Channel 进行安全读写共享变量,还可以通过原子性操作进行。
2、无缓冲Chan的发送和接收是否同步
首先了解一下什么是缓冲
A := make(chan int) //无缓冲
B := make(chan int,1)//有缓冲
//A <- 1,这时候往A中写入1,一定会有<- A,只有这样其他值写入A的时候才可以进行下去,不然会阻塞。
//B <- 1,则不会发生阻塞,因为B的缓冲大小为1,只有当第二个值写入B时,第一个值还未被取出才会阻塞。
无缓冲通道
指在接收前没有能力保存任何值的通道。这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。如果两个goroutine没有同时准备好,通道会导致先执行发送或接收操作的goroutine阻塞等待。这种对通道进行发送和接收的交互行为本身就是同步的,其中任意一个操作都无法离开另一个操作单独存在。
有缓冲通道
指通道可以保存多个值。如果给定了一个缓冲区容量,那么通道就是异步的,只要缓冲区有未使用空间用于发送数据,或还包含可以接收的数据,那么其通信就会无阻塞地进行。
所以,无缓冲Chan的发送和接收是同步的。
3、Golang的CSP并发模型的实现
Golang的CSP并发模型,是通过Goroutine和Channel来实现的。
channel <- data<-channel
4、Golang中常用的并发模型
Golang中常用的并发模型有三种:
- 通过channel实现并发控制
- 通过sync包中的WaitGroup实现并发控制
- 在Go 1.7以后引进的强大的Context上下文,实现并发控制.
通过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")
}
<-ch
通过sync包中的WaitGroup实现并发控制
Goroutine是异步执行的,有的时候为了防止在结束main函数的时候结束掉Goroutine,所以需要同步等待,这个时候就需要用 WaitGroup了,在Sync包中,提供了 WaitGroup,它会等待它收集的所有 goroutine 任务全部完成。
在WaitGroup底层的结构体中主要有三个方法:
- Add, 可以添加或减少 goroutine的数量。
- Done, 相当于Add(-1)。
- Wait, 执行后会堵塞主线程,直到WaitGroup 里的值减至0。
在主goroutine 中 Add(delta int) 索要等待goroutine 的数量。在每一个goroutine 完成后Done()表示这一个goroutine 已经完成,当所有的 goroutine 都完成后,在主 goroutine 中 WaitGroup 返回。
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()
}
在Go 1.7以后引进的强大的Context上下文,实现并发控制
channelWaitGroup
比如一个网络请求 Request,每个 Request 都需要开启一个 goroutine 做一些事情,这些 goroutine 又可能会开启其他的 goroutine,比如数据库和RPC服务。
所以我们需要一种可以跟踪 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() 返回一个只能接受数据的channel类型,当该context关闭或者超时时间到了的时候,该channel就会有一个取消信号
Done() <-chan struct{}
// Err indicates why this Context was canceled, after the Done channel
// is closed.
// Err() 在Done() 之后,返回context 取消的原因。
Err() error
// Deadline returns the time when this Context will be canceled, if any.
// Deadline() 设置该context cancel的时间点
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
// Value() 方法允许 Context 对象携带request作用域的数据,该数据必须是线程安全的。
Value(key interface{}) interface{}
}
Context 对象是线程安全的,你可以把一个 Context 对象传递给任意个数的 gorotuine,对它执行取消操作时,所有 goroutine 都会接收到取消信号。
一个 Context 不能拥有 Cancel 方法,同时我们也只能 Done channel 接收数据。其中的原因是一致的:接收取消信号的函数和发送信号的函数通常不是一个。
典型的场景是:父操作为子操作操作启动 goroutine,子操作也就不能取消父操作。
5、Go中对nil的Slice和空Slice的处理是一致的吗
不一致。
var slice []int
slice[1] = 0
上述代码是错误的,因为只是声明了slice,并没有给实例化对象,也即没有分配内存。
slice := make([]int,0)
slice := []int{}
空slice是指slice不为nil,但是slice没有值,slice的底层的空间是空的,如上所述代码所示。
6、协程、线程和进程的区别
进程
进程是程序的一次执行过程,是程序在执行过程中的分配和管理资源的基本单位,每个进程都有自己的地址空间,进程是系统进行资源分配和调度的一个独立单位。
每个进程都有自己的独立内存空间,不同进程通过IPC(Inter-Process Communication)进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。
线程
线程是进程的一个实体,线程是内核态,而且是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。
协程
协程是一种用户态的轻量级线程,调度完全由用户控制。协程拥有自己的寄存器上下文和栈。
协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
6、Golang的内存模型中为什么小对象多了会造成GC压力
通常小对象过多会导致GC三色法消耗过多的GPU。优化思路是,减少对象分配。
7、什么是channel,为什么它可以做到线程安全
Channel是Go中的一个核心类型,可以把它看成一个管道,通过它并发核心单元就可以发送或者接收数据进行通讯,Channel也可以理解是一个先进先出的队列,通过管道进行通信。
Golang的Channel, 发送一个数据到Channel和从Channel接收一个数据都是原子性的。
Go的设计思想就是, 不要通过共享内存来通信,而是通过通信来共享内存,前者就是传统的加锁,后者就是Channel。也就是说,设计Channel的主要目的就是在多任务间传递数据的,本身就是安全的(从底层来说的话,chan底层中有一个互斥锁)。