Golang:进行例程和候补组
#go #并发性 #goroutines #100datsofgolang

介绍

设置的关键功能之一与许多其他语言区别开来的是对Goroutines的本机支持 - 轻巧的并发功能可以同时有效地管理并发任务。 Goroutines是GO并发模型的重要方面,使开发人员能够轻松地构建高度并发和性能应用程序。

在这篇博客文章中,我们将深入研究Goroutines的世界,并探讨它们的工作方式,为什么它们对于同时进行GO的编程至关重要,以及它们如何大大提高应用程序的响应能力和效率。这篇文章将涵盖GO例程和等待小组的入门,在下一篇文章中,我们将深入研究所有这三件事都可以以更好,更有用的方式证明和理解的渠道。

并发和并行性

这两个概念在潜入企业的基本面之前至关重要。

并发是一次处理多个事情。并发程序可以通过在它们之间快速切换(上下文切换)在单个CPU核心上同时运行的几个过程或线程。线程是交错的,不一定在同一时间执行。 CPU可以在这些任务之间切换以同时出现。

并行性是在同一时间做多件事。并行程序可以在单独的CPU内核上同时执行多个计算。线程实际上并行执行。

什么是常规

GO例程是由GO运行时管理的简单轻巧线程。用最简单的术语,可以将GO例程定义为:

GO例程是在程序中执行多个任务的一种方式,允许程序的不同部分同时工作并充分利用资源。

也可以说为:

Golang中的Goroutine是一种轻巧的独立执行功能,与同一地址空间内的其他goroutines同时运行。换句话说,它是执行的并发单位。

专注于单词相同的地址空间在本文的后面部分中至关重要。

GO例程的功能

GO例程构成GO并发模型的关键部分。这是GO例程的一些关键特征:

  • 轻巧线程
    GO例程通常称为轻量级线程。

  • 独立执行
    GO例程彼此独立运行,实现并发执行。

  • 由Go管理
    GO例程由GO运行时管理,使其易于使用。

  • 低开销
    GO例程的内存较低,使我们能够有效地创建数千个。

  • 交流
    GO例程可以通过渠道传达和同步数据。

  • 异步
    GO例程可以异步执行,允许程序的其他部分继续运行。

  • 可伸缩性
    GO例程是GO中可扩展并发编程的基础。

与其他语言中的线程不同,goroutines很便宜,您可以轻松地在程序中创建数千甚至数百万个。

GO例程的示例

创建GO例程并不困难,只需在函数调用之前添加关键字go,并且GO运行时将在主函数内部或任何位于上下文中创建一个新的GO例程。请记住,主要功能也是GO例程。

package main

import (
    "fmt"
    "time"
)

func process() {
    fmt.Println("Hello World!")
    time.Sleep(time.Second)
}

func main() {
    start := time.Now()
    go process()
    go process()
    end := time.Now()
    duration := end.Sub(start)
    fmt.Println(duration)
}
scripts/go-routines on  main via 🐹 v1.20 
$ go run main.go 

Hello World!
15.607µs


scripts/go-routines on  main via 🐹 v1.20 
$ go run main.go 

9.889µs


scripts/go-routines on  main via 🐹 v1.20 
$ go run main.go 

8.834µs


scripts/go-routines on  main via 🐹 v1.20 
$ go run main.go 

9.158µs

scripts/go-routines on  main via 🐹 v1.20 
$ go run main.go 

12.54µs


scripts/go-routines on  main via 🐹 v1.20 
$ go run main.go 

Hello World!
Hello World!
10.19µs


scripts/go-routines on  main via 🐹 v1.20 
$ go run main.go 

14.1µs


scripts/go-routines on  main via 🐹 v1.20 
$ 

非常不可预测的输出吧?这是行驶的力量。它是异步,因此不会阻止主函数。对process的两个函数调用,独立于主函数范围执行。该程序仅调用过程函数并捕获输出,然后依次到达程序的末尾(主函数),此时,主函数内的GO例程(线程)被突然停止。

