Golang:并发

文章详细介绍了进程、线程和协程的基本概念,指出协程在轻量级和效率上的优势,适合于I/O密集型任务。并对比了并发与并行的区别,强调并行是理想的并发执行模式。文中以Go语言为例,展示了Goroutine的创建、管道的使用以及如何通过select进行多路复用。还讨论了runtime包中的Gosched和Goexit函数,以及sync包中的同步原语如WaitGroup和锁机制。
摘要由CSDN通过智能技术生成

基本概念:

进程Process 与线程 Thread

进程定义:进程 是并发执行的程序中分配和管理资源的基本单位。

线程定义:线程是进程的执行单元,是进行调度的实体,是比进程更小的独立运行单位。

进程Process 与协程 coroutine

  1. 线程是由操作系统调度的,而协程是由程序员控制的。
  2. 线程需要更多的资源,包括内存和 CPU 时间,而协程则比较轻量级,可以创建大量的协程。
  3. 线程之间的切换需要操作系统的介入,而协程之间的切换则是程序员控制的,更加高效。
  4. 线程是操作系统级别的,可以利用多核 CPU,而协程则是在单个线程内运行,不能直接利用多核 CPU,但可以通过多个协程在多个线程上运行来实现并发。

综上所述,协程比线程更加轻量级和高效,但也有其局限性。在 CPU 密集型任务中,使用线程可能更加适合,而在 I/O 密集型任务中,使用协程则更加合适。

并行Concurrent与并发Paralled

并发定义: 多线程交替操作同一资源类,逻辑上具备同时处理多个任务的能力

并行定义:多个线程同时操作多个资源类,物理上在同一时刻执行多个并发任务

(并行是并发设计的理想执行模式)

图解:

Goroutine

只须在函数调用前添加go关键字即可创建并发任务

关键字go并非执行并发操作,而是创建一个并发任务单元。新建任务被放置在系统队列中,等待调度器安排合适系统线程去获取执行权。

基本操作

func hello() {
    fmt.Println("Hello Goroutine!")
}
func main() {
    go hello()
    fmt.Println("main goroutine done!")
}

管道

package main

func main() {
   done := make(chan struct{}) //结束事件
   c := make(chan string)      //数据传输

   go func() {
      s := <-c //接受消息
      println(s)
      close(done) //关闭通道,作为结束通知
   }()

   c <- "hi" //发送消息
   <-done    //阻塞,直到有数据或管道关闭
}

关闭后的通道有以下特点:

    1.对一个关闭的通道再发送值就会导致panic。
    2.对一个关闭的通道进行接收会一直获取值直到通道为空。
    3.对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。
    4.关闭一个已经关闭的通道会导致panic。

可以使用ok-idom或range模式处理数据

 go func() {
        for {
            i, ok := <-ch1 // 通道关闭后再取值ok=false
            if !ok {
                break
            }
            ch2 <- i * i
        }
        close(ch2)
    }()

 for i := range ch2 { // 通道关闭后会退出for range循环
        fmt.Println(i)
    }

无论收发,nil管道都会阻塞

单向管道

管道默认是双向的,并不区分发送和接受端,但某些时候,我们可限制收发操作的方向来获得更严谨的操作逻辑

尽管可用make创建单向管道,但那没有任何意义,通常使用类型转换来获取单向管道,并赋予操作双方

func main() {
   var wg sync.WaitGroup
   wg.Add(2)

   c := make(chan int)
   var send chan<- int = c
   var recv <-chan int = c

   go func() {
      defer wg.Done()

      for x := range recv {
         println(x)
      }
   }()
   go func() {
      defer wg.Done()
      defer close(c)

      for i := 0; i < 3; i++ {
         send <- i
      }
   }()
   wg.Wait()
}

注意:

1、不能在单项管道上做逆向操作

2、close不能用于接受端

3、无法将单向管道重新转回去

select(多路复用)

