进程与线程
进程就是程序执行在操作系统中的一次在执行过程,是系统进行资源分配的基本单位。
线程就是进程的一个执行实例,是程序的最小执行单元,是比进程更小的独立运行的单位。
一个进程可以创建多个线程,同一一个进程中的多个线程可以并发执行。一个程序至少有一个进程,一个进程至少有一个线程。
并发和并行
多线程的程序在单核上运行就是并发。
多线程程序咋在多核上运行就是并行。
并行发生在同一时刻,并发发生在同一时间间隔。
程序控制多线程
在应用中一个app的启动运行相当于一个进程,(进程运行在操作系统上);app内各种按钮的操作相当于线程,线程可以互相通讯。
Go语言中主程序相当于一个进程,各个方法相当于线程,各个线程之间可以互相通讯,但是不同的进程却不可以。(协程是轻量级的线程)
Go语言中协程具有独立的栈空间,共享程序堆空间,调度由开发者控制,协程是轻量级的线程。
在Go语言中通过goroutine实现多线程(协程)。但默认是单线程执行的。Go程序的执行顺序总是先执行源文件的init 函数并总是以单线程执行,并且按照包的依赖关系顺序执行,调用其它包时会递归执行,依然从init函数开始,最后从main包顺序执行。
var starttime int
var endtime int
func main() {
test()
for i := 0; i < 5; i++ {
fmt.Println("main~", strconv.Itoa(i))
//time.Sleep(time.Second)
}
endtime = time.Now().Second()
fmt.Println(endtime - starttime)
}
func test() {
//开始时间
starttime = time.Now().Second()
for i := 0; i < 5; i++ {
fmt.Println("test", strconv.Itoa(i))
//time.Sleep(time.Second)
}
}
可以看到单线程的情况是顺序执行,依然很块即使是以秒为单位,也可以忽略不计,在10000次的循环才1秒。
在Go语言中通过go
关键字开启goroutine非常的方便,开启线程后就不在是单线程执行程序了,新起的线程会使用另一个处理器支持,其他程序任何在主线程中执行,这样多个线程就并发执行了。
在使用多线程时一定义确保main执行时间比其他线程长,不然其他线程没执行完,主线先先停了。就像马拉松比赛,你跑的太慢,人家赛事结束了你才跑到,发现啥也没了。
主线程执行时间一定要比其他线程长,以下就是错误案例:
func main() {
go test()
fmt.Println(endtime - starttime)
}
主线程没有程序,很快结束,新线程还没开始执行,所以啥也没输出。在主线程休眠一秒钟,如下:
func main() {
go test()
time.Sleep(time.Second)
fmt.Println(endtime - starttime)
}
休眠后才有打印,而且看时间差还是负数。
goroutine
主线程时物理线程,直接作用再cpu上时重量级的,goroutine是轻量级的。
MPG是goroutine的调度模型。MPG:m是操作系统的主线程,p是协程执行需要的上下文,G是协程。
对于调度来说,当前若干程序在一个cpu上运行就是并发,在不同cpu上运行就是并行;
线程(协程通讯)
不同的线程之间有两种通讯方式全局变量加锁,channel管道。
全局变量加锁
如果不对全局变量加锁,线程都在写入会造成数据错误,因此Go语言不支持无锁写入数据,通过加互斥所解决问题。
import (
"container/list"
"fmt"
"time"
)
var lis = list.New()
func main() {
//test(10)
for i := 10; i <= 20; i++ {
go test(i)
}
time.Sleep(time.Second * 2)
for i := lis.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}
}
func test(n int) {
res := 1
for i := 1; i < n; i++ {
res = res * i
}
lis.PushBack(res)
}
在上面的程序中,通过不同线程执行循环并插入到单列表中list.List
,根据单链表的特性,使用尾插法依次插入。但是由于是多线程,那个先执行完谁先插入,另外碰到同一时间完成,或者上一个还未插入完就要执行下一个插入的情况时就会出错。(单链表使线程安全的,这里模拟错误。)
import (
"container/list"
"fmt"
"sync"
"time"
)
var lis = list.New()
// 声明一个全局锁
// Mutex是互斥的
var lock sync.Mutex
func main() {
//test(10)
for i := 10; i <= 20; i++ {
go test(i)
}
time.Sleep(time.Second * 2)
//加锁
lock.Lock()
for i := lis.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}
//解锁
lock.Unlock()
}
func test(n int) {
res := 1
for i := 1; i < n; i++ {
res = res * i
}
//加锁
lock.Lock()
lis.PushBack(res)
//解锁
lock.Unlock()
}
sync.Mutex
是一个全局的互斥锁,用于对重要数据加锁,在上述代码中lis为全局数据,由于多线程操作需要对其加锁。
- 写入时加锁
//加锁
lock.Lock()
lis.PushBack(res)
//解锁
lock.Unlock()
- 读出时加锁
//加锁
lock.Lock()
for i := lis.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}
//解锁
lock.Unlock()
全局变量加锁时使用了time.Sleep(time.Second * 2)
使所有数据写入完毕,但是实际上主线程是不知道程序写入完毕,在实际中可以会出现一边读一边写入的问题,如果还没写完就在读就会出现脏读,幻读,重复都等问题。因此读写都是需要加锁。
全局锁是不完美了,主线程并不知道全部写入需要多长时间,所以无法设置等待时间(等数据写完),若线程很多,对全局变量读写会很复杂,Go提供了新的通讯方式管道channel
来解决这些问题。
channel管道
单纯的并发是没有意义的,并发的线程应该是同一为主进程服务的,就需要数据的共享,数据共享时不同线程的数据的操作不同,因此容易造成错误。Go除了锁还提供了goroutine来解决这一问题。
channel是一个队列,数据总是先进先出,因此是线程安全的,多goroutine时,无需加锁。在goroutine操作时数据经过处理进入channel,该数据一直为本次的线程服务,除非线程结束放行数据。(先进先出,上一个不走下一个出不去)
- 声明
var 变量名 chan 数据类型
channel是引用类型,必须初始化才能写入数据,管道也是有类型的。声明管道后必须通过make
关键字初始化才可以使用,否则会报错。
var intchan chan int
intchan = make(chan int, 3)
- 写入数据
管道通过<-
符号插入数据
intchan <- 10
num := 100
intchan <- num
- 容积
channel放入数据后会有两个长度,容积cap
和长度len
,分别表示定义的chan的大小和当前chan存储的元素的个数。
//查看容量和数据长度
println(cap(intchan))
println(len(intchan))
管道的容积在定义时就声明了,后续不会再发生变化。但是存储数据的长度会随着元素的存入和取出发生变化。
- 管道取值
item1 := <-intchan
管道的取值和管道存数据使用的符号一致<-
,知识顺序反转,表示取出数据。
item1 := <-intchan
println(item1)
print(len(intchan))
管道在是一个队列数据结构,在存放数据是是先进先出。
func test2() {
var intChan chan int
intChan = make(chan int, 5)
var aList []int
aList = append(aList, 1, 2, 3, 4, 5)
//print(aList[0])
intChan <- aList[0]
intChan <- aList[3]
intChan <- aList[4]
a := <-intChan
b := <-intChan
c := <-intChan
fmt.Printf("set data %d-%d-%d,but get data %d-%d-%d", aList[0], aList[3], aList[4], a, b, c)
}
使用channel时,只能存放指定的数据类型;达到数据的容积后就不能在存放数据了;取出最后一个数后也不能在取数据否则会报错。
//map类型的数据
func mapChan() {
var mapChan chan map[string]string
mapChan = make(chan map[string]string, 5)
a := map[string]string{
"1": "北极",
"2": "南极",
}
mapChan <- a
var b map[string]string
b = <-mapChan
print(b["1"])
}
//结构体类型
func structChan() {
structChan := make(chan Person, 3)
per := Person{
1,
"xiaoxu",
"男",
18,
}
structChan <- per
a := <-structChan
fmt.Print(a)
}
除了上面两种最主要的数据类型外channel也支持接口类型。
- channel的遍历和关闭
chanen也是可以关闭的,Go语言提供了内置函数close
来关闭管道。
close(structChan)
遍历时需要关闭管道,否则会报死锁的错误。
func bainli() {
var intChan = make(chan int, 101)
for i := 0; i < 10; i++ {
intChan <- i
}
close(intChan)
for item := range intChan {
fmt.Println(item)
}
}
管道应用,使用管道读取数据。
func main() {
intchan := make(chan int, 5)
go writeData(intchan)
go readData(intchan)
time.Sleep(time.Second * 2)
}
func writeData(intChan chan int) {
for i := 0; i < 10; i++ {
intChan <- i
}
close(intChan)
}
func readData(intChan chan int) {
for {
a, ok := <-intChan
if ok {
fmt.Println(a)
} else {
fmt.Println("读取完毕")
break
}
}
}
在上面的管道中,数据读取
读管道哥写入管道的数据不一样,但是必须双方都存在。
线程应用
在goroutine和channel后,实现并发就需要将两者结合,实现并行或并发。
判断10000一类的所有素数?并将素数相加
上述问题无并发操作的实现方案是通过for循环
go中通过goroutine的实现并发更快捷
package main
import (
"fmt"
"time"
)
func main() {
intchan := make(chan int, 100)
var sum int = 0
//start := time.Now().UnixMilli()
write(intchan, 80)
read(intchan, sum)
//end := time.Now().UnixMilli() - start
//fmt.Println("执行时间(微秒)", end)
// fmt.Println("last sum is :", sum)
time.Sleep(time.Second * 3)
}
func write(dataChan chan int, n int) {
for i := 1; i < n; i++ {
dataChan <- i
fmt.Println("写入数据", i)
}
close(dataChan)
}
func read(data chan int, sun int) {
sun = 0
for item := range data {
if sushu(item) != 0 {
sun += item
fmt.Println("读取数据", item)
} else {
continue
}
}
}
func sushu(a int) int {
for i := 2; i < a; i++ {
if a%i != 0 {
a = a
} else {
a = 0
}
}
return a
}
线程一定要为主线程或者进程服务不然该线程就没有意义。
成功读取大素数
//将主函数不用多线程执行,并记录执行时间,改到基数
func main() {
intchan := make(chan int, 100005)
var sum int = 0
start := time.Now().UnixMilli()
write(intchan, 100000)
read(intchan, sum)
end := time.Now().UnixMilli() - start
fmt.Println("执行时间(微秒)", end)
// fmt.Println("last sum is :", sum)
time.Sleep(time.Second * 10)
}
在不用线程的情况下执行时间如下图
对函数改造记录执行执行,对于线程来说由资源调度完成需要记录线程开始的时间,和主线程开始的时间,用于比较,分别记录各线程执行时间哥主线程执行时间。
package main
import (
"fmt"
"time"
)
func main() {
start := time.Now().UnixMilli()
intchan := make(chan int, 100005)
var sum int = 0
fmt.Println("主线程开始时间", time.Now().UnixMilli())
go write(intchan, 100000)
go read(intchan, sum)
mix := time.Now().UnixMilli() - start
fmt.Println("执行时间(微秒)", mix)
time.Sleep(time.Second * 2)
}
func write(dataChan chan int, n int) {
fmt.Println("写入线程开始时间", time.Now().UnixMilli())
start := time.Now().UnixMilli()
for i := 1; i < n; i++ {
dataChan <- i
//fmt.Println("写入数据", i)
}
close(dataChan)
end := time.Now().UnixMilli()
fmt.Println("写入线程执行时间", end-start)
}
func read(data chan int, sun int) {
fmt.Println("读取线程开始时间", time.Now().UnixMilli())
start := time.Now().UnixMilli()
sun = 0
for item := range data {
if sushu(item) != 0 {
sun += item
//fmt.Println("读取数据", item)
} else {
continue
}
}
end := time.Now().UnixMilli()
fmt.Println("读取线程执行时间", end-start)
}
func sushu(a int) int {
for i := 2; i < a; i++ {
if a%i != 0 {
a = a
} else {
a = 0
}
}
return a
}
对于改造后的函数执行100000个素数查找,由上图可知读取线程和写入线程同时开启,它们的执行时间各有不同,根据打印显示主线程执行时间为1微妙,写入时间为1微妙,读取的长一点为1670微妙,由于主线程休眠了2秒因此读取线程也正常完成。对比单线程的执行时间块了近5倍。
进程是面向操作系统的,线程是面向程序的。
在上述程序中让主函数休眠2分钟time.Sleep(time.Second * 120)
,在windows的任务管理中能够看到这个进程。
在一个程序中mian函数为程序的入口,运行的所有程序构成一个进程,在一个进程中默认会有一个主要的线程,线程必须运行在进程中。(操作系统的线程与进程)。
每个线程都为进程服务,因此线程的数据必须是共享的,而且是线程安全的,在程序中全局变量为共享的变量,因此如果只用全局变量的话就要涉及安全锁的问题,而Go语言中设计的channel就是为了解决共享数据的问题,从上述程序中可以看出,在使用通道channel是,读写分别对线程操作,并且使用锁的问题,而且其本身也是线程安全的。
甚至为了更快多开几个线程写入数和读取数据,写入方法改造,多线程分段写入。
func write(dataChan chan int, left, right int) {
fmt.Println("写入线程开始时间", time.Now().UnixMilli())
start := time.Now().UnixMilli()
for i := left; i < right; i++ {
dataChan <- i
//fmt.Println("写入数据", i)
}
//close(dataChan)
end := time.Now().UnixMilli()
fmt.Println("写入线程执行时间", end-start)
}
//多线程写入
go write(intchan, 1, 100000)
go write(intchan, 100001, 200000)
管道是线程安全的,一定是上一个写完,下一个才可以写入,因此线程对数据的写入顺序无法判断,但唯一确定的是,写入完成后,一定存在1~200000这些数。
管道的读取也是如此,因为管道是队列的数据结构,先入队的必须先出队才可以读取下一个数据。因此多线读取数据也是安全的。
线程尽量使用defer
和recover
处理错误,以免程序故障。
//匿名函数处理错误
defer func () {
err:= recover()
if err != nil{
fmt.Println("err",err)
}
}()