如果尚不清楚,让我们将其分解。

  • 主要功能开始。
  • go process()创建了一个例程并开始执行。
  • 同时,对go process()的另一个呼吁也创建了一个单独的GO例程并开始执行。
  • 同时,计算主函数启动和结束之间的时差。
  • 同时主要功能结束。

因此,总而言之,主函数只能捕获duration的输出,因为它是同步的,如果执行该过程,它将打印Hello World!消息。因此,我们之所以具有不同的输出,是因为不受控制的并发,缺乏协调以及整个程序运行的OS调度是不同的。

等待小组

简单地说,WaitGroup用于同步多个Goroutines,并等待它们完成执行。这允许在完成主函数之前完成GO例程,因此它阻止了主函数离开/退出范围。

waitgroup是一种同步原始性,它允许Goroutine等待其他Goroutines的集合来完成执行。

  • 初始化了一个候补,以代表要等待的goroutines数量的计数器。
  • add()方法通过给定值增加计数器。每个goroutine都称这是指它正在运行的。
  • 主goroutine调用add()以设置初始计数,然后启动worker goroutines。
  • 通常由需要等待的goroutines的指针传递。
  • Done()方法将计数器减少1. goroutines在完成后调用此。
  • 完成后每个工人呼叫DONE DONE(),减少计数器。
  • wait()方法阻止柜台达到0,表明所有goroutines都已经完成。
  • 主呼叫wait()阻塞直到完成()将与0相反。

这提供了一种简单的方法,可以使多个Goroutines同步,将其工作与需要等待它们完成的主线程完成工作。计数器确保主线程知道它在等待多少goroutines。使用等待组的同步进行交互并与GO例程一起工作非常直观且易于遵循。让我们看一个简单的示例:

package main

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

func process(pid int, wg *sync.WaitGroup) {
    fmt.Printf("Started process %d\n", pid)
    time.Sleep(1 * time.Second)
    fmt.Printf("Completed process %d\n", pid)
    defer wg.Done()
}

func main() {

    now := time.Now()
    var wg sync.WaitGroup

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go process(i, &wg)
    }
    wg.Wait()
    fmt.Println("All processes completed")
    end := time.Now()
    fmt.Println(end.Sub(now))
}
scripts/go-routines on  main [?] via 🐹 v1.20 
$ go run wg.go 
Started process 9
Started process 0
Started process 1
Started process 2
Started process 3
Started process 4
Started process 5
Started process 6
Started process 7
Started process 8
Completed process 8
Completed process 3
Completed process 9
Completed process 0
Completed process 1
Completed process 2
Completed process 5
Completed process 4
Completed process 6
Completed process 7
1.000563257s
All processes completed

scripts/go-routines on  main [?] via 🐹 v1.20 
$ 

在上面的示例中,我们使用了相同的函数process,但是在稍有扭曲的情况下,我们添加了一个过程ID,只是一个整数来表示GO例程。我们在睡眠之间打印功能的开始和完成1秒。我们也有一个等待小组。

  • WaitGroup基本上是sync软件包中定义的结构类型。
  • 变量wg是一个新的等待组实例,将用于同步并等待完成GO例程组的完成。

我们使用10迭代创建一个用于循环的循环,以使10个process函数调用。我们首先使用wg.Add(1),该wg.Add(1)向等待组说等待1个例程。下一行是GO例程go process(),它吸收了pid,只是为了跟踪在循环中执行的GO例程。

process函数中,我们只是说带有给定的pid的过程是作为打印说明开始的,睡一秒钟,然后用pid打印过程的结尾。函数的末尾用defer wg.Done()标记,这表明GO例程已完成。

WG(Wait Group)有一个计数器,可以跟踪其同步或等待完成的GO例程数。在Add函数中,WaitGroup中的内部计数器被delta递增的整数被解析为参数。和Done功能,降低了WaitGroup中的内部计数器,这表明GO例程已完成。

在主函数中,我们调用wg.Wait,该函数将阻止,直到WaitGroup的计数器为0,即所有GO例程都已完成了执行。因此,我们创建了10个GO例程,并同时运行,但在WaitGroup的帮助下同步。允许候补组阻止主函数,直到执行所有GO例程。

使用候补案例进行例行程序

