简单回答:
线程切换需要切换内核栈和硬件上下文的,而协程切换只发生在用户态,没有时钟中断、系统调用等机制,效率更高。
参考:线程与进程:问题6
详细解释:
线程是内核对外提供的服务,应用程序可以通过系统调用让内核启动线程,由内核来负责线程调度和切换。线程在等待IO操作时线程变为unrunnable状态会触发上下文切换。现代操作系统一般都采用抢占式调度,上下文切换一般发生在时钟中断和系统调用返回前,调度器计算当前线程的时间片,如果需要切换就从运行队列中选出一个目标线程,保存当前线程的环境,并且恢复目标线程的运行环境,最典型的就是切换ESP指向目标线程内核堆栈,将EIP指向目标线程上次被调度出时的指令地址。
go协程是不依赖操作系统和其提供的线程,golang自己实现的CSP并发模型实现(MGP),同时go协程也叫用户态线程,协程之间的切换发生在用户态,很轻量。在用户态没有时钟中断,系统调用等机制, 因此效率比较高。
协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
执行go协程只需要极少的栈内存(大概是4~5KB),默认情况下,而线程栈的大小为1MB。goroutine就是一段代码,一个函数入口,以及在堆上为其分配的一个堆栈。所以它非常廉价,我们可以很轻松的创建上万个goroutine,但它们并不是被操作系统所调度执行。
数组:
切片:
len()返回切片中的元素个数。
cap()返回切片的容量即切片最长可以容纳的元素数量。
type slice struct {
array unsafe.Pointer
len int
cap int
}
指针变量存储的是另一个变量的地址。
引用变量指向另外一个变量。
引用类型:
数组切片、字典(map)、通道(channel)、接口(interface)
int包括有符号整型或无符号整型。
uint和uint8等都属于无符号int类型。
go语言中的int的大小是和操作系统位数相关的,如果是32位操作系统就是4个字节,如果是64位就是8个字节。
线程模型
在细说 Go 的调度模型之前,先来说说一般意义的线程模型。线程模型一般分三种,由用户级线程和 OS 线程的不同对应关系决定的。
为什么 Go Scheduler 需要实现 M:N 的方案? (为什么在内核的线程调度器之外Go还需要一个自己的调度器?)
线程创建开销大。对于 OS 线程而言,其很多特性均是操作系统给予的,但对于 Go 程序而言,其中很多特性可能非必要的。这样一来,如果是 1:1 的方案,那么每次 go func(){…} 都需要创建一个 OS 线程,而在创建线程过程中,OS 线程里某些 Go 用不上的特性会转化为不必要的性能开销,不经济。
减少 Go 垃圾回收的复杂度。依据1:1方案,Go 产生所用用户级线程均交由 OS 直接调度。 Go 的垃圾回收器要求在运行时需要停止所有线程,才能使得内存达到稳定一致的状态,而 OS 不可能清楚这些,垃圾回收器也不能控制 OS 去阻塞线程。
Go Scheduler 由哪些元素构成呢?
注:GOMAXPROCS 参数很重要,其决定了 P 的最大数量,也决定了自旋 M 的最大数量。线程自旋是相对于线程阻塞而言的,如果 G 迟迟不来,CPU 会白白浪费在这无意义的计算上。但好处也很明显,降低了 M 的上下文切换成本,提高了性能。Go 的设计者倾向于高性能的并发表现,为了避免过多浪费 CPU 资源,自旋的线程数不会超过 GOMAXPROCS。
本地队列(local queue): 本地是相对 P 而言的本地,每个 P 维护一个本地队列;与 P 绑定的 M 中如若生成新的 G,一般情况下会放到 P 的本地队列;当本地队列满了的时候,才会截取本地队列中 “一半” 的元素放入全局队列中;
全局队列(global queue):承载本地队列“溢出”的 G。为了保证调度公平性,schedule 过程中有 1/61 的几率优先检查全局队列,否则本地队列一直满载的情况下,全局队列中的 G 将永远无法被调度到;
窃取(stealing): 这似乎和 Java Fork-Join 中的 work-stealing 模型很相似,其目的也是一样,就是为了使得空闲(idle)的 M 有活干,不空等,提高计算资源的利用率。窃取也是有章法的,规则是随机从其他 P 的本地队列里窃取 “一半” 的 G。
一言蔽之,调度的本质就是 P 将 G 合理的分配给某个 M 的过程。
context包的用途Context通常被译作上下文,它是一个比较抽象的概念,其本质,是存在上下层的传递,上层会把内容传递给下层。在Go语言中,程序单元也就指的是Goroutine。
三色标记法
我们来看普通的标记清楚算法是如何做的:
第一步,暂停程序业务逻辑, 找出不可达的对象,然后做上标记。第二步,回收标记好的对象。
对实时性要求比较高的系统来说,Mark-Sweep两个阶段,在Mark阶段会STW,这种需要长时间挂起的标记清除算法是不可接受的,而三色标记算法就很好的解决了这个问题。
Golang通过三色标记法,使得mark阶段和用户程序是并行的。先来看看如果mark阶段是与用户程序并发的,会产生如下问题:
可以知道,在mark阶段可能会有新的对象分配,这时候会出现对象的误标记为白色。为了防止这种现象的发生,最简单的方式就是STW,直接禁止掉其他用户程序对对象引用关系的干扰,但是STW的过程有明显的资源浪费,对所有的用户程序都有很大影响,如何能在保证对象不丢失的情况下合理的尽可能的提高GC效率,减少STW时间呢?这个时候就需要通过混合写屏障(hybrid write barrier)记录下来。
插入写屏障:
删除写屏障:
插入写屏障和删除写屏障的短板:
混合写屏障:
参考:Golang垃圾回收
package main
import (
"fmt"
)
func main() {
defer_call()
}
func defer_call() {
defer func() { fmt.Println("打印前") }()
defer func() { fmt.Println("打印中") }()
defer func() { fmt.Println("打印后") }()
panic("触发异常")
}
考点:
defer执行顺序
解答:
defer 是后进先出。
panic 需要等defer 结束后才会向上传递。 出现panic恐慌时候,会先按照defer的后入先出的顺序执行,最后才会执行panic。
输出:
打印后
打印中
打印前
panic: 触发异常
package main
import (
"fmt"
)
type student struct {
Name string
Age int
}
func pase_student() map[string]*student{
m := make(map[string]*student)
stus := []student{
{Name: "zhou", Age: 24},
{Name: "li", Age: 23},
{Name: "wang", Age: 22},
}
for _, stu := range stus {
m[stu.Name] = &stu
}
return m
}
func main() {
m:=pase_student()
fmt.Println(m)
}
解答:
与Java的foreach一样,都是使用副本的方式。所以m[stu.Name]=&stu实际上一致指向同一个指针, 最终该指针的值为遍历的最后一个struct的值拷贝。
输出:
map[li:0xc00011c018 wang:0xc00011c018 zhou:0xc00011c018]
func main() {
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 10; i++ {
go func() {
fmt.Println("A: ", i)
wg.Done()
}()
}
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println("B: ", i)
wg.Done()
}(i)
}
wg.Wait()
}
考点:
go执行的随机性和闭包。
解答:
谁也不知道执行后打印的顺序是什么样的,所以只能说是随机数字。 但是A:均为输出10,B:从0~9输出(顺序不定)。 第一个go func中i是外部for的一个变量,地址不变化。遍历完成后,最终i=10。 故go func执行时,i的值始终是10。
第二个go func中i是函数参数,与外部for中的i完全是两个变量。 尾部(i)将发生值拷贝,go func内部指向值拷贝地址。
type People struct{}
func (p *People) ShowA() {
fmt.Println("showA")
p.ShowB()
}
func (p *People) ShowB() {
fmt.Println("showB")
}
type Teacher struct {
People
}
func (t *Teacher) ShowB() {
fmt.Println("teacher showB")
}
func main() {
t := Teacher{}
t.ShowA()
}
考点:
go的组合继承
解答:
这是Golang的组合模式,可以实现OOP的继承。 被组合的类型People所包含的方法虽然升级成了外部类型Teacher这个组合类型的方法(一定要是匿名字段),但它们的方法(ShowA())调用时接受者并没有发生变化。 此时People类型并不知道自己会被什么类型组合,当然也就无法调用方法时去使用未知的组合者Teacher类型的功能。
输出:
showA
showB
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:
panic(value)
}
}
考点:
select随机性
解答:
select会随机选择一个可用通用做收发操作。 所以代码是有肯触发异常,也有可能不会。 单个chan如果无缓冲时,将会阻塞。但结合 select可以在多个chan间等待执行。有三点原则:
- select 中只要有一个case能return,则立刻执行。
- 当如果同一时间有多个case均能return则伪随机方式抽取任意一个执行。
- 如果没有一个case能return则可以执行”default”块。
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
a := 1
b := 2
defer calc("1", a, calc("10", a, b))
a = 0
defer calc("2", a, calc("20", a, b))
b = 1
}
考点:
defer执行顺序
解答:
这道题类似第1题 需要注意到defer执行顺序和值传递
index:1肯定是最后执行的,但是index:1的第三个参数是一个函数,所以最先被调用calc(“10”,1,2)
执行index:2时,与之前一样,需要先调用calc(“20”,0,2)
执行到b=1时候开始调用,calc(“2”,0,2)
最后执行calc(“1”,1,3)。
输出:
10 1 2 3
20 0 2 2
2 0 2 2
1 1 3 4
func main() {
s := make([]int, 5)
s = append(s, 1, 2, 3)
fmt.Println(s)
}
考点:
make默认值和append
解答:
make初始化是由默认值的哦,此处默认值为0
输出:
[0 0 0 0 0 1 2 3]
type UserAges struct {
ages map[string]int
sync.Mutex
}
func (ua *UserAges) Add(name string, age int) {
ua.Lock()
defer ua.Unlock()
ua.ages[name] = age
}
func (ua *UserAges) Get(name string) int {
if age, ok := ua.ages[name]; ok {
return age
}
return -1
}
考点:
map线程安全
解答:
可能会出现fatal error: concurrent map read and map write. 读操作加锁即可。
package main
import (
"fmt"
)
type People interface {
Speak(string) string
}
type Stduent struct{}
func (stu *Stduent) Speak(think string) (talk string) {
if think == "bitch" {
talk = "You are a good boy"
} else {
talk = "hi"
}
return
}
func main() {
var peo People = Stduent{}
think := "bitch"
fmt.Println(peo.Speak(think))
}
考点:
golang的方法集
解答:
编译不通过。
Go本身不具有多态的特性,不能够像Java、C++那样编写多态类、多态方法。但是,使用Go可以编写具有多态功能的类绑定的方法。
比如下面的代码是可行的:
package main
import (
"fmt"
)
type People interface {
Speak(string) string
}
type Stduent struct{}
func (stu *Stduent) Speak(think string) (talk string) {
if think == "bitch" {
talk = "You are a good boy"
} else {
talk = "hi"
}
return
}
func speak(p People) {
fmt.Println(p.Speak("bitch"));
}
func main() {
speak(&Stduent{})
// var peo People = Stduent{}
// think := "bitch"
// fmt.Println(peo.Speak(think))
}
package main
import (
"fmt"
)
type People interface {
Show()
}
type Student struct{}
func (stu *Student) Show() {
}
func live() People {
var stu *Student
return stu
}
func main() {
if live() == nil {
fmt.Println("AAAAAAA")
} else {
fmt.Println("BBBBBBB")
}
}
考点:
interface内部结构
解答:
很经典的题! 这个考点是很多人忽略的interface内部结构。
输出:
BBBBBBB
go中的接口分为两种。
var in interface{}
type People interface {
Show()
}
这两种接口的底层结构如下:
type eface struct { //空接口
_type *_type //类型信息
data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type iface struct { //带有方法的接口
tab *itab //存储type信息还有结构实现方法的集合
data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type _type struct {
size uintptr //类型大小
ptrdata uintptr //前缀持有所有指针的内存大小
hash uint32 //数据hash值
tflag tflag
align uint8 //对齐
fieldalign uint8 //嵌入结构体时的对齐
kind uint8 //kind 有些枚举值kind等于0是无效的
alg *typeAlg //函数指针数组,类型实现的所有方法
gcdata *byte
str nameOff
ptrToThis typeOff
}
type itab struct {
inter *interfacetype //接口类型
_type *_type //结构类型
link *itab
bad int32
inhash int32
fun [1]uintptr //可变大小 方法集合
}
可以看出iface比eface 中间多了一层itab结构。 itab 存储_type信息和[]fun方法集,从上面的结构我们就可得出,因为data指向了nil 并不代表interface 是nil, 所以返回值并不为空,这里的fun(方法集)定义了接口的接收规则,在编译的过程中需要验证是否实现接口。
同理,下面也是非空的:
func Foo(x interface{}) {
if x == nil {
fmt.Println("empty interface")
return
}
fmt.Println("non-empty interface")
}
func main() {
var x *int = nil
Foo(x)
}
func main() {
i := GetValue()
switch i.(type) {
case int:
println("int")
case string:
println("string")
case interface{}:
println("interface")
default:
println("unknown")
}
}
func GetValue() int {
return 1
}
考点:
type
解答:
编译失败,因为type只能使用在interface
func funcMui(x,y int)(sum int,error){
return x+y,nil
}
考点:
函数返回值命名
解答:
在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须有命名。 如果返回值有有多个返回值必须加上括号; 如果只有一个返回值并且有命名也需要加上括号; 此处函数第一个返回值有sum名称,第二个未命名,所以错误。
package main
func main() {
println(DeferFunc1(1))
println(DeferFunc2(1))
}
func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
}()
return t
}
func DeferFunc2(i int) int {
t := i
defer func() {
t += 3
}()
return t
}
考点:
defer和函数返回值
解答:
需要明确一点是defer需要在函数结束前执行。 函数返回值名字会在函数起始处被初始化为对应类型的零值并且作用域为整个函数。
DeferFunc1有函数返回值t作用域为整个函数,在return之前defer会被执行,所以t会被修改,返回4;
DeferFunc2函数中t的作用域为函数,返回1。
package main
import "fmt"
func main() {
s1 := []int{1, 2, 3}
s2 := []int{4, 5}
s1 = append(s1, s2)
fmt.Println(s1)
}
考点:
append
解答:
append切片时候别漏了’…’
func GetValue(m map[int]string, id int) (string, bool) {
if _, exist := m[id]; exist {
return "存在数据", true
}
return nil, false
}
func main() {
intmap:=map[int]string{
1:"a",
2:"bb",
3:"ccc",
}
v,err:=GetValue(intmap,3)
fmt.Println(v,err)
}
考点:
函数返回值类型
解析:
nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”。但是如果不特别指定的话,Go 语言不能识别类型,所以会报错:cannot use nil as type string in return argument.
const (
x = iota
y
z = "zz"
k
p = iota
)
func main() {
fmt.Println(x,y,z,k,p)
}
考点:
iota
输出:
0 1 zz zz 4
package main
const cl = 100
var bl = 123
func main() {
println(&bl,bl)
println(&cl,cl)
}
考点:
常量
解析:
常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用。会报错:cannot take the address of cl
package main
import "fmt"
func main() {
type MyInt1 int
type MyInt2 = int
var i int =9
var i1 MyInt1 = i
var i2 MyInt2 = i
fmt.Println(i1,i2)
}
考点:
Go 1.9 新特性 Type Alias
解析:
基于一个类型创建一个新类型,称之为defintion;基于一个类型创建一个别名,称之为alias。 MyInt1为称之为defintion,虽然底层类型为int类型,但是不能直接赋值,需要强转; MyInt2称之为alias,可以直接赋值。输出为:cannot use i (type int) as type MyInt1 in assignment
package main
func test(x int) (func(),func()) {
return func() {
println(x)
x+=10
}, func() {
println(x)
}
}
func main() {
a,b:=test(100)
a()
b()
}
考点:
闭包引用相同变量
输出:
100
110