Go语言学习(I/O、goroutine、channel)

本文介绍了Go语言中的文件操作,包括获取文件信息、创建/删除文件和目录、IO读写、文件复制。接着讨论了Goroutine的并发执行和同步机制,如Mutex、WaitGroup、通道和死锁问题。还提到了通道的使用和缓冲通道的概念,以及定时器和选择器的运用。
摘要由CSDN通过智能技术生成

IO

获取文件信息 file

计算机中的文件时存储在外部介质(通常是磁盘)上的数据集合,文件分为文本文件二进制文件

file类是在os包中的,封装了底层的文件描述符和相关信息,同时封装了Read和Write的实现。

  • 获取文件信息

  • 创建目录、创建文件

  • IO读、写

  • 文件复制

  • 断点续传

  • bufio

package main

import (
    "fmt"
    "os"
)

/*
   type FileInfo interface {
        Name() string       // base name of the file
        Size() int64        // length in bytes for regular files; system-dependent for others
        Mode() FileMode     // file mode bits
        ModTime() time.Time // modification time
        IsDir() bool        // abbreviation for Mode().IsDir()
        // 获取更加详细的文件信息
        Sys() any           // underlying data source (can return nil)
    }
*/
func main() {
    // 获取某个文件的状态
    // func Stat(name string) (FileInfo, error)
    fileinfo, err := os.Stat("D:\\Environment\\GoWorks\\src\\xuego\\lesson06\\Demo06.go")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(fileinfo.Name())    // Demo06.go
    fmt.Println(fileinfo.IsDir())   // false
    fmt.Println(fileinfo.ModTime()) // 2023-02-26 14:56:36.4989813 +0800 CST
    fmt.Println(fileinfo.Size())    // 439
    fmt.Println(fileinfo.Mode())    // -rw-rw-rw-
}
科普:权限

r:可读 w:可写 x:可执行

-     ---    ---     ---
type  owner  group   others


- rw- rw- rw-
d rw- rw- rw-

如果没有那个权限就用 - 代替

八进制表示权限

r  004
w  002
x  001
-  000

- rwx rwx rwx
- 4+2+1 4+2+1 4+2+1 
chomd 7 7 7
chomd 6 6 6

创建文件、目录

通过代码创建文件

路径:

  • 相对路径

  • 相对当前目录的路径

  • ./ 当前目录

  • ../ 上一级目录

  • 绝对路径

  • 从盘符开始的路径

创建目录

mkdir 路径+权限 创建单个目录

mkdirAll 路径+权限 创建层级目录

删除目录

remove 删除单个空文件夹

removeAll 强制删除目录

package main

import (
    "fmt"
    "os"
)

func main() {
    // 删除文件夹
    // 通过Remove方法只能删除单个空的文件夹
    // func Remove(name string) error
    err := os.Remove("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\file2")
    if err != nil {
        // The directory is not empty.
        fmt.Println(err)
    }
    fmt.Println("单个空文件夹删除成功")

    // 如果存在多层不为空的文件夹, 可用RemoveAll方法,此方法会删除指定目录下的所有文件,慎用
    // 类似于 linux命令 rm -rf ./
    // func RemoveAll(path string) error
    err2 := os.RemoveAll("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\file2")
    if err2 != nil {
        fmt.Println(err2)
        return
    }
    fmt.Println("文件夹删除成功")

}

// 创建文件夹(存在就无法创建,不存在就创建)
func m1() {
    // ModePerm: 0777
    // func Mkdir(name string, perm FileMode) error
    err := os.Mkdir("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\file1", os.ModePerm)
    if err != nil {
        // Cannot create a file when that file already exists.
        fmt.Println(err)
        return
    }

    fmt.Println("文件夹创建完毕")
}

// 创建层级文件夹
func m2() {
    // func MkdirAll(path string, perm FileMode) error
    err := os.MkdirAll("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\file2\\aa\\bb\\cc", os.ModePerm)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println("层级文件夹创建完毕")
}
创建文件

文件,就是一个File结构体对象,由相应的方法和属性。

create 方法,如果文件存在就返回File对象,如果不存在就创建并返回File对象。

package main

import (
    "fmt"
    "os"
)

