首页 > 编程语言 >第七章:并发编程

第七章:并发编程

时间:2023-04-03 20:55:56浏览次数:45  
标签:Println 第七章 fmt 编程 并发 信道 func go 执行

第七章:并发编程

目录

一、并发与并行

并发:同一时间段内,多个任务在执行(单个cpu,执行多个任务)。

并行:同一时刻,多个任务在执行(多个cpu的支持)。

二、Go协程(Goroutine)

1 Go协程介绍

单线程下实现并发,也就是协程。其实是多个线程下的多个协程。

  • 相比线程而言,go协程的成本极低。堆栈大小只有若干kb(2kb),并且可以根据需求进行增减。而线程是必须指定堆栈大小的,其堆栈是固定不变的。
  • Go协程会复用数量更少的OS线程,即便程序可能有数以千计的Go协程,也可能只有一个线程。
  • Go协程使用信道(channel)来进行通讯。信道用于防止多个协程访问共享内存时发生竞态条件(Race Condition)。信道可以看作是Go协程之间通信的管道。

goroutine协程 ----> 2kb大小

线程 ----> 几M

go推崇用信道通信,而不推崇用共享变量通信,因为使用共享变量通讯,会涉及到锁,有锁就会有死锁。

2 启动Go协程

go的主线程不会等待Goroutine执行完毕,和python不同。go没有python中的join、守护线程之类的方式。

func test(){
    fmt.Println("go go go")
}

func main(){
    fmt.Println("主线程开始执行...")  
    go test()
    fmt.Println("主线程结束执行...")
}

// 打印结果 
主线程开始执行...  // go的主线程不会等待Goroutine执行完毕,和python不同
主线程结束执行...
// 睡一会
import (
	"fmt"
	"time"
)

func test(){
	fmt.Println("go go go")
}

func main(){
	fmt.Println("主线程开始执行...")
	go test() 
	time.Sleep(1 * time.Second)  // 暂停几秒
	fmt.Println("主线程结束执行...")
}

// 打印结果
主线程开始执行...
go go go
主线程结束执行...

go语言中使用匿名函数开协程。go 关键字开启goroutine,一个goroutine只占2kb。

func main(){
	fmt.Println("主线程开始执行...")
	for i := 0; i < 10; i++{
         // 匿名函数
		go func() {
			fmt.Println("go go go")
		}()
	}
	fmt.Println("主线程结束执行...")
}

// 打印结果【循环了10次,但只打印了5次。。。】
主线程开始执行...
go go go
go go go
主线程结束执行...
go go go
go go go
go go go

3 GMP调度模型

  • G:Goroutine;
  • M:当成是操作系统真正的线程,实际上是用户线程;
  • P:Processor,现在版本默认时cpu核数;

python中,开的线程开出用户线程,用户线程跟操作系统是1:1的对应关系。

某些语言,用户线程和操作系统是n:1的关系。

go语言,用户线程和操作系统是n:m的关系。

简单参考下两图:

// 演示
func main() {
    //设置P的大小,认为是cpu核数即可
    runtime.GOMAXPROCS(1)
    fmt.Println("主线程开始执行")
    go func() {
        for  {  // 死循环
            fmt.Println("我是死循环...")

        }
    }()
    time.Sleep(10*time.Second)
    fmt.Println("主线程结束执行")
} 

三、信道(Channel)

1 信道使用

不同Goroutine之间通讯,通过channel实现。

func main(){
    // 1 定义信道,不同协程之间通讯
    var c chan int
    // 2 信道的零值(引用类型,空值为nil,当做参数传递时,不需要取地址,改的就是原来的,需要初始化再使用)
    fmt.Println(c)  // <nil>
    // 3 信道初始化
    c = make(chan int)  // 后面的数字暂时先不关注
    // 4 信道的存值和取值
    c <- 1  // 信道存值
    // c = 12  // 报错,类型不匹配。chan int不等于int
    a := <-c  // 信道取值,取出来是一个int
    
    // 5 信道默认不管放值还是取值,都是阻塞的。
    
    // 操作
    // 由于c是引用类型,所以直接传递,就会修改原值
    go test(c)
    
    a := <-c  // 阻塞【不但实现了两条协程之间通信,还实现了等待协程执行结束】
    
}