如要同时处理多个管道,可选用select语句,它会随机选择一个可用管道做收发操作

    select {
    case <-chan1:
       // 如果chan1成功读到数据,则进行该case处理语句
    case chan2 <- 1:
       // 如果成功向chan2写入数据,则进行该case处理语句
    default:
       // 如果上面都没有成功,则进入default处理流程
    }

runtime

runtime.Gosched()

暂停,释放线程去执行其他任务。当前任务被放回队列,等待下次调度时恢复执行

package main

import (
    "fmt"
    "runtime"
)

func main() {
    go func(s string) {
        for i := 0; i < 2; i++ {
            fmt.Println(s)
        }
    }("world")
    // 主协程
    for i := 0; i < 2; i++ {
        // 切一下,再次分配任务
        runtime.Gosched()  //Gosched 产生处理器,允许其他 goroutines 运行。它没有 暂停当前的 GoRoutine,以便自动恢复执行
        fmt.Println("hello")
    }
}

//结果
//world
//world
//hello
//hello
//如果不执行这个,则结果
//hello
//hello

runtime.Goexit()

立即终止当前任务,运行时确保所有已注册延迟调用被执行。该函数不会影响其他并发任务,不会引起panic,自然也就无法捕获

package main

import (
    "fmt"
    "runtime"
)

func main() {
    go func() {
        defer fmt.Println("A.defer")
        func() {
            defer fmt.Println("B.defer")
            // 结束协程
            runtime.Goexit()
            defer fmt.Println("C.defer")
            fmt.Println("B")
        }()
        fmt.Println("A")
    }()
    for {
    }
}

runtime.GOMAXPROCS()

Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

func a() {
    for i := 1; i < 10; i++ {
        fmt.Println("A:", i)
    }
}

func b() {
    for i := 1; i < 10; i++ {
        fmt.Println("B:", i)
    }
}

func main() {
    runtime.GOMAXPROCS(2)
    go a()
    go b()
    time.Sleep(time.Second)
}

Sync

sync.WaitGroup

方法名功能
(wg * WaitGroup) Add(delta int)计数器+delta
(wg *WaitGroup) Done()计数器-1
(wg *WaitGroup) Wait()阻塞直到计数器变为0

基本操作:

var wg sync.WaitGroup

func hello() {
    defer wg.Done()
    fmt.Println("Hello Goroutine!")
}
func main() {
    wg.Add(1)
    go hello() // 启动另外一个goroutine去执行hello函数
    fmt.Println("main goroutine done!")
    wg.Wait()
}

并发安全和锁

互斥锁

全局变量 通过加锁lock unlock 的方法 达到线程安全

lock sycn.Mutex

lock.Lock() 等使用完 lock.Unlock()

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
    for i := 0; i < 5000; i++ {
        lock.Lock() // 加锁
        x = x + 1
        lock.Unlock() // 解锁
    }
    wg.Done()
}
func main() {
    wg.Add(2)
    go add()
    go add()
    wg.Wait()
    fmt.Println(x)
}

读写锁

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。

读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果是获取读锁会继续获得锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是获取读锁还是写锁都会等待。

package main

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

var wg sync.WaitGroup //只定义无需赋值
// 加入读写锁:
var lock sync.RWMutex

func read() {
   defer wg.Done()
   lock.RLock() //如果只是读数据,那么这个锁不产生影响,但是读写同时发生的时候,就会有影响
   fmt.Println("开始读取数据")
   time.Sleep(time.Second)
   fmt.Println("读取数据成功")
   lock.RUnlock()
}
func write() {
   defer wg.Done()
   lock.Lock()
   fmt.Println("开始修改数据")
   time.Sleep(time.Second * 10)
   fmt.Println("修改数据成功")
   lock.Unlock()
}
func main() {
   wg.Add(6)
   //启动协程 ---> 场合:读多写少
   for i := 0; i < 5; i++ {
      go read()
   }
   go write()
   wg.Wait()
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值