func main() {
    // 返回的file结构体对象就是需要的文件
    // func Create(name string) (*File, error)
    file1, err := os.Create("a.go")
    if err != nil {
        fmt.Println(err)
        return
    }
    // &{0xc000004a00}
    fmt.Println(file1)
    // 删除文件
    os.Remove("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\a.go")
}

IO读

  1. 与文件建立连接

package main

import (
    "fmt"
    "os"
)

func main() {
    file1, err := os.Open("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\a.txt")
    if err != nil {
        fmt.Println(err)
        return
    }
    // &{0xc000004a00}
    fmt.Println(file1)

    // 打开文件时,选定权限,可读可写的方式打开
    // OpenFile(文件名, 打开方式:可读、可写等, 权限)
    // func OpenFile(name string, flag int, perm FileMode) (*File, error)
    file2, err2 := os.OpenFile("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\a.txt", os.O_RDONLY|os.O_WRONLY, os.ModePerm)
    if err2 != nil {
        fmt.Println(err2)
        return
    }
    // &{0xc0000cca00}
    fmt.Println(file2)
}
  1. 读取文件

file.Read([]byte),将file中的数据读取到 []byte中,返回值n和error, n代表读取到的字节数,error如果是EOF错误,就代表文件读取完毕了!Read方法一直被调用,就代表光标移动。

package main

import (
    "fmt"
    "os"
)

// 读取文件数据
func main() {
    // 建立连接
    // a.txt内容:abcd
    file, _ := os.Open("a.txt")
    // 关闭连接(习惯使用defer)
    defer file.Close()

    // 1、创建一个容器,缓冲区,接收读取的数据
    bs := make([]byte, 2, 1024)
    // 2、读取到缓冲区中
    // func (f *File) Read(b []byte) (n int, err error)
    n, err := file.Read(bs)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(n) // 2
    // 获取读取到的字符串
    fmt.Println(string(bs)) // ab

    n, err = file.Read(bs)
    if err != nil {
        // 如果光标读取到文件末尾,就会返回EOF
        fmt.Println(err)
        return
    }
    fmt.Println(n)          // 2
    fmt.Println(string(bs)) // cd

    n, err = file.Read(bs)
    if err != nil {
        // 如果光标读取到文件末尾,就会返回EOF
        fmt.Println(err)
        return
    }
    fmt.Println(n)
    fmt.Println(string(bs))
}

IO写

建立连接(需要使用openFile方法并设置相应的权限,可读可写可扩充)

关闭连接

写入文件的方法wirte/writeString

package main

import (
    "fmt"
    "os"
)

// 读取文件数据
func main() {
    // 建立连接
    // 权限添加:如果需要追加内容就得加O_APPEND权限,如果不加就是从头开始覆盖
    file, _ := os.OpenFile("a.txt", os.O_WRONLY|os.O_RDONLY|os.O_APPEND, os.ModePerm)
    // 关闭连接
    defer file.Close()

    // 操作
    bs := []byte{65, 66, 67, 68, 69}
    n, err := file.Write(bs)
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(n) // 5

    n, err = file.WriteString("我爱Go语言")
    if err != nil {
        fmt.Println(err)
        return
    }
    fmt.Println(n) //14
}

文件复制

自己手写copy方法
package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    srcSource := "D:\\Environment\\GoWorks\\src\\xuego\\IMG_20200604_205838.jpg"
    destSource := "D:\\Environment\\GoWorks\\src\\xuego\\copy.jpg"
    copy(srcSource, destSource, 1024)
}

// 文件复制
func copy(srcSource, destSource string, bufferSize int) {
    srcFile, err := os.Open(srcSource)
    if err != nil {
        fmt.Println("Open错误:", err)
    }
    destFile, err := os.OpenFile(destSource, os.O_WRONLY|os.O_CREATE, os.ModePerm)
    if err != nil {
        fmt.Println("OpenFile错误:", err)
    }
    // 关闭
    defer srcFile.Close()
    defer destFile.Close()
    buf := make([]byte, bufferSize)
    for {
        // 读取目标文件内容到缓冲区
        n, err := srcFile.Read(buf)
        if n == 0 || err == io.EOF {
            fmt.Println("文件复制完毕")
            break
        } else if err != nil {
            fmt.Println("读取错误:", err)
            return // 出现错误,终止函数
        }

        // 将缓冲区的内容写到目标文件
        _, err = destFile.Write(buf[:n])
        if err != nil {
            fmt.Println("写出错误:", err)
        }
    }
}
io包提供的copy方法
package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    srcSource := "D:\\Environment\\GoWorks\\src\\xuego\\IMG_20200604_205838.jpg"
    destSource := "D:\\Environment\\GoWorks\\src\\xuego\\copy1.jpg"
    copy(srcSource, destSource)
}