func test(a chan int){
    fmt.Println("go go go")
    time.Sleep(1 * time.Second)
    // 往信道中放一个值
    a <- 100  // 阻塞
}

总结:信道的存值和取值默认是阻塞的。这个就相当于是pv操作一样。

信道小例子:

package main


// 程序有一个数中,每一位的平方和与立方和,然后把平方和与立方和相加并打印出来
import (
	"fmt"
	"time"
)

func calcSquares(number int, squareop chan int) {
	sum := 0  //总和
	for number != 0 {
		digit := number % 10   //589对10取余数,9   8   5
		sum += digit * digit  //sum=9*9   8*8     5*5
		number /= 10         //num=58    5       0
	}
	squareop <- sum  // 程序阻塞
}

func calcCubes(number int, cubeop chan int) {
	sum := 0
	for number != 0 {
		digit := number % 10
		sum += digit * digit * digit
		number /= 10
	}
	cubeop <- sum  // 程序阻塞
}

func main() {
	startTime := time.Now().Unix()
	number := 589
	sqrch := make(chan int)
	cubech := make(chan int)
	go calcSquares(number, sqrch)  // 开设两个goroutine,时间减少
	go calcCubes(number, cubech)
	squares, cubes := <-sqrch, <-cubech  // 程序阻塞
	fmt.Println("Final output", squares + cubes)
	endTime := time.Now().Unix()
	fmt.Println("程序执行:", endTime - startTime, "秒")
}

// 执行结果
Final output 1536
程序执行: 0 秒  // 真的强

多线程应用场景:原来线性执行的东西,让它们并发执行起来。

2 死锁现象

信道的死锁现象,默认都是阻塞的,一旦有一个人放没有人取,或者有人取没有人放,就会出现死锁现象。

// 死锁演示
func main(){
    var c chan int = make(chan int)
    // 放了没有人取
    // c <- 1  
    // 取没有人放
    <-c
}

// 打印提示死锁

3 单向信道【了解】

简单来说,就是控制在一个goroutine,只负责读,或者只负责写。

func sendData(sendch chan<- int) {  // 定义一个
	sendch <- 10  // 写数据
}

func main() {
	//sendch := make(chan<- int)   //定义了一个只写信道
	sendch := make(chan int)   //定义了一个可读可写信道
	go sendData(sendch)        //传到函数中转成只写信道,在goroutine中,只负责写,不能往外读,主协程读
	fmt.Println(<-sendch)   //只写信道一旦读,就有问题
}

4 关闭信道

关闭信道之后,不能在进行读写。

func main(){
    sendch := make(chan int)  
    close(sendch)  // 关闭信道
}

5 循环信道

package main

import "fmt"

func producer(chnl chan int) {
	for i := 0; i < 10; i++ {
		chnl <- i
	}
	close(chnl)
}

func main() {
	ch := make(chan int)
	go producer(ch)
	for v := range ch {
		fmt.Println("Received ",v)
	}
}

// 打印结果
Received  0
Received  1
Received  2
Received  3
Received  4
Received  5
Received  6
Received  7
Received  8
Received  9

四、缓冲信道

1 缓冲信道

信道默认是阻塞的,缓冲信道只有放满了或者取不出去了,才会阻塞。【可以定义一个缓冲长度】。无缓冲信道,数字其实是0。【其实,我个人感觉数字默认是1,建议书写的时候用缓冲信道,并指定大小即可】【这里其实就是操作系统中pv操作】

// 缓冲信道
var c chan int =make(chan int,6)  //无缓冲信道数字是0
c<-1
c<-2
c<-3
c<-4
c<-5
c<-6
//c<-7  //死锁
<-c
<-c
<-c
<-c
<-c
<-c

