sync

sync包提供了基本的同步基元,如互斥锁。除了Once和WaitGroup类型,大部分都是适用于低水平程序线程,高水平的同步使用channel通信更好一些。

本包的类型的值不应被拷贝。


1.type Locker interface

type Locker interface {
    Lock()   //用来设置互斥量的上锁
    Unlock() //用来解锁互斥量
}
  • Locker接口代表一个可以加锁和解锁的对象。

2.type Once struct

type Once struct {
    m    Mutex
    done uint32  //是否运行过的标志,默认为0,当被调用一次后值将会修改为1
}

func (o *Once) Do(f func())

var once Once

  • 如果once.Do(f)被多次调用,只有第一次调用会执行f,即使f每次调用Do 提供的f值不同。需要给每个要执行仅一次的函数都建立一个Once类型的实例。
代码示例
package main

import (
    "fmt"
    "sync"
)

func main() {
    //声明once变量
    var once sync.Once
    //用于记录once中的onceBody函数到底调用了几次
    var count int
    onceBody := func() {
        count++
        fmt.Println("Only once", count)
    }
    done := make(chan int)
    for i := 0; i < 10; i++ {
        go func() {
            once.Do(onceBody) //虽然被多次调用,但是只有第一次调用会执行onceBody函数
            done <- i //返回都为10,是因为只有i为10后才能到下面的接收通道处,通道的阻塞才会打开
        }()
    }
    for i := 0; i < 10; i++ {
        fmt.Println(<-done)
    }
}

返回:

userdeMBP:go-learning user$ go run test.go
Only once 1  //由此可见的确是只执行了一次
10
10
10
10
10
10
10
10
10
10

注意:无缓冲通道与缓冲通道为1的区别

  • 无缓冲通道channel必须在接受方与发送方同时准备好时,通道才能正常传递数据,否则双方只有一方在线都会阻塞。

  • 缓冲通道当缓冲区满时,发送数据会阻塞,当缓冲区空时,接受数据会阻塞。发送方与接收方不需要同时做好准备

//无缓存,由于是同步逻辑,则会导致死锁,需开启线程并发执行才不会出错

package main
 
import (
	"fmt"
)
 
func main() {
	ch := make(chan int)
	// ch := make(chan int,1)  缓冲为1的通道则没有问题
	ch <- 1  
	fmt.Println(<-ch)
}

3. type Mutex struct

复制代码
//Mutex是一个互斥锁
// 零值为解锁状态
//
// A Mutex must not be copied after first use.
type Mutex struct {
    state int32   //0为解锁,1为上锁
    sema  uint32   //等待信号
}

Mutex是一个互斥锁,通常为其他结构体的字段,通过加锁控制,实现并发安全;
零值为解锁状态。Mutex类型的锁和线程无关,可以为不同的线程加锁和解锁

func (m *Mutex) Lock()
Lock方法锁住m,如果m已经加锁,则阻塞直到m解锁。

func (m *Mutex) Unlock()
Unlock方法解锁m,如果m未加锁会导致运行时错误。锁和线程无关,可以由不同的线程加锁和解锁。

4.type RWMutex struct 读写锁

type RWMutex struct {
    w           Mutex  // held if there are pending writers
    writerSem   uint32 // semaphore for writers to wait for completing readers
    readerSem   uint32 // semaphore for readers to wait for completing writers
    readerCount int32  // number of pending readers
    readerWait  int32  // number of departing readers
}
  • RWMutex是读写互斥锁。该锁可以被同时多个读取者持有或唯一个写入者持有。RWMutex可以创建为其他结构体的字段;零值为解锁状态。RWMutex类型的锁也和线程无关,可以由不同的线程加读取锁/写入和解读取锁/写入锁
func (rw *RWMutex) Lock()  //Lock方法将rw锁定为写入状态,禁止其他线程读取或者写入

func (rw *RWMutex) Unlock()  //Unlock方法解除rw的写入锁状态,如果m未加写入锁会导致运行时错误。

func (rw *RWMutex) RLock()  //可多读,唯一写,RLock方法将rw锁定为读取状态,禁止其他线程写入,但不禁止读取。

func (rw *RWMutex) RUnlock() //Runlock方法解除rw的读取锁状态,如果m未加读取锁会导致运行时错误。

5. type Cond struct 条件变量

cond的主要作用就是获取锁之后,wait()方法会等待一个通知,来进行下一步锁释放等操作,
条件变量的作用并不是保证在同一时刻仅有一个线程访问某一个共享数据,而是在对应的共享数据的状态发生变化时,通知其他因此而被阻塞的线程

type Cond struct {
    // 在观测或更改条件时L会冻结
    L Locker
    notify  notifyList
	checker copyChecker
	noCopy noCopy
}
package main 
import(
    "fmt"
    "sync"
    "time"
)
var locker = new(sync.Mutex)//创建一个互斥锁
var cond = sync.NewCond(locker)