// 文件复制
func copy(srcSource, destSource string) {
    srcFile, err := os.Open(srcSource)
    if err != nil {
        fmt.Println("Open错误:", err)
    }
    destFile, err := os.OpenFile(destSource, os.O_WRONLY|os.O_CREATE, os.ModePerm)
    if err != nil {
        fmt.Println("OpenFile错误:", err)
    }
    // 关闭
    defer srcFile.Close()
    defer destFile.Close()
    n, err := io.Copy(destFile, srcFile)
    if err != nil {
        fmt.Println("文件复制错误:", err)
    }
    fmt.Println("复制文件的字节大小: ", n)
}
os包提供的ReadFile、WriteFile方法
package main

import (
    "fmt"
    "os"
)

func main() {
    srcSource := "D:\\Environment\\GoWorks\\src\\xuego\\IMG_20200604_205838.jpg"
    destSource := "D:\\Environment\\GoWorks\\src\\xuego\\copy2.jpg"
    copy(srcSource, destSource)
}

// 文件复制
func copy(srcSource, destSource string) {
    fileBuf, _ := os.ReadFile(srcSource)
    err := os.WriteFile(destSource, fileBuf, 0777)
    if err != nil {
        fmt.Println("文件复制错误:", err)
    } else {
        fmt.Println("文件复制成功")
    }
}

Seeker 接口

type Seeker interface {
    // 1、offset 偏移量
    // 2、whence 设置当前光标的位置
    Seek(offset int64, whence int) (int64, error)
}

// Seek whence values.
const (
    SeekStart   = 0 // seek relative to the origin of the file
    SeekCurrent = 1 // seek relative to the current offset
    SeekEnd     = 2 // seek relative to the end
)
package main

import (
    "fmt"
    "io"
    "os"
)

func main() {
    file, _ := os.OpenFile("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\a.txt", os.O_RDWR, os.ModePerm)
    defer file.Close()
    // io.SeekStart 相对于文件开头,偏移2
    file.Seek(2, io.SeekStart)
    buf := []byte{0}
    file.Read(buf)
    fmt.Println(string(buf))

    // 相对于当前位置
    file.Seek(3, io.SeekCurrent)
    file.Read(buf)
    fmt.Println(string(buf))

    // 在结尾追加内容
    file.Seek(0, io.SeekEnd)
    file.WriteString("Go语言")
}

断点续传

package main

import (
    "fmt"
    "io"
    "os"
    "strconv"
)

// 断点续传
func main() {
    // 源文件地址
    srcFile := "D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\IMG_20200604_205838.jpg"
    // 目标文件地址
    destFile := "D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\copy.jpg"
    // 临时文件地址
    tempFile := "D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\temp.txt"

    // 源文件
    file1, _ := os.Open(srcFile)
    // 目标文件
    file2, _ := os.OpenFile(destFile, os.O_CREATE|os.O_RDWR, os.ModePerm)
    // 临时文件
    file3, _ := os.OpenFile(tempFile, os.O_CREATE|os.O_RDWR, os.ModePerm)
    // 文件关闭
    defer file1.Close()
    defer file2.Close()

    // 临时文件光标重置到开头
    file3.Seek(0, io.SeekStart)
    buf := make([]byte, 1024, 1024)
    // 读取临时文件内容到buf
    n, _ := file3.Read(buf)
    counStr := string(buf[:n])
    count, _ := strconv.ParseInt(counStr, 10, 64)
    fmt.Println("temp.txt中记录的值为:", count)

    // 源文件、目标文件的光标移动到临时文件末尾(也就是断点处)
    file1.Seek(count, io.SeekStart)
    file2.Seek(count, io.SeekStart)

    bufData := make([]byte, 1024, 1024)
    total := int(count)

    for {
        // 读取目标文件内容到bufData
        readNum, err := file1.Read(bufData)
        if err == io.EOF {
            fmt.Println("文件传输完毕了")
            file3.Close()
            // 删除临时文件
            os.Remove(tempFile)
            break
        }
        // 将buf内容写入到目标文件
        writeNum, err := file2.Write(bufData[:readNum])
        // 将写入数据记录到total中,也就是传输进度
        total = total + writeNum
        // 重置临时文件光标到开头
        file3.Seek(0, io.SeekStart)
        // 写入传输数据到临时文件
        file3.WriteString(strconv.Itoa(total))

        // 模拟断电
        //if total > 5000 {
        //    panic("断电了")
        //}
    }
}