GO例程可用于创建异步任务,也可用于创建并发任务。通过使用等待组,我们可以创建一种方法来等待多个Goroutines完成。通过使用GO例程和等待组,我们可以在1个任务完成的时间范围内完成n个任务数。但是,要在其他过程之间创建并发通信,我们需要channels(我们将在下一篇文章中进行探讨)。

这是异步和并发任务可能指的内容的简单分解:

异步任务独立于主程序流程,允许主程序继续执行而无需等待任务的完成。例如,不阻止主函数的顺序流中的其他任务。

并发任务同时运行,并且可以与其他任务同时执行。他们利用多个线程(在GO的情况下使用Goroutines)来实现并行执行。例如,在完成另一个任务后,运行多个任务并行将缩短旋转每个任务的时间。

可以使用GO例程可以完成的一些异步任务可能包括以下内容:

  • 在将用户保存到数据库时发送邮件。
  • 从多个网站获取和处理数据(网络刮擦/爬网)。
  • 高性能消息经纪和排队系统间通信。

与等待组一起使用GO例程的一种实用方法是发送邮件,我们将不会看到邮件订购内容的实际实现。但是,我们可以尝试模仿发送批量邮件的设置。通过创建一个等待组和要发送的邮件地址列表,可以使用发送这些电子邮件的函数创建GO例程。

package main

import (
    "fmt"
    "sync"
)

func sendMail(address string, wg *sync.WaitGroup) {
    fmt.Println("Sending mail to", address)
    defer wg.Done()
    // Actual mail sending, smtp stuff
    // handle errors

    // client, err := smtp.Dial("smtp.example.com:587")
    // errr = client.Mail("sender@example.com")
    // err = client.Rcpt(address)
    // wc, err := client.Data()
    //_, err = wc.Write([]byte("This is the email body."))
    // err = wc.Close()
    // client.Quit()
}

func main() {
    emails := []string{
        "recipient1@example.com",
        "recipient2@example.com",
        "xyz@example.com",
    }
    wg := sync.WaitGroup{}
    wg.Add(len(emails))

    for _, email := range emails {
        go sendMail(email, &wg)
    }
    wg.Wait()
    fmt.Println("All emails queued for sending")
    // Do other stuff
}

在上面的示例中,emails是发送邮件的电子邮件ID列表。我们创建了一个等待组,我们可以使用该程序进行初始化的GO例程数量可能要执行。 wg.Add方法用要发送的电子邮件数量进行解析,因此等于卵形的GO例程。

因此,我们可以在for循环中,迭代每个邮件,并以sendMail函数作为GO例程发送电子邮件。循环外的wg.Wait功能将确保在所有GO例程完成执行之前将主函数停止。

还有另一种方法可以将函数称为GO例程而不更改其签名,因为我们必须将wg指针作为WaitGroup引用,以确认GO例程的完成。我们可以包裹这两个操作。调用该功能并使用匿名函数调用wg.Done方法。

package main

import (
    "fmt"
    "sync"
)

func sendMail(address string) {
    fmt.Println("Sending mail to", address)
}

func main() {
    emails := []string{
        "recipient1@example.com",
        "recipient2@example.com",
        "xyz@example.com",
    }
    wg := sync.WaitGroup{}
    wg.Add(len(emails))

    for _, email := range emails {
        mail := email
        go func(m string) {
            sendMail(m)
            wg.Done()
        }(mail)
    }
    wg.Wait()
    fmt.Println("All emails queued for sending")
    // Do other stuff
}

这做完全相同的事情,但是我们不必更改函数的签名,这可以使功能的功能逻辑位于其位置并处理GO中的并发。


for _, email := range emails {
    mail := email
    go func(m string) {
        sendMail(m)
        wg.Done()
    }(mail)
}