死锁现象,只会出现在一个goroutine中,如果是多个goroutine,会一直进行阻塞,不会出现死锁现象。

信道的长度和容量:

len(c)  // 目前放了多少
cap(c)  // 最多可以放多少

2 WaitGroup

WaitGroup等待所有goroutine执行完成。之前使用缓冲信道来实现。

func process1(i int,wg *sync.WaitGroup)  {
	fmt.Println("started Goroutine ", i)
	time.Sleep(2 * time.Second)
	fmt.Printf("Goroutine %d ended\n", i)
	//一旦有一个完成,减一
	wg.Done()  
}

func main() {
	var wg sync.WaitGroup   //没有初始化,值类型,当做参数传递,需要取地址
	//fmt.Println(wg)
	for i:=0;i<10;i++ {
		wg.Add(1) //启动一个goroutine,add加1
		go process1(i,&wg)
	}

	wg.Wait() // 一直阻塞在这,直到调用了10个done,计数器减到零
}

// 执行结果
started Goroutine  0
started Goroutine  3
started Goroutine  1
started Goroutine  9
started Goroutine  4
started Goroutine  5
started Goroutine  6
started Goroutine  7
started Goroutine  8
started Goroutine  2
Goroutine 8 ended
Goroutine 7 ended
Goroutine 6 ended
Goroutine 5 ended
Goroutine 4 ended
Goroutine 3 ended
Goroutine 0 ended
Goroutine 1 ended
Goroutine 9 ended
Goroutine 2 ended

补充:可以使用缓冲信道,来实现上述效果。

五、select

select用于在多发送/接受信道操作中进行选择,select语句会一直阻塞,直到发送/接受操作准备就绪。如果有多个信道操作准备完毕,select会随机选取其中之一执行。该语法与switch类似,所有不同的是,这里的每个case语句都是信道操作。

// 方式一: 信道中谁先回来,就执行谁,其他的丢弃【可以做爬虫使用】,简单来说,就是挑最快的去执行即可。【找最快的线路】

package main

import (
"fmt"
"time"
)

func server1(ch chan string) {
	time.Sleep(3 * time.Second)
	ch <- "from server1"
}


func server2(ch chan string) {
	time.Sleep(3 * time.Second)
	ch <- "from server2"

}

func main() {
	output1 := make(chan string)
	output2 := make(chan string)
	//开启两个协程执行server
	go server1(output1)
	go server2(output2)
    // select
	select {
	case s1 := <-output1:
		fmt.Println(s1)
	case s2 := <-output2:
		fmt.Println(s2)
	}
}

// 执行结果
from server2
// 方式二: 不是一直卡住,而是如果卡住,执行其他的事
func process(ch chan string) {
	time.Sleep(10500 * time.Millisecond)
	ch <- "process successful"
}

func main() {
	ch := make(chan string)
	go process(ch)  
	for {  // 死循环
		time.Sleep(1000 * time.Millisecond)
		select {
		case v := <-ch:
			fmt.Println("received value: ", v)
			return
		default:  // 如果不写default,会出现死锁【只有一个goroutine】
			// 可以干其他事,模拟非阻塞式io【逻辑自写】
			fmt.Println("no value received")
		}
	}
}

六、mutex

使用锁的场景:多个goroutine需要访问同一些资源,也就是临界区的概念。由此出现了并发安全的概念,并发安全,需要加锁处理。【修改共享资源的代码叫做临界区。】

如果在任意时刻只允许一个 Go 协程访问临界区,那么就可以避免竞态条件。而使用 Mutex 可以达到这个目的。【python中通过队列来实现通讯】

mutex.Lock()  // 加锁
x = x + 1
mutex.Unlock()  // 解锁
var x  = 0  //全局,各个goroutine都可以拿到并且操作
func increment(wg *sync.WaitGroup,m *sync.Mutex) {
	m.Lock()  // 加锁
	x = x + 1  // 临界区资源
	m.Unlock()  // 解锁
	wg.Done()
}