遍历文件夹

package main

import (
    "fmt"
    "log"
    "os"
)

func main() {
    dir := "D:\\Environment\\GoWorks\\src\\xuego\\lesson01"
    tree(dir, 0)
}
func tree(dir string, level int) {
    // 体现层级
    tabString := "|--"
    for i := 0; i < level; i++ {
        tabString = "|  " + tabString
    }
    // 获取目录信息[]DirEntry
    // func ReadDir(name string) ([]DirEntry, error)
    fileInfos, err := os.ReadDir(dir)
    if err != nil {
        log.Println(err)
        return
    }
    // 遍历目录信息,获取子文件信息
    for _, file := range fileInfos {
        fileName := dir + "\\" + file.Name()
        fmt.Println(tabString + file.Name())
        // 如果子文件是文件夹,继续遍历
        if file.IsDir() {
            tree(fileName, level+1)
        }
    }
}

bufio

bufio Go语言自带的IO操作包,使用此包可大幅提升文件的读写效率。

如果频繁地访问本地磁盘文件,io包的操作效率较低。

bufio包,提供了一个缓冲区,读写都先在缓冲区中,然后再一次性读写到文件中,从而降低对本地磁盘的访问次数。

  1. 读出

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    file, err := os.Open("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\Demo01.go")
    if err != nil {
        fmt.Println(err)
    }
    defer file.Close()

    // 读取文件内容到Reader中
    // func NewReader(rd io.Reader) *Reader
    bufioReader := bufio.NewReader(file)
    buf := make([]byte, 1024)
    n, err := bufioReader.Read(buf)
    fmt.Println("读取到了多少个字节:", n)
    fmt.Println("读取到的内容:", string(buf[:n]))

    // 读取键盘的输入,输入实际是流
    inputReader := bufio.NewReader(os.Stdin)
    // delim 到何位置结束读取
    // func (b *Reader) ReadString(delim byte) (string, error)
    readString, _ := inputReader.ReadString('\n')
    fmt.Println("输出键盘输入的信息:", readString)
}
  1. 写入

package main

import (
    "bufio"
    "fmt"
    "os"
)

func main() {
    file, _ := os.OpenFile("D:\\Environment\\GoWorks\\src\\xuego\\lesson01\\a.txt",
        os.O_RDWR|os.O_CREATE|os.O_APPEND, os.ModePerm)
    defer file.Close()
    
    // 获取写入文件的Writer
    // func NewWriter(w io.Writer) *Writer
    fileWriter := bufio.NewWriter(file)
    // 字符串写入到文件中
    // func (b *Writer) WriteString(s string) (int, error)
    writeNum, _ := fileWriter.WriteString("我爱Go语言")
    fmt.Println("writeNum: ", writeNum)

    // 写入文件需要手动刷新一下
    fileWriter.Flush()
}

Goroutine

进程、线程、协程

程序:指令和数据的有序集合,静态的概念,本身没有任何含义。

进程(Process),线程(Thread), 协程(Coroutine, 也叫轻量级线程)

进程

进程是一个程序在一个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序",它是CPU资源分配和调度的独立单位。

进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;

数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。 进程的局限是创建、撤销和切换的开销比较大

线程

线程是在进程之后发展出来的概念。线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、 程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。

线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能缺点是线程没有自己的系统资源,同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。不过对于某些独占性资源存在锁机制,处理不当可能会产生”死锁"。

协程Goroutine

协程是一种用户态的轻量级线程又称微线程,英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。

就好比是启动了一个函数,单次执行完毕它。不影响我们main线程的执行。

子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

与传统的系统级线程和进程相比,协程的最大优势在于其"轻量级”可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万的。这也是协程也叫轻量级线程的原因。

Goroutine

Go语言对于并发的实现是靠协程,Groutine

Goroutine是Go语言特有的名词,区别于进程Process、线程Thread。