func testCond(x int){
    cond.L.Lock()//上锁,该goroutine获取锁
    cond.Wait()  //在获取锁后用于等待通知,只有调用Signal()或Broadcast()后才获得通知进行下一步操作,暂时阻塞
    x += 5
    fmt.Println(x)
    time.Sleep(time.Second * 1) //等待一秒后再去释放锁
    cond.L.Unlock() //用于释放锁
}

func main() {
    for i := 0; i < 4; i++{
        go testCond(i)
    }
    fmt.Println("start test")
    time.Sleep(time.Second * 3) //等待3秒后下发一个通知给等待通知的goroutine
    cond.Signal() //下发一个通知
    time.Sleep(time.Second * 3) //等待3秒后再下发一个通知给等待通知的goroutine
    cond.Signal() //下发一个通知
    time.Sleep(time.Second * 3) //等待3秒后直接广播给所有等待通知的goroutine
    cond.Broadcast()
    time.Sleep(time.Second * 4)
}

6.type WaitGroup struct

type WaitGroup struct {
    noCopy noCopy

    // 64-bit value: high 32 bits are counter, low 32 bits are waiter count.
    // 64-bit atomic operations require 64-bit alignment, but 32-bit
    // compilers do not ensure it. So we allocate 12 bytes and then use
    // the aligned 8 bytes in them as state, and the other 4 as storage
    // for the sema.
    state1 [3]uint32
}

WaitGroup用于等待一组线程的结束。父线程调用Add方法来设定应等待的线程的数量。每个被等待的线程在结束时应调用Done方法。同时,主线程里可以调用Wait方法阻塞至所有线程结束。

除了可以使用通道(channel)和互斥锁进行两个并发程序间的同步外,还可以使用等待组进行多个任务的同步,等待组可以保证在并发环境中完成指定数量的任务

package main

import (
    "fmt"
    "net/http"
    "sync"
)

func main() {

    // 声明一个等待组
    var wg sync.WaitGroup

    // 准备一系列的网站地址
    var urls = []string{
        "http://www.github.com/",
        "https://www.qiniu.com/",
        "https://www.golangtc.com/",
    }

    // 遍历这些地址
    for _, url := range urls {

        // 每一个任务开始时, 将等待组增加1
        wg.Add(1)

        // 开启一个并发
        go func(url string) {

            // 使用defer, 表示函数完成时将等待组值减1
            // wg.Done() 方法等效于执行 wg.Add(-1)
            defer wg.Done() 

            // 使用http访问提供的地址
            // Get() 函数会一直阻塞直到网站响应或者超时
            _, err := http.Get(url)

            //在网站响应和超时后,打印这个网站的地址和可能发生的错误
            fmt.Println(url, err)

            // 通过参数传递url地址
        }(url)
    }

    // 等待所有的网站都响应或者超时后,任务完成,Wait 就会停止阻塞。
    wg.Wait()

    fmt.Println("over")
}

7.type Pool struct

type Pool struct {
    // 可选参数New指定一个函数在Get方法可能返回nil时来生成一个值
    // 该参数不能在调用Get方法时被修改
    New func() interface{}
    // 包含隐藏或非导出字段
}

如果Pool为空,则调用New返回一个新创建的对象。如果没有设置New,则返回nil。
pool的容量受限于内存,缓存时间为两次垃圾回收的间隔时间,
垃圾回收GC是固定两分钟触发一次,每次清理会将Pool中的所有对象都清理掉!
Pool的一个好例子在fmt包里。该Pool维护一个动态大小的临时输出缓存仓库。该仓库会在过载(许多线程活跃的打印时)增大,在沉寂时缩小。

package main 
import(
    "fmt"
    "sync"
)


func main() {
    //创建一个对象,如果pool为空,就调用该New;如果没有定义New,则返回nil
    pipe := &sync.Pool{
        New: func() interface{} {
            return "hello ,i am New"
            },
    }
    //在pool中放入字符串
    pipe.Put("put some string to Pool")
    //然后将其取出
    fmt.Println(pipe.Get())
    //如果再取就没有了,会自动调用New
    fmt.Println(pipe.Get())
}

返回

userdeMacBook-Pro:go-learning user$ go run test.go
put some string to Pool
hello ,i am New

如果没有New且Pool为空,Get()返回

package main 
import(
    "fmt"
    "sync"
)


func main() {
    //创建一个对象,如果pool为空,就调用该New;如果没有定义New,则返回nil
    pipe := &sync.Pool{}
    //然后将其取出,因为没有New,且pool为空,返回nil
    fmt.Println(pipe.Get()) //<nil>
}
posted @ 2020-08-14 16:35  fanzou  阅读(431)  评论(0编辑  收藏  举报