如果上面有点吓到您,请不要担心它太简单了。

  • 我们正在使用for循环的电子邮件切片进行迭代,并为每个电子邮件地址创建一个例程。循环变量电子邮件表示当前迭代中的电子邮件地址。
  • 但是,为避免循环变量捕获问题(所有GO例程都将共享相同的电子邮件变量),我们创建一个新的变量邮件,并将电子邮件值分配给它。此步骤可确保每个GO例程都捕获其自己的电子邮件地址副本。
  • 我们立即使用GO关键字创建一个匿名函数(闭合)。此匿名函数将mail变量作为参数M,并作为GO例程同时执行。在GO例程中,我们使用电子邮件地址m调用sendMail函数。
  • 在执行sendMail呼叫后,即发送电子邮件,我们致电wg.done(),以通知waitgroup,go例程已完成其工作。这允许WaitGroup正确同步,并等待所有GO例程完成,然后在程序以外的wg.wait()以外的主函数中进行。

如果您想将邮件示意逻辑与Goroutines/并发任务分开,这是一种方法。但是,应该谨慎处理这一点,因为可以在所有goroutines中共享封闭中的变量,而不是具有单个变量文字。

为了确保每个Goroutine都在其电子邮件地址的副本上运行,我们使用创建新变量邮件并将其作为参数传递给匿名函数的方法。这样,每个Goroutine都会捕获其独特的电子邮件地址,避免了Goroutines之间数据的任何干扰或意外共享。

相互排除锁

在前面的示例中,我们看到了如何goroutines和等待组允许我们在GO中同时运行多个任务。但是,有时这些并发的goroutines需要访问内存,文件,网络插座等共享资源

当多个Goroutine试图同时访问资源时,它可能会导致种族条件和无法预测的行为。为了处理这一点,我们需要一种方法来确保只有一个goroutine可以一次访问资源。

这是相互排除锁的来源。A相互排除锁 mutex ,提供了锁定访问共享资源的机制。它可以确保一次只能获得一个goroutine,可以收到锁,阻止其他goroutines直到锁定锁。

例如,说我们有多个goroutines尝试将数据附加到同一内存缓冲区(可能是文件/数据库/等)。

package main

import (
    "fmt"
    "os"
    "sync"
)

func WriteToFile(filename string, contents string, buffer *[]byte, wg *sync.WaitGroup) {
    defer wg.Done()
    *buffer = append(*buffer, []byte(contents)...)
    err := os.WriteFile(filename, *buffer, 0644)
    if err != nil {
        fmt.Println(err)
    }
}

func main() {
    var wg sync.WaitGroup
    var sharedBuffer []byte

    wg.Add(2)
    go WriteToFile("data/f1.txt", "Hello ", &sharedBuffer, &wg)
    go WriteToFile("data/f1.txt", "World! ", &sharedBuffer, &wg)
    wg.Wait()

    fmt.Println(string(sharedBuffer))
}
$ go run --race no-mutex.go
==================
WARNING: DATA RACE
Read at 0x00c000012030 by goroutine 8:
  main.WriteToFile()
      /home/meet/code/100-days-of-golang/scripts/go-routines/no-mutex.go:11 +0xe7
  main.main.func2()
      /home/meet/code/100-days-of-golang/scripts/go-routines/no-mutex.go:24 +0x64

Previous write at 0x00c000012030 by goroutine 7:
  main.WriteToFile()
      /home/meet/code/100-days-of-golang/scripts/go-routines/no-mutex.go:11 +0x16a
  main.main.func1()
      /home/meet/code/100-days-of-golang/scripts/go-routines/no-mutex.go:23 +0x64

Goroutine 8 (running) created at:
  main.main()
      /home/meet/code/100-days-of-golang/scripts/go-routines/no-mutex.go:24 +0x1f6

Goroutine 7 (finished) created at:
  main.main()
      /home/meet/code/100-days-of-golang/scripts/go-routines/no-mutex.go:23 +0x14e
==================

这是因为我们试图同时访问相同的内存地址。这是一种种族条件,可能导致不可预测的行为。在跑步时尝试删除--race标志,在这个小愚蠢的示例中,这可能不是显而易见的,但是在复杂且受约束的环境中,这可能会使应用程序陷入严重的麻烦。

注意:我们正在使用go run --race no-mutex.go检查程序中是否有任何种族条件。这是运行命令中的race detector标志。

要避免这种比赛条件,我们需要添加sync.Mutex类型中提供的静音锁。有一些方法,例如LockUnlockTryLock,可以在给定时间将资源访问到单个实体中。