Goroutine是其它函数或方法同时运行的函数或方法。Goroutine可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小,就是一段代码,一个函数入口,在堆上为其分配的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。因为非常廉价,所以Go可以轻松并发运行数千个Goroutine。

使用Goroutine,只需要在函数或方法前面添加一个go 关键字即可。

package main

import "fmt"

func main() {
    // goroutine: 和普通方法的调用完全不同,它是并发执行的,快速交替
    go hello()
    for i := 0; i < 100; i++ {
        fmt.Println("main - ", i)
    }
}
func hello() {
    for i := 0; i < 100; i++ {
        fmt.Println("hello - ", i)
    }
}

Goroutine的执行规则:

  • 当新的Goroutine开始时, Goroutine调用立即返回。与函数不同,go不等待Goroutine执行结束;

  • 当Goroutine调用,并且Goroutine的任何返回值被忽略之后,go立即执行到下一行代码;

  • main的Goroutine应该为其他的Goroutines执行。如果main的Goroutine终止了,程序将被终止,而其他Goroutine将不会运行。

主Goroutine main

封装main函数的goroutine称为主goroutine。

主goroutine所做的事情并不是执行main函数那么简单。它首先要做的是:设定每一个goroutine所能申请的栈空间的最大尺寸。在32位的计算机系统中此最大尺寸为250MB,而在64位的计算机系统中此尺寸为1GB。如果有某个goroutine的栈空间尺寸大于这个限制,那么运行时系统就会引发一个栈溢出(stack overflow)的运行时恐慌。随后,这个go程序的运行也会终止。

此后,主goroutine会 进行一系列的初始化工作,涉及的工作内容大致如下:

  • 创建一个特殊的defer语句,用于在主goroutine退出时做必要的善后处理。因为主goroutine也可能非正常的结束;

  • 启动专用于在后台清扫内存垃圾的goroutine,并设置GC可用的标识;

  • 执行main包中所引用包下的init函数;

  • 执行main函数;

执行完main函数后,它还会检查主goroutine是否引发了运行时恐慌,并进行必要的处理。

程序运行完毕后,主goroutine会结束自己以及当前进程的运行。

runtime包

获取系统信息runtime、让出时间片、终止goroutine

package main

import (
    "fmt"
    "runtime"
    "time"
)

func main() {
    // 终止程序 runtime.Goexit()
    go func() {
        fmt.Println("start")
        test()
        fmt.Println("end")
    }()

    time.Sleep(time.Second * 3)
}

func test() {
    defer fmt.Println("test defer")
    // 终止当前的goroutine
    runtime.Goexit()
    fmt.Println("test")
}

func runtime1() {
    // 获取GOROOT目录
    fmt.Println("GOROOT path: ", runtime.GOROOT()) // D:\Environment\Go
    // 获取操作系统信息
    fmt.Println("OS System: ", runtime.GOOS) // windows
    // 获取CPU数量
    fmt.Println("CPU num: ", runtime.NumCPU()) // 8
}

func runtime2() {
    // goroutine是竞争cpu的
    go func() {
        for i := 0; i < 5; i++ {
            fmt.Println("goroutine-", i)
        }
    }()

    for i := 0; i < 5; i++ {
        // Gosched 礼让,让出时间片,让其他的goroutine先执行
        // 由于cpu的调度是随机的,所以并非绝对的礼让
        runtime.Gosched()
        fmt.Println("main-", i)
    }
}

临界资源安全问题

临界资源:指并发环境中多个进程、线程、协程共享的资源

在并发编程中对临界资源的处理不当,往往会导致数据不一致的问题。

package main

import (
    "fmt"
    "time"
)

func main() {
    // 临界资源
    a := 1
    go func() {
        // 临界资源a被修改为2
        a = 2
        fmt.Println("goroutine a:", a)
    }()

    a = 3
    time.Sleep(3 * time.Second)
    fmt.Println("mian a:", a)
}
售票问题

并发本身并不复杂,但是因为有了资源竞争的问题,就使得我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。

如果多个goroutine在访问同一个数据资源的时候,其中一个线程修改了数据,那么这个数值就被修改了,对于其他的goroutine来讲,这个数值可能是不对的。

package main

import (
    "fmt"
    "time"
)

// 定义全局变量 票库存为10张
var ticket int = 10

