并发编程 & 网络编程
go的并发编程
并发编程开发将一个过程按照并行算法拆分为多个可以独立执行的代码块,从而充分利用多核和多处理器提高系统吞吐率
顺序,并行与并发
- 顺序是指发起执行的程序只能有一个
- 并发是指同时发起(同时处理)的程序可以有多个(单车道并排只能有一辆,可同时驶入路段多辆车)
- 并行是指同时执行(同时做)的程序可以有多个(多车道并排可以有多个车)
例程:
Go语言中每个并发执行的单元叫做Goroutine,使用go关键字后接函数调用来创建一个Goroutine
例程
并发的执行单元就是例程
go + 函数:启动一个例程
主例程:main函数
工作例程:go+函数
说明
当主进程结束,工作例程也会随着结束,不管例程有没有执行完,所以得想办法让例程执行完,主进程再结束。
操作
- 写一个time.sleep主进程执行完,等待多长时间,这样给例程时间结束,不过也不能确定例程是否完成
- 定义计数信号量:定义一共有多少例程,在工作例程执行完,让信号量产生,在主例程去等待,当主进程没有收到信号量产生,就一直等待例程的结束。
func PrintChars(name string) {
for ch := 'A'; ch <= 'Z'; ch++ {
fmt.Printf("%s: %c\n", name, ch)
runtime.Gosched() // 结束cpu的占用,所以当前循环会退出,等下个程序执行
}
}
func main() {
go PrintChars("1")
go PrintChars("2") // 作为例程,当主进程结束,例程也就跟着结束
PrintChars("3") // 做为主进程
time.Sleep(time.Second * 3) // 主进程结束,子进程也结束,为了防止子进程没有结束,主进程等待几秒
}
计数信号量
用来等待例程结束
&sync.WaitGroup{}
func PrintChars(name string, group *sync.WaitGroup) {
for ch := 'A'; ch <= 'Z'; ch++ {
fmt.Printf("%s: %c\n", name, ch)
runtime.Gosched() // 结束cpu的占用,所以当前循环会退出,等下个程序执行
}
group.Done() // 产生一个信号量
}
func main() {
group := &sync.WaitGroup{} // 声明一个函数
group.Add(2) // 定义有2个信号量
go PrintChars("1", group)
go PrintChars("2", group)
PrintChars("3", group)
group.Wait() // 当定义的2个信号量没有出现就一直等待,出现就结束
}
匿名例程
因为闭包使用函数外变量,当例程执行时外部变量已经发生变化,导致答应内容不正确,可在创建例程时通过函数传递参数(值拷贝)方式避免
func main() {
group := &sync.WaitGroup{} // 声明一个变量
n := 2
group.Add(n) // 定义有2个信号量
for i := 1; i <= n; i++ {
go func(id int) {
for ch := 'A'; ch <= 'Z'; ch++ {
fmt.Printf("%d:%d:%c\n", id, i, ch)
runtime.Gosched()
}
group.Done()
}(i) // 匿名一定要传参,否则例程执行比for循环晚,for为2了,go才开始执行,那闭包中的i获取的值巨不正确
}
group.Wait()
}
- 小案例
之所以这里结果不固定,是因为并不是顺序执行,使用例程随机执行,所以影响结果。
func main() {
var counter int
group := &sync.WaitGroup{} // 创建变量
incr := func() { // 创建匿名函数
defer group.Done()
for i := 0; i <= 100; i++ {
counter++
runtime.Gosched()
}
}
desc := func() {
defer group.Done()
for i := 0; i <= 100; i++ {
counter--
runtime.Gosched()
}
}
for i := 0; i < 10; i++ { // 循环1-10
group.Add(2) // 定义2个例程
go incr() // 例程1执行函数
go desc() // 例程2执行函数
}
group.Wait() // 等待2个例程执行完成
fmt.Println(counter) // -8 结果不固定
}
管道
chan类型为管道类型
var 变量名 chan string: string定义管道类型
初始化:var 变量名 chan string = make(chan string)
func main() {
var notice chan string = make(chan string) // 定义管道,并设置管道类型为string
fmt.Printf("%T %v\n", notice, notice) // chan string 0xc0000160c0
fmt.Println(len(channel)) // 获取管道中元素数量,当全部读取后,管道为0
channel := make(chan string, 2)
channel <- "2"
fmt.Println(<-channel)
close(channel) // 关闭管道后能读取管道,但是不能写入
channel <- "2" // 报错
// 遍历管道中的元素,必须close,不然读管道会卡死
channel := make(chan string, 2)
channel <- "2"
channel <- "3"
close(channel)
for ch := range channel { // 这里会一直读管道,会导致死锁,需要某个例程能结束管道
fmt.Println(len(channel))
}
}
案例①
func main() {
var notice chan string = make(chan string) // 定义管道,并设置管道类型为string
fmt.Printf("%T %v\n", notice, notice) // chan string 0xc0000160c0
go func() {
fmt.Println("go start")
notice <- "xxx" // 这里发消息到管道,主例程收到消息,才会执行最后的end
fmt.Println("go end")
}()
fmt.Println("start") // 主例程执行
fmt.Println(<-notice) // 这里如果没有收到消息会阻塞,所以会执行匿名函数
fmt.Println("end") // 保证了end最后执行,不会在例程前面执行
}
案例②
- 一次的结果完成,发送消息一次
- 遍历10次管道,当收到管道1次消息,说明遍历1次成功
func PrintChars(name int, channel chan int) {
for ch := 'A'; ch <= 'Z'; ch++ {
fmt.Printf("%d: %c\n", name, ch)
runtime.Gosched()
}
channel <- 0
}
func main() {
var channel chan int = make(chan int)
for i := 0; i < 10; i++ {
go PrintChars(i, channel)
}
for i := 0; i < 10; i++ {
<-channel
}
fmt.Println("over")
}
只读写管道
虽然读管道和写管道不是同一个名称,不过底层都是用的一个管道,所以写管道最终将数据写到channel中,读管道通过channel去读取。
var channel chan int = make(chan int, 5)
// 只读管道
var rchannel <-chan int = channel // 只读管道和只写管道底层还是用的同一个管道
// 只写管道
var wchannel chan<- int = channel
wchannel <- 1
fmt.Println(<-rchannel) // 1
- 例程只读写管道
var channel chan int = make(chan int, 5)
go func(channel <-chan int) { // 定义管道为只读管道
fmt.Println(<-channel) // 0
}(channel) // 将全局的管道传入
go func(channel chan<- int) { // 定义管道为只写管道
channel <- 0 // 写入0到管道中,底层到了channel中,能被上面的例程读取
}(channel) // 将全局管道传入
time.Sleep(time.Second * 3)
Select Case
类似于switch case,从任意一个通道读取数据都结束
func main() {
channel01 := make(chan int, 2)
channel02 := make(chan int, 2)
go func() {
channel01 <- 1
}()
go func() {
channel02 <- 2
}()
select { // select,case实现随意哪个通道能读取数据都可以
case <-channel01:
fmt.Println("channel01")
case <-channel02:
fmt.Println("channel02")
}
}
- select case 遍历元素存储到切片
func main() {
channel := make(chan int, 1)
slice := make([]int, 10)
// 随机写入数字到通道,通过切片存储
for i := 0; i < 10; i++ {
select {
case channel <- 0:
case channel <- 1:
case channel <- 2:
case channel <- 3:
case channel <- 4:
case channel <- 5:
case channel <- 6:
case channel <- 7:
}
slice[i] = <-channel // 不能append存储元素,切片已经被初始化长度为10,前10都是0
}
fmt.Println(slice) // [3 5 6 5 5 6 5 2 0 3]
}
- 练习
设置超时时间为3秒,一旦3秒没有执行完毕,就认为超时,不再执行,否则,执行改代码
// 方法一:
func main() {
rand.Seed(time.Now().Unix())
// 生成两个管道
result := make(chan int)
timeout := make(chan int)
// 创建例程,创建1-10的随机数,休眠随机数秒数再写入信息到管道
go func() {
interval := time.Duration(rand.Int()%10) * time.Second
fmt.Println(interval)
time.Sleep(interval)
result <- 0
}()
// 创建例程,等待3秒再写入消息到管道,如果上面的例程大于三秒,此例程就优先执行
go func() {
time.Sleep(3 * time.Second)
timeout <- 0
}()
// 优先接受到管道信息的输出
select {
case <-result:
fmt.Println("执行完成")
case <-timeout:
fmt.Println("执行超时")
}
}
// 方法二:
// 使用time.After方法返回只读类型的通道,指定时间为3秒
func main() {
rand.Seed(time.Now().Unix())
result := make(chan int)
// 创建例程,创建1-10的随机数,休眠随机数秒数再写入信息到管道
go func() {
interval := time.Duration(rand.Int()%10) * time.Second
fmt.Println(interval)
time.Sleep(interval)
result <- 0
}()
// 优先接受到管道信息的输出
select {
case <-result:
fmt.Println("执行完成")
case <-time.After(3 * time.Second): // time.After返回只读管道,但只读一次
fmt.Println("执行超时")
}
}
after & tick
time.After:返回通道类型,只能读一次
time.Tick: 返回通道类型,能多次读取,但有指定时间
func main() {
fmt.Println(time.Now())
fmt.Println("after")
channel := time.After(3 * time.Second)
fmt.Println(<-channel) // time.After只能读一次,不能再次读取
fmt.Println("tick")
ticker := time.Tick(3 * time.Second)
fmt.Println(<-ticker) // time.Tick可以读取多次,指定秒数后可以再次读取
fmt.Println(<-ticker)
fmt.Println(<-ticker)
for now := range ticker { // 实现循环,每隔3秒执行一次
fmt.Println(now)
}
}
锁
- sync.Metex 互斥锁
func main() {
var counter int
group := &sync.WaitGroup{}
// 使用锁机制,保证抢占到锁和释放锁里面的操作必须执行完
lock := &sync.Mutex{} // 使用sync包里面的互斥锁方法
incr := func() {
defer group.Done()
for i := 0; i <= 100; i++ {
lock.Lock() // 加锁
counter++
lock.Unlock() // 释放锁
runtime.Gosched()
}
}
desc := func() {
defer group.Done()
for i := 0; i <= 100; i++ {
lock.Lock() // 加锁
counter--
runtime.Gosched()
lock.Unlock() // 释放锁
}
}
for i := 0; i < 10; i++ {
group.Add(2)
go incr()
go desc()
}
group.Wait()
fmt.Println(counter) // 0
}
- 原子性
- automic.AddInt32(&Int32,int)
- 作用有限,针对int32和int64
func main() {
var counter int32 = 0
group := &sync.WaitGroup{}
incr := func() {
defer group.Done()
for i := 0; i <= 100; i++ {
atomic.AddInt32(&counter, 1) // 使用automic保证原子性,传入指针,和加1
runtime.Gosched()
}
}
decr := func() {
defer group.Done()
for i := 0; i <= 100; i++ {
atomic.AddInt32(&counter, -1)
}
}
for i := 0; i <= 10; i++ {
group.Add(2)
go incr()
go decr()
}
group.Wait()
fmt.Println(counter) // 0
}
- 读写锁
RWMutex
Lock: 锁
RLock:读锁
ULock:释放锁
RULock:读释放锁
场景:
当数据有事务正在写操作,那么读的数据会存在脏数据,所以需要针对不同场景加锁
RLock --> Rlock: 两个读锁,不阻塞,使用URLock释放锁
- 死锁
产生死锁原因:
- 超过管道中指定数量
- 有写管道,但没读管道
- 有读管道,没有写管道
func main() {
channel := make(chan string, 2) // 定义管道中的数量
channel <- "x" // 写管道
channel <- "y"
fmt.Println(<-channel) // 读管道
fmt.Println(<-channel)
}
sync
sync.Once
就算for循环10次,但定义了sync.Once也只执行1次
func main() {
// 使用sync.Once函数只执行一次
var once sync.Once
for i := 0; i < 10; i++ {
once.Do(func() {
fmt.Println(i) // 0
})
}
}
sync.Map
实现存储为map
store存储
load读取
delete删除
func main() {
var users sync.Map
users.Store(10, "tcy")
users.Store(20, "twg")
if value, ok := users.Load(10); ok { // 因为是map类型,所以需要将key传入
fmt.Println(value, ok) // tcy true 只有这个结果可以输出
}
if value, ok := users.Load(30); ok { // 没有30的key,判断false不执行
fmt.Println(value)
}
users.Delete(10) // 删除key为10的map
if value, ok := users.Load(10); ok {
fmt.Println(value)
}
}
sync.pool
创建对象池,避免重复创建对象销毁对象
func main() {
// 对象池
pool := sync.Pool{ // 为结构体类型
New: func() interface{} { // 结构体名为new,值为函数,返回接口
fmt.Println("new")
return 1
},
}
x := pool.Get() // 因为首次池子没有对象,这里创建对象
fmt.Println(x) // 1
pool.Put(x) // 使用完成放回池子
x = pool.Get() // 再次使用,并没有创建
x = pool.Get() // 但再次使用就需要创建,因为put放回去的已经被上个get使用,结果为new
}
runtime
运行时
func main() {
fmt.Println(runtime.GOROOT()) // 查看goroot目录
fmt.Println(runtime.NumCPU()) // 查看cpu的数量,作用于限制一个进程能使用多少个cpu
fmt.Println(runtime.GOMAXPROCS(1)) // 设置系统只能使用1核
fmt.Println(runtime.NumGoroutine()) // 例程数量
}
点对点聊天
Socket: 套接字,是操作系统提供TCP和UDP的操作API,应用程序使用套接字时,可以设置对端的IP地址,端口号,实现数据的发送和接受。
net模块:
net包提供了对socket编程的支持,socket编程分服务端和客户端编程,对服务端可以使用函数Listen创建监听,对于客户端使用函数Dial连接服务器
TCP服务端开发:
- 创建监听服务
- 循环接受客户端连接
- 数据处理(向客户端发送数据/读客户端发送的数据)
- 关闭监听服务
TCP客户端开发:
- 连接服务器
- 数据处理(向服务端发送数据/读服务端发送的数据)
- 关闭连接
- 创建简单服务端,客户端一次性连接(简易版)
// server
func main() {
addr := "0.0.0.0:9999"
listen, err := net.Listen("tcp", addr) // 服务端创建监听
if err != nil {
fmt.Println(err)
os.Exit(-1) // 出现错误退出
}
fmt.Println("Listen:", addr)
client, err := listen.Accept() // 获取客户端连接
client.Write([]byte(time.Now().Format("2006-01-02 15:04:04"))) // 给客户端发送数据
client.Close() // 关闭客户端
listen.Close() // 关闭服务端
}
// client
func main() {
// 客户端读取数据
addr := "127.0.0.1:9999"
conn, err := net.Dial("tcp", addr) // 客户端创建连接服务器
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
// 连接成功后,读取服务端发送的数据
bytes := make([]byte, 1024)
if n, err := conn.Read(bytes); err == nil {
fmt.Println(string(bytes[:n])) // 使用string转换字节切片为字符串
} else {
fmt.Println(err)
}
conn.Close() // 关闭客户端连接
}
- 客户端与服务端聊天,持续交互,完整版
服务端:
先写数据到客户端,再读取客户端返回的数据
客户端:
先读取服务端发送的信息,再发送数据到服务端
// 服务端
func main() {
addr := "0.0.0.0:9999"
listen, err := net.Listen("tcp", addr) // 服务端建立Listen监听
if err != nil {
fmt.Println(err)
os.Exit(-1) // 出现错误退出
}
fmt.Println("Listen:", addr)
defer listen.Close() // 关闭服务端,延迟关闭服务端
input := bufio.NewScanner(os.Stdin) // 将当前控制台输入为bufio
for {
client, err := listen.Accept() // 获取客户端连接
if err == nil {
reader := bufio.NewReader(client) // 将客户端请求放入到bufio中
writer := bufio.NewWriter(client)
fmt.Printf("客户端%s连接成功\n", client.RemoteAddr())
for {
// 先写数据再读客户端发送过来的数据
fmt.Print("请输入需要给客户端发送的数据:q为退出 ")
input.Scan() // 扫描上面输入的结果
test := input.Text() // 获取结果赋值给变量test
if test == "q" { // 如果服务端写入q表示退出
break
}
_, err := writer.WriteString(test + "\n") // 将控制台写入数据通过bufio发送至客户端
writer.Flush() // flush表示发送操作
if err != nil {
break
}
line, err := reader.ReadString('\n') // 读取客户端发送过来的数据
if err != nil {
break
}
fmt.Println("客户端:", strings.TrimSpace(line))
}
fmt.Printf("客户端%s关闭", client.RemoteAddr()) // 获取客户端地址
client.Close() // 关闭获取的客户端
}
}
}
// 客户端
func main() {
addr := "127.0.0.1:9999"
conn, err := net.Dial("tcp", addr) // 与服务端建立连接
if err != nil {
fmt.Println(err)
os.Exit(-1)
}
defer conn.Close() // 延迟关闭与服务端的连接
reader := bufio.NewReader(conn) // 将服务端连接信息存放在bufio中
writer := bufio.NewWriter(conn)
input := bufio.NewScanner(os.Stdin) // 将控制台输入信息存放在bufio中
for {
line, err := reader.ReadString('\n') // 读取服务端发送过来的数据
if err != nil {
break
}
fmt.Println("服务器:", line)
fmt.Print("请输入 q为退出:")
input.Scan() // 与服务端交互发送至服务端
if input.Text() == "q" {
break
}
_, err = writer.WriteString(input.Text() + "\n") // 将控制台信息发送至服务端
writer.Flush()
if err != nil {
break
}
}
}