当goroutine在静音上调用Lock()时,它会获取锁。如果Mutex已经被另一个goroutine锁定,则将呼叫Goroutine被阻止(入睡),直到锁可用为止。一旦成功获取了锁,Goroutine就可以进行关键部分,这是代码的一部分,不应由多个Goroutines同时执行。

当goroutine在互惠符上调用Unlock()时,它会释放锁。这允许其他等待的Goroutines获取锁并执行其关键部分。至关重要的是要确保Unlock()在关键部分之后被调用以释放静音并避免僵局。发布此锁后不应访问关键部分/共享资源。

package main

import (
    "fmt"
    "os"
    "sync"
)

func WriteToFile(filename string, contents string, buffer *[]byte, wg *sync.WaitGroup, mutex *sync.Mutex) {
    defer wg.Done()
    contentBytes := []byte(contents)

    mutex.Lock()
    *buffer = append(*buffer, contentBytes...)

    f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0644)
    if err != nil {
        fmt.Println(err)
    }
    defer f.Close()
    _, err = f.Write(contentBytes)
    if err != nil {
        fmt.Println(err)
    }
    mutex.Unlock()
}

func main() {
    var wg sync.WaitGroup
    var mut sync.Mutex
    var sharedBuffer []byte

    wg.Add(2)
    go WriteToFile("data/f1.txt", "Hello Gophers!\n", &sharedBuffer, &wg, &mut)
    go WriteToFile("data/f1.txt", "Welcome to Goworld!\n", &sharedBuffer, &wg, &mut)
    wg.Wait()

    fmt.Println(string(sharedBuffer))
}
$ go run --race mutex.go
Welcome to Goworld!
Hello Gophers!

$ go run --race mutex.go
Hello Gophers!
Welcome to Goworld!

$ go run --race mutex.go
Hello Gophers!
Welcome to Goworld!

$ go run --race mutex.go
Welcome to Goworld!
Hello Gophers!

上面的示例是对GO例程中使用的共享资源的竞赛条件的预防措施。

让我们逐步分解代码。首先,我们初始化了一些变量:

  • wg as sync.WaitGroup
    wg是一个候补组,将通过阻止主函数退出

  • 来用于Go-Routines的同步。
  • mut as sync.Mutex
    mut是一种内部保存一些整数值的结构,指示被阻塞或未阻止的状态。 sync.Mutex有两个私人领域。 statesema,该状态持有mutex状态0(解锁)或1锁定。 sema字段是用于阻断和信号传导的uint32,它是管理阻塞和解密goroutines试图获得互斥的信号。
    这将用于将数据写入文件或将数据附加到资源时,在共享资源上获取LockUnlock

  • sharedBuffer as []byte
    sharedBuffer是实际共享的资源,将用于保存字符串,以跟踪写入文件中的数据。这将是需要锁定其值(将其值(附加到切片)中)的资源。

我们将2添加到wg中,以表示要等待两个go routines完成,在下一行中,我们将两个GO例程称为函数WriteToFileWriteToFile是一个函数,它具有许多参数,即文件名,写入内容,对共享buffer,waitgroup和sutex的引用。

函数WriteToFile

  • 我们首先是defer waitgroups作为Done,即在函数调用末尾调用wg.Done方法。
  • string键入contents[]byte
  • 获取mutex.Lock(),即说“以下操作不应同时进行,一次。”然后,我们将contents附加到buffer,这是主要功能中sharedBuffer的指针,因此从本质上讲,我们试图在此功能中突变sharedBuffer
  • O_APPENDO_WRONLY打开文件,以指示应在附加/写作模式下打开该文件。 (我们在Golang: File Write文章中观察到了这种操作)
  • 使用Write方法将字节(内容)的切片写入我们打开的文件中。我们有一个defer用于关闭文件。

我们显然检查错误并打印它们,但根据应用程序正在执行的操作类型,它可能是paniclog
因此,这就是我们要做的所有操作,因此我们最终使用mutex.Unlock()打开锁定,该锁将允许其他程序(如果有的话)访问资源并继续操作。