func main() {
    // 单线程不存在问题,多线程资源争抢就出现了问题
    go saleTickets("张三")
    go saleTickets("李四")
    go saleTickets("王五")
    go saleTickets("赵六")

    time.Sleep(time.Second * 5)
}

// 售票函数
func saleTickets(name string) {
    for {
        if ticket > 0 {
            time.Sleep(time.Millisecond * 1)
            fmt.Println(name, "剩余票的数量为:", ticket)
            ticket--
        } else {
            fmt.Println("票已售完")
            break
        }
    }
}

发现结果和预想的不同,多线程加入之后,原先单线程的逻辑出现了问题。出现了临界资源安全问题。

sync包 - 锁

要想解决临界资源安全的问题,很多编程语言的解决方案都是同步。通过上锁的方式,某一时间段,只能允许一个goroutine来访问这个共享数据,当前goroutine访问完毕, 解锁后,其他的goroutine才 能来访问

我们可以借助于sync包下的锁操作。 synchronization

但是实际上,在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信:锁,而要以通信的方式去共享内存。

在Go语言中并不鼓励用锁保护共享状态的方式,在不同的Goroutine中分享信息(以共享内存的方式去通信)。而是鼓励通过channeI将共享状态或共享状态的变化在各个Goroutine之间传递(以通信的方式去共享内存),这样同样能像用锁一样保证在同一的时间只有一个Goroutine访问共享状态。

当然,在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多

package main

import (
    "fmt"
    "sync"
    "time"
)

// 定义全局变量 票库存为10张
var ticket int = 10

// 定义一个锁 Mutex
var mutex sync.Mutex

func main() {
    // 单线程不存在问题,多线程资源争抢就出现了问题
    go saleTickets("张三")
    go saleTickets("李四")
    go saleTickets("王五")
    go saleTickets("赵六")

    time.Sleep(time.Second * 5)
}

// 售票函数
func saleTickets(name string) {
    for {
        // 在拿到共享资源之前上锁
        mutex.Lock()
        if ticket > 0 {
            time.Sleep(time.Millisecond * 1)
            fmt.Println(name, "剩余票的数量为:", ticket)
            ticket--
        } else {
            // 票售完后解锁
            mutex.Unlock()
            fmt.Println("票已售完")
            break
        }
        // 操作完后解锁
        mutex.Unlock()
    }
}

同步等待组

package main

import (
    "fmt"
    "sync"
)

// 同步等待组
var wg sync.WaitGroup

func main() {
    // 判断有几个线程
    wg.Add(2)
    go test1()
    go test2()

    wg.Wait() // 等待 wg 归零后才会继续向下执行
    //time.Sleep(time.Second * 3)
}

func test1() {
    for i := 0; i < 10; i++ {
        fmt.Println("test1-", i)
    }
    wg.Done() // 告知当前goroutine已结束
}

func test2() {
    defer wg.Done() // 告知当前goroutine已结束
    for i := 0; i < 10; i++ {
        fmt.Println("test2-", i)
    }
}
package main

import (
    "fmt"
    "sync"
    "time"
)

// 定义全局变量 票库存为10张
var ticket int = 10

// 锁
var mutex sync.Mutex

// 同步等待组
var wg sync.WaitGroup

func main() {
    // 添加4个线程
    wg.Add(4)
    // 单线程不存在问题,多线程资源争抢就出现了问题
    go saleTickets("张三")
    go saleTickets("李四")
    go saleTickets("王五")
    go saleTickets("赵六")

    // 等待线程结束
    wg.Wait()
}

// 售票函数
func saleTickets(name string) {
    defer wg.Done()
    for {
        mutex.Lock()
        if ticket > 0 {
            time.Sleep(time.Millisecond * 1)
            fmt.Println(name, "剩余票的数量为:", ticket)
            ticket--
        } else {
            mutex.Unlock()
            fmt.Println("票已售完")
            break
        }
        mutex.Unlock()
    }
}

channel

通道的概念及定义

Go语言不建议使用锁机制解决多线程问题,建议使用通道。

通道,类似于水管,数据可以从一端流到另一端。一个goroutine需要将一些信息告诉另外一个goroutine ,就直接将数据信息放入chan通道即可。通道必须作用在两个及两个以上的Goroutine。

不要通过共享内存来通信,而应该通过通信共享内存。

