关于编程语言中的进程、线程、调度
对于操作系统来说,一个任务就是一个进程(Process),比如打开一个浏览器就是启动一个浏览器进程,打开一个记事本就启动了一个记事本进程。
有些进程不止同时干一件事,比如Word,它可以同时进行打字、拼写检查、打印等事情。在一个进程内部,要同时干多件事,就需要同时运行多个“子任务”,我们把进程内的这些“子任务”称为线程(Thread)。
同一个 CPU 在同一时间只能执行一个任务,这是物理上的限制。所以任务一般只会处于正在执行或者未执行(等待或者终止)的状态;这些而用于处理这些任务的 CPU 往往都是不可再分的。
在操作系统的进程调度器(Process Scheduler)中,待调度的任务就是线程,Go 语言的调度器与操作系统的调度器面对的是几乎相同的场景,其中的任务是 Goroutine,可以分配的资源是在 CPU 上运行的线程。
1.goroutine
在go语言里面实现并发很容易。
独立运行的任务被称为goroutine,goroutine不同于线程,它的运行表面上看似乎都是在同时运行,但是由于计算机通常只具有有限数量的处理单元,所以,从技术上来说,goroutine并不是真正的同时在运行。其实相当于是在一条时间轴上,go语言会分配时间,让他们轮流运行。至于运行顺序,那将是随机的。
启动goroutine就像调用函数一样简单,只需要在调用的前面加上go就可以了。
func main() {
for i := 0;i <= 5;i++{
go printNum(i)
}
time.Sleep(1 * time.Second)
}
func printNum(i int){
fmt.Println(strconv.Itoa(i))
}
上面这段代码在每次运行时,你都可以看到不同的运行结果,这也证实了运行顺序的随机性。
但是一旦注释掉time.Sleep(1 * time.Second)这句代码,你就得不到任何输出。
因为golang的主函数(其实也是跑在一个goroutine中)并不会等待其他goroutine结束。如果主goroutine结束了,所有其他goroutine都将结束。
2.channel(通道)
和go语言的其他类型一样,你可以将通道用作变量、传递至函数、存储在结构中,或者其它你想做的事情。
通道是负责在多个goroutine之间进行通信的。
创建一个通道使用make函数创建 ,如 c := make(chan int),chan表示是通道类型,int表示这个通道的接受的值类型。
数据使用 <- 来进行传递,放在chan变量的左边表示把chan接收到的值传递给一个变量,放在右边表示将数据传递给chan。
c <- 99 //表示将99传递给chan变量c
v := <- c //表示将chan变量c里面的值赋值给变量v
当在执行发送操作,也就是正在执行将值传递给chan变量的操作时,执行该操作的goroutine任务会处于等待的过程中,无法执行其它操作。但是其它未等待的扔可以继续自由的运行。执行接收操作的也一样,接收的会等待接收到下一个数据以后才会继续执行。
func main() {
c := make(chan int)
for i := 0;i <= 5;i++{
go printNum(i,c)//1.for循环先会创建6个goroutine任务,去执行,对于goroutine的执行是无序的。
}
time.Sleep(200) //4.增加sleep是为了让读者更直观的感受到chan等待时后续代码是不执行的。因为等待,所以第3步发送给chan的数据没有接收者。
for i := 0;i <= 5;i++{
fmt.Println("get " + strconv.Itoa(i))//5.等待结束,开始执行打印
newi := <- c //6.接收到了通道传来的值。但是接收到的6个数据肯定是按通道发送数据的顺序接收到的。但是下面一条语句执行的时机就不一定了
fmt.Println("get end " + strconv.Itoa(newi))7.这个执行肯定是要在第6步执行完成以后,再根据系统分配,随机执行了。但是执行的肯定都是已经接收到值了的。
}
time.Sleep(1 * time.Second)
}
func printNum(i int,c chan int){
fmt.Println("go " + strconv.Itoa(i)) //2.先执行到这里,创建出的6个goroutine会先执行这一句,即使下面一句代码的通道处于等待状态也不影响6个goroutine这句的执行。
c <- i //3.当第一个goroutine执行到这里的时候发现没有通道接收值的时候,就会在这里等待。后面5个goroutine会在这里排队,后面5个排队执行的顺序也要看系统分配。
fmt.Println("go end " + strconv.Itoa(i))7.这个执行肯定是要在第3步执行完成以后,再根据系统分配,随机执行了。但是执行的肯定都是已经发送过值了的。
}
上面这段代码读者运行一下就可以很清楚channel的等待机制。
具体分析可以看上面代码的注释部分。
对于chan的结束发送,我们也有方法能够处理。使用close()方法可以在channel发送完数据以后关掉它,当然,如果接受channel数据的方法是使用for循环,那么在main函数退出之前会一直接受到定义的chan类型的零值。这个时候可以使用range来进行处理。
func main() {
var c = make(chan string)
go func (c chan string){
for n := range c{
println(n)
}
}(c)
sendChan(c)
}
func sendChan(c chan string) {
c <- "hello"
c <- "world"
c <- "你好"
close(c)
}
除了上面的方式以外,我们还可以使用sync.WaitGroup来进行等待处理。
func main() {
var wg sync.WaitGroup
var c = make(chan string)
go func (c chan string,wg *sync.WaitGroup){
for n := range c{
println(n)
wg.Done()
}
}(c,&wg)
wg.Add(3)
sendChan(c)
wg.Wait()
}
func sendChan(c chan string) {
c <- "hello"
c <- "world"
c <- "你好"
}
3.select处理通道
上面介绍channel的时候使用的都是int类型的chan,所以这其实是一种理想情况,真正的使用时,可能会存在不同类型的chan。这个时候我们就可以使用select来处理不同类型的chan了。
func main() {
c := make(chan int)
for i := 0;i <= 5;i++{
go printNum(i,c)
}
timeOut := time.After(time.Second)
for i := 0;i <= 5;i++{
select {
case newi := <- c:
fmt.Println(strconv.Itoa(newi))
case <- timeOut:
fmt.Println("运行超时")
return
}
}
}
func printNum(i int,c chan int){
time.Sleep(time.Duration(rand.Intn(1500)) * time.Millisecond)
c <- i
}
4.阻塞和死锁
当goroutine在等待通道的发送或者接收操作的时候,我们就说它被阻塞了。当时在go语言中,除goroutine本身占用的少量内存之外,被阻塞的goroutine并不消耗任何资源。
goroutine会静静的停在那里,等待导致它阻塞的事情发生,然后解除阻塞。
当一个或者多个goroutine因为某些永远无法发生的事情而被阻塞时,我们称之为死锁。
5.互斥锁
在go语言中,当有2个或者多个goroutine同时使用一个共享值的时候,程序可能会出错。两个goroutine同时读取相同的事物并不会产生问题,如果一个goroutine在写入事物的同时,另一个goroutine尝试写入或者读取相同的事物,那么后者的行为将是未定义的。
所以为了解决这种问题,我们需要使用到互斥锁。goroutine可以通过互斥锁阻止其他goroutine在同一时间进行某些事情。
互斥锁具有Lock和Unlock两个方法。如果持有互斥锁的goroutine因为某些原因而尝试锁定同一个互斥锁,那么就会引发死锁。
func main() {
p := people{}
p.saveSameName("张三")
}
type people struct {
mu sync.Mutex
num int32
}
func (p *people)saveSameName(name string)int32{
p.mu.Lock()
defer p.mu.Unlock()
p.num++
return p.num
}