阅读写相互排除锁

如果您有较重的操作应用程序,则相互排除锁是不错的。但是,如果我们以相同比例的比例更少或较少的读写操作(阅读重),我们不想在其他读者访问资源时被读者阻止,因为它不是突变过程。

我们可以允许许多读者同时阅读。但是对于写作操作,我们想阻止读者/作家。如果其他作家锁定,应首先对作者偏爱。这将阻止作家等待读者完成。这通常被称为读写互斥

package main

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

func reader(id int, count *int, wg *sync.WaitGroup, mutex *sync.Mutex) {
    defer wg.Done()
    if !mutex.TryLock() {
        fmt.Printf("Reader %d blocked!\n", id)
        return
    }
    defer mutex.Unlock()
    fmt.Printf("Reader %d: read count %d\n", id, *count)
}

func writer(id, increment int, count *int, wg *sync.WaitGroup, mutex *sync.Mutex) {
    defer wg.Done()
    defer mutex.Unlock()
    mutex.Lock()
    *count += increment
    time.Sleep(5 * time.Millisecond)
    fmt.Printf("Writer %d: wrote count %d\n", id, *count)
}

func main() {

    count := 1
    var wg sync.WaitGroup
    var mutex sync.Mutex
    readers := 5
    writers := 3
    wg.Add(readers)
    for i := 0; i < readers; i++ {
        go reader(i, &count, &wg, &mutex)
    }

    wg.Add(writers)
    for i := 0; i < writers; i++ {
        go writer(i, 1, &count, &wg, &mutex)
    }
    wg.Wait()

}
$ go run --race no-rwmutex.go 
Reader 0: read count 1
Reader 3 blocked!
Reader 2 blocked!
Reader 1 blocked!
Reader 4 blocked!
Writer 0: wrote count 2
Writer 1: wrote count 3
Writer 2: wrote count 4

$ go run --race no-rwmutex.go 
Reader 2 blocked!
Reader 0: read count 1
Reader 1 blocked!
Reader 4 blocked!
Reader 3 blocked!
Writer 2: wrote count 2
Writer 0: wrote count 3
Writer 1: wrote count 4

上面的示例具有readerwriter方法,reader方法只需读取共享资源count即可。在阅读之前,它获取了Mutex锁,然后将其解锁。同样,writer函数用于增加count共享资源。

reader方法具有TryLock方法,该方法试图在资源上获取Mutex锁定,如果资源已经锁定,该函数将返回false,因此我们可以说读数被阻止(仅用于演示)。如果函数TryLock返回true,它将获取Lock。我们将进一步defer Unlock并访问作为参考传递的count变量。

writer方法简单地获取了Lock并增加counter,然后使用Unlock来调用defer

在上述代码中:

  • readerwriter都可能在等待锁定锁定,但是,读者等待阅读是没有意义的。
  • 因为如果您只想阅读特定的内存地址,那么一位读者不应该等待其他读者完成。
  • 但是,要写,必须有一个锁。无论readerwriter,Mutex锁将锁定资源。

这可能在这里不那么可见,但可能是原因,所有readers由于另一个读者或作家的锁而被阻止。

sync软件包的RWMutex具有完全相同的操作。但是,它几乎与Mutex相似,它将允许并发阅读操作,并且更喜欢在读者面前的写作操作,以防止作家饥饿。

package main

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

func reader(id int, count *int, wg *sync.WaitGroup, mutex *sync.RWMutex) {
    defer wg.Done()
    defer mutex.RUnlock()

    if !mutex.TryRLock() {
        fmt.Printf("Reader %d blocked!\n", id)
        return
    }
    fmt.Printf("Reader %d: read count %d\n", id, *count)

}

func writer(id, increment int, count *int, wg *sync.WaitGroup, mutex *sync.RWMutex) {
    defer wg.Done()
    defer mutex.Unlock()
    mutex.Lock()

    *count += increment
    time.Sleep(5 * time.Millisecond)
    fmt.Printf("Writer %d: wrote count %d\n", id, *count)
}