package main

import (
    "fmt"
    "time"
)

// 定义通道chan
func main() {
    // 定义一个 bool 通道
    var ch chan bool
    ch = make(chan bool)

    // 在一个Goroutine往通道中存入数据
    go func() {
        // 模拟阻塞等待
        time.Sleep(time.Second * 3)
        ch <- true
    }()

    // 另一个Goroutine可以从通道中取数据(线程之间的通信)
    // 阻塞等待ch拿到值
    data := <-ch
    fmt.Println("data:", data)
}

一个通道发送和接收数据,默认是阻塞的。

当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从该通道读取数据。

相对地,当从通道读取数据时,读取被阻塞,直到一个Goroutine将数据写入该通道。

本身channel就是同步的, 意味着同一时间,只能有一个goroutine来操作。

最后,通道是goroutine之间的连接,所有通道的发送和接收必须处在不同的goroutine中

这些通道的特性是帮助Goroutines有效地进行通信,而无需像使用其他编程语言中非常常见的显式锁或条件变量

死锁

如果创建了chan,没有 Goroutine 来使用了,则会出现死锁。

使用通道时要考虑的一个重要因素是死锁。如果Goroutine在一 个通道 上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发生,那么程序将在运行时出现死锁。

类似地,如果Goroutine 正在等待从通道接收数据,那么必须有另一个Goroutine将会在该通道上写入数据,否则程序将会死锁。

package main

// 死锁的产生
func main() {
    ch := make(chan int)
    // 如果通道中存入数据,没有其它Goroutine消耗数据
    // 则会产生死锁,fatal error: all goroutines are asleep - deadlock!
    // 相反亦然
    ch <- 1
}

死锁的产生:

  • 单线程的使用,没有其他的goroutine消费

  • 两个chan,互相需要对方的数据,但是由于判断,拿不到对方的数据。

  • sync 锁产生的死锁。

通道关闭

通过ok的状态判断通道是否关闭
package main

import (
    "fmt"
    "time"
)

// 关闭通道
func main() {
    ch1 := make(chan int)
    // 存入数据
    go test(ch1)
    // 读取数据
    for {
        time.Sleep(time.Second)
        // ok 判断通道的状态是否关闭,如果false 表示关闭
        data, ok := <-ch1
        if !ok {
            fmt.Println("读取完毕:", ok) // false
            break
        }
        fmt.Println("ch1 data: ", data)
    }
}
func test(ch chan int) {
    for i := 0; i < 10; i++ {
        ch <- i
    }
    // 告诉接收方已经结束,不会再有数据放入通道了
    close(ch)
}
通过for range循环遍历通道中的数据,简化开发
package main

import (
    "fmt"
    "time"
)

// 关闭通道
func main() {
    ch1 := make(chan int)
    // 存入数据
    go test(ch1)
    // 读取数据
    for data := range ch1 {
        time.Sleep(time.Second)
        fmt.Println(data)
    }
    fmt.Println("end")
}

// 通道可以进行参数传递
func test(ch chan int) {
    for i := 0; i < 10; i++ {
        ch <- i
    }
    // 告诉接收方已经结束,不会再有数据放入通道了
    close(ch)
}

缓冲通道

非缓冲通道

只能存一个数据,发送和接收都是阻塞的,一个发送对应一个接收。

缓冲通道

通道带有缓冲区,发送方发送的数据直到缓冲区填满为止,才会被阻塞,接收方也是,只有缓冲区清空,才会被阻塞。

package main

import (
    "fmt"
    "strconv"
    "time"
)

// 缓冲通道
func main() {
    // 非缓冲通道
    ch1 := make(chan int)
    fmt.Println(cap(ch1), len(ch1)) // 0 0

    // 缓冲通道
    ch2 := make(chan string, 5)
    fmt.Println(cap(ch2), len(ch2)) // 5 0
    ch2 <- "1"
    ch2 <- "2"
    ch2 <- "3"
    ch2 <- "4"
    ch2 <- "5"
    fmt.Println(cap(ch2), len(ch2)) // 5 5
    // 如果通道满了,还没有goroutine取数据,会死锁
    // fatal error: all goroutines are asleep - deadlock!
    // ch2 <- "6"
    fmt.Println("--------------------")

    ch3 := make(chan string, 4)
    go test(ch3)
    for s := range ch3 {
        time.Sleep(time.Second)
        fmt.Println("main中读取的数据:", s)
    }
    fmt.Println("main-end")
}
func test(ch chan string) {
    for i := 0; i < 10; i++ {
        ch <- "test - " + strconv.Itoa(i)
        fmt.Println("子goroutine存入数据:test-", i)
    }
    // 关闭通道
    close(ch)
}