func main() {
	var w sync.WaitGroup  
	var m sync.Mutex  // 是个值类型,函数传递需要传地址
	fmt.Println(m)
	for i := 0; i < 1000; i++ {
		w.Add(1)
		go increment(&w,&m)
	}
	w.Wait()  // 等待所有goroutine执行完毕
	fmt.Println("final value of x", x)
}


// >>>>>>>>>>>>>>>>>>>通过缓冲信道来实现>>>>>>>>>>>>>>>>>>>>>>
var x  = 0
func increment(wg *sync.WaitGroup, ch chan bool) {
	ch <- true  // 缓冲信道放满了,就会阻塞
	x = x + 1
	<- ch
	wg.Done()
}

func main() {
	var w sync.WaitGroup
	ch := make(chan bool, 1)  //定义了一个有缓存大小为1的信道
	for i := 0; i < 1000; i++ {
		w.Add(1)
		go increment(&w, ch)
	}
	w.Wait()
	fmt.Println("final value of x", x)
}

不同goroutine之间传递数据:①共享变量 ② 信道

  • 如果是修改共享变量,建议加锁;
  • 如果是协程之间通讯,建议用信道;

七、异常处理

1 defer:延迟执行,并且程序出现严重错误也会执行

// 注册一下,并不执行,等main函数执行完了以后,从下往上执行defer定义的东西
func main() {
	defer fmt.Println("我是最后执行的")
	defer fmt.Println("我是倒数第二个执行")
	fmt.Println("我先执行")
}

// 输出结果
我先执行
我是倒数第二个执行
我是最后执行的

案例使用:打开文件等

// 伪代码
f := open()
defer f.close()  // 不管如何,都会执行这里
操作文件
// f.close() 可以不用写

2 panic:主动抛出异常。【python中的raise】

panic("我出错了")  // python中的raise
// 举例子
func f1(){
	fmt.Println("f1 f1")
}
func f2(){
	fmt.Println("f2 f2")
	panic("主动抛出错误")
}
func f3(){
	fmt.Println("f3 f3")
}

func main() {
	f1()
	f2()  // 抛出错误
	f3()  // 不会被执行掉
}

// >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
// 所以我们需要对f2跑出来的错误进行异常捕获,让程序继续运行
func f1(){
	fmt.Println("f1 f1")
}
func f2(){
    defer func(){  // 这个匿名函数永远会执行
        recover()  // 恢复错误,继续执行
    }()
	fmt.Println("f2 f2")
	panic("主动抛出错误")
}
func f3(){
	fmt.Println("f3 f3")
}

func main() {
	f1()
	f2()  // 抛出错误
	f3()  
}

3 recover:恢复程序,继续执行。【其实就是python中except中的东西】。如果没有错误,执行recover会返回nil,如果有错误,执行recover返回错误信息。

// 固定写法
func f1(){
	fmt.Println("f1 f1")
}
func f2(){
    defer func(){  // 这个匿名函数永远会执行
        if error:=recover(); error!=nil{
            // 表示出错了,打印一下错误信息,程序恢复了,继续执行
            fmt.Println(error)
        }
        fmt.Println("我永远会执行,无论是否出错...")
    }()
	fmt.Println("f2 f2")
	panic("主动抛出错误")
}
func f3(){
	fmt.Println("f3 f3")
}

func main() {
	f1()
	f2()  // 抛出错误
	f3()  // 不会被执行掉
}
# python中的写法
try:
    可能出现的代码
expect Exception as e:
    print(e)
finally:
    无论是否出错都会执行
// go中的写法【如果觉得谁出错,就写在谁的上面即可】
defer func(){  // 这个匿名函数永远会执行
    if error:=recover(); error!=nil{
         // 表示出错了,打印一下错误信息,程序恢复了,继续执行【python中except中的东西】
         fmt.Println(error)
    }
    // 此处相当于python中的finally,无论是否出错都会执行
}()

go的错误处理:

func main() {
	f, err := os.Open("/test.txt")  
	if err != nil {
		fmt.Println(err)
		return  // 退出main主函数
	}
	fmt.Println(f.Name(), "opened successfully")
}

标签:Println,第七章,fmt,编程,并发,信道,func,go,执行
From: https://www.cnblogs.com/yangyi215/p/17284387.html

相关文章

  • 我的第一个win32汇编程序
    .386.ModelFlat,stdcalloptioncasemap:none;头文件包含includewindows.incincludekernel32.incincludelibkernel32.libincludeuser32.incincludelibuser32.libincludegdi32.incincludelibgdi32.lib;数据段定义.datahInstancedd......
  • 【Java 并发】【七】【Unsafe】什么是Unsafe及其作用
    1 前言这节我们来看看JDK底层的unsafe,因为很多的操作都是依赖于unsafe提供的功能的。2  unsafe是什么?unsafe是JDK提供的一个工具类,里面的方法大多是native方法,unsafe类是JDK给你提供的一个直接调用操作系统底层功能的一个工具类,unsafe提供了非常多操作系统级别的方法。(1)比......
  • 函数式编程-高阶函数
    函数本身也可以赋值给变量,即:变量可以指向函数  那么函数名是什么呢?函数名其实是指向函数的变量!对于abs()这个函数,完全可以把函数名abs看成变量,它指向一个可以计算绝对值的函数! 既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函......
  • 【CSAPP】进程 | 上下文切换 | 用户视角下的并发进程
     ......
  • 编程里同步和异步的选择
    同步是指方法调用后必须等到返回才会执行后续代码异步是方法调用没等到返回也可以继续执行后续代码在java中如果是多线程,而各线程间会公用一个变量时,需要使用同步来保证线程安全,否则异步就是最好的在前端js中,一般http请求都是默认异步的,允许在发送http请求时执行其他函数,这样......
  • 零基础Go语言从入门到精通(数据库编程:02-Gorm 操作 MySQL 数据库)
    gin-gorm-api-example/main.goatmaster·cgrant/gin-gorm-api-example·GitHubhttps://github.com/cgrant/gin-gorm-api-example/blob/master/main.goGorm介绍ThefantasticORMlibraryforGolangGo语言的超棒的ORM类库功能强大:全功能ORM(几乎)关联(包含一个,包含多个,属......
  • 网络原理与网络编程
     io模型有哪些网络io模型?哪些网络操作可以是异步的?常见的网络IO模型有:同步阻塞IO,同步非阻塞IO,多路复用IO和异步IO。异步网络操作包括:连接请求,数据发送和数据接收。(不确定)select/poll/epollselect/poll与epoll区别select和poll是两个系统调用,用于监视多个......
  • Rust编程语言入门
    Rust编程语言入门Rust简介为什么要用Rust?Rust是一种令人兴奋的新编程语言,它可以让每个人编写可靠且高效的软件。它可以用来替换C/C++,Rust和他们具有同样的性能,但是很多常见的bug在编译时就可以被消灭。Rust是一种通用的编程语言,但是它更善于以下场景:需要运行时的速度需......
  • 2023 - Dubbo 谷歌编程之夏报名启动了!
    作者:Dubbo社区我们很高兴地宣布ApacheDubbo已正式参与到GSoC2023(2023谷歌编程夏令营)中,当前贡献者报名阶段也已经正式启动,如果您对Dubbo、对GSoC、对开源感兴趣,欢迎报名参与。今年的活动同时对在校大学生、社会员工开放。也就是说,只要是对开源和编码感兴趣的开发者就可以......
  • C++ Primer 第五版 第十一章 练习题编程题目答案
    https://github.com/jzplp/Cpp-Primer-Answer练习11.1map用关键字索引,是一个字典。vector用整数索引,是一个列表。练习11.2list链表vector顺序列表deque双端队列map字典set集合练习11.311.3map单词计数程序代码练习11.411.4去标点map单词计数程序代码练习11.5如果关键......