func main() {

    count := 1
    var wg sync.WaitGroup
    var mutex sync.RWMutex
    readers := 5
    writers := 3
    wg.Add(readers)
    for i := 0; i < readers; i++ {
        go reader(i, &count, &wg, &mutex)
    }

    wg.Add(writers)
    for i := 0; i < writers; i++ {
        go writer(i, 1, &count, &wg, &mutex)
    }
    wg.Wait()

}
$ go run --race rwmutex.go 
Reader 0: read count 1
Reader 3: read count 1
Reader 1: read count 1
Reader 2: read count 1
Reader 4: read count 1
Writer 1: wrote count 2
Writer 0: wrote count 3
Writer 2: wrote count 4
Writer 3: wrote count 5
Writer 4: wrote count 6

$ go run --race rwmutex.go 
Reader 1 blocked!
Reader 0: read count 1
Reader 2 blocked!
Reader 3 blocked!
Reader 4 blocked!
Writer 4: wrote count 2
Writer 0: wrote count 3
Writer 1: wrote count 4
Writer 3: wrote count 5
Writer 2: wrote count 6

在修改后的示例中,所有逻辑保持不变,只有sync.Mutexsync.RWMutex替换。同样,用于尝试在reader方法中获取锁定TryRLock中的锁定代替TryLock,它将检查获得的现有锁是否是读者或作者的,如果是读者,它将返回true,else koode87。此外,Unlock还用RUnlock方法替换,用于释放读取锁。在writer方法中,一切都保持不变,因此作者必须获取锁定锁,无论当前锁是否来自读者/作者,所以这是普通的LockUnlock

在上面的示例中,我们可以看到所有读取操作有时被执行而不是被阻止。这是由于RWMutex锁并在读取操作/功能上解锁。

  • 当一个读者正在阅读时,它不能阻止其他读者。
  • 但是,对于简单的Mutex,当另一个读者正在阅读时,读者甚至被阻止。
  • 对于写操作,它将像往常一样被阻止,因此,如果作者执行写作操作并且读者/读者进来,它们将被阻止,同时,如果资源被锁定,则作家进来,作者将获得偏好,而不是等待所有读者完成。这阻止了作家饥饿。

您可以看到,读者仍然在大部分时间被阻止,但是,由于作者锁定资源而不是其他任何读者,它们被阻止了。

file读取和写作相关的example更合适,请务必检查一下,以更清楚地了解使用RWMutex的实际情况。

频道

这是一个很大的部分,我想在另一篇文章中深入研究这个话题。有一些模式,例如fan-infan-outworker-poolpub-sub等,它们在Web应用程序和后端系统中确实很常见。我们将在下一篇文章中探讨这些模式。

频道是一种提供一种安全和惯用的方式,使Goroutines交换数据并协调其执行,而无需求助于低级机制,例如共享内存或显式锁定。

就是这一系列的第30部分,示例的所有源代码都链接在100 days of Golang存储库的GitHub中。

100-days-of-Golang GitHub Repository

GitHub logo Mr-Destructive / 100-days-of-golang

Golang系列100天的脚本和资源

100 Days of Go lang

Go lang is a programming language which is easier to write and even provides type and memory safety, garbage collection and structural typing. It is developed by the Google Team in 2009 and open sourced in 2012. It is a programming language made for Cloud native technologies and web applications. It is a general purpose programming lanugage and hence there are no restrictions on the type of programs you wish to make in it.

学习golang

的资源

一些著名的应用程序!

Web应用程序 devops工具 CLI工具
SoundCloud-音乐系统 Prometheus-监视系统和时间序列数据库 gh-cli-官方GitHub Cli
Uber-乘车共享/出租车预订webApp

参考:

结论

从本系列的这一部分中,Golang的并发模型的基本面被理解为专门产生的go-routines,在等待组的帮助下同步执行go-routines,Mutex锁,以及如何确保同时访问共享资源。在系列的下一部分中,这些概念将用于使用频道的异步通信。

希望您已经从这篇文章中清除了Golang的并发基础。如果您有任何疑问,建议或反馈,请随时在下面发表评论,或在社交手柄上与我联系。谢谢您的阅读。快乐编码:)