缓冲通道,可以定义缓冲区的数量。

如果缓冲区没有满,可以继续存放,如果满了,也会阻塞等待。

如果缓冲区空的,读取也会等待,如果缓冲区中有多个数据,依次按照先进先出的规则进行读取。

如果缓冲区满了,同时有两个线程在读或者写,这个时候和普通的通道一样。一进一出。

单向通道

package main

import "fmt"

// 定向通道
func main() {
    // 双向通道
    ch1 := make(chan int, 1)
    ch1 <- 100
    data := <-ch1
    fmt.Println(data) // 100

    // 单向通道
    ch2 := make(chan<- int, 1) // 只能写数据,不能读
    ch2 <- 200
    // data := <- ch2 Invalid operation: <- ch2 (receive from the send-only type chan<- int)

    // ch3 := make(<-chan int, 1) // 只能读数据,不能写
    // Invalid operation: ch3 <- 300 (send to the receive-only type <-chan int)
    // ch3 <- 300
}
package main

import (
    "fmt"
    "time"
)

// 定向通道
func main() {
    // 单向通道使用场景
    // 指定函数读写数据,防止通道滥用
    ch := make(chan int, 1)
    go writeOnly(ch)
    go readOnly(ch)

    time.Sleep(time.Second)
}
// 指定函数,只能往通道中写数据,不能读数据
func writeOnly(ch chan<- int) {
    ch <- 100
}
// 指定函数,只能读取数据,不能写数据
func readOnly(ch <-chan int) {
    data := <-ch
    fmt.Println(data)
}

Select

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan int, 1)
    ch2 := make(chan int, 1)

    go func() {
        time.Sleep(time.Second * 2)
        ch1 <- 100
    }()
    go func() {
        time.Sleep(time.Second * 2)
        ch2 <- 200
    }()

    // select 只是在通道中使用,case表达式获取通道结果,就会输出,从而抛弃其它的
    select {
    case num1 := <-ch1:
        fmt.Println(num1)
    case num2 := <-ch2:
        fmt.Println(num2)
    default:
        fmt.pringtln("default")
    }
}
  • 每一个case都必须是一个通道的操作;

  • 所有chan操作都要有结果(通道表达式都必须会被求值);

  • 如果任意的通道拿到了结果,他就会立即执行该case,其它的都会被忽略。

  • 如果有多个case,select会随机选取一个运行,其它就不会运行;

  • 如果存在default语句,没有拿到相应的值,就会执行该语句;如果不存在;阻塞等待select直到拿到某个通道的结果。

Timer定时器

通道的应用场景一

可以控制程序在某个时间执行

package main

import (
    "fmt"
    "time"
)

// 定时器
func main() {
    // 创建一个定时器 NewTimer
    timer := time.NewTimer(time.Second * 3)
    // 当前时间
    // 2023-07-05 22:56:08.6040883 +0800 CST m=+0.018072201
    fmt.Println(time.Now())
    // time.C 时间通道,通道中存放着定时器对应的时间
    timeChan := timer.C
    // 2023-07-05 22:56:11.6210377 +0800 CST m=+3.035021601
    fmt.Println(<-timeChan)

    timer2 := time.NewTimer(time.Second * 5)
    // 手动停止定时器
    timer2.Stop()
}
package main

import (
    "fmt"
    "time"
)

// 定时器
func main() {
    // 通道中放入当前时间之后的某个时间
    timeChan := time.After(time.Second * 3)
    // 2023-07-05 23:07:33.0042737 +0800 CST m=+0.015017801
    fmt.Println(time.Now())
    chanTime := <-timeChan
    // 2023-07-05 23:07:33.0042737 +0800 CST m=+0.015017801
    fmt.Println(chanTime)

    // 在3s以后执行发邮件
    time.AfterFunc(time.Second*3, mail)

    // 等待上面结束
    time.Sleep(time.Second * 4)
}
func mail() {
    fmt.Println("发邮件")
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值