GoLong的学习之路(二十三)进阶,语法之并发(go最重要的特点)(锁,sync包,原子操作)

这章是我并发系列中最后的一章。这章主要讲的是锁。但是也会讲上一章channl遗留下的一些没有讲到的内容。select关键字的用法,以及错误的一些channl用法。废话不多说。。。

文章目录

  • select多路复用
  • 通道错误示例
  • 并发安全和锁
    • 问题描述
    • 互斥锁
    • 读写互斥锁
  • sync
    • sync.WaitGroup
      • 加载配置文件示例
      • 并发安全的单例模式
    • sync.Map
  • 原子操作
    • 读取操作
    • 写入操作
    • 修改操作
    • 交换操作
    • 比较并交换操作

select多路复用

使用场景:需要同时从多个通道接收数据

通道在接收数据时,如果没有数据可以被接收那么当前 goroutine 将会发生阻塞。

当然办法不是没有。遍历呗。

for{// 尝试从ch1接收值data, ok := <-ch1// 尝试从ch2接收值data, ok := <-ch2…
}

这种方式虽然可以实现从多个通道接收值的需求,但是程序的运行性能会差很多。

Go 语言内置了select关键字,使用它可以同时响应多个通道的操作。

Select 的使用方式类似于之前学到的 switch 语句,它也有一系列 case 分支一个默认的分支

每个 case 分支会对应一个通道的通信(接收或发送)过程。select 会一直等待,直到其中的某个case的通信操作完成时,就会执行该 case 分支对应的语句

select {
case <-ch1://...
case data := <-ch2://...
case ch3 <- 10://...
default://默认操作
}

Select 语句具有以下特点:

  • 可处理一个或多个 channel 的发送/接收操作。
  • 如果多个 case 同时满足select随机选择一个执行。
  • 对于没有caseselect 会一直阻塞,可用于阻塞 main 函数,防止退出。
package mainimport "fmt"func main() {ch := make(chan int, 1)for i := 1; i <= 10; i++ {select {case x := <-ch:fmt.Println(x)case ch <- i:}}
}

在这里插入图片描述
代码首先是创建了一个缓冲区大小为1的通道 ch,进入 for 循环后:

  • 第一次循环时 i = 1,select 语句中包含两个 case 分支,此时由于通道中没有值可以接收,所以x := <-ch 这个 case 分支不满足,而ch <- i这个分支可以执行,会把1发送到通道中,结束本次 for 循环;
  • 第二次 for 循环时,i = 2,由于通道缓冲区已满,所以ch <- i这个分支不满足,而x := <-ch这个分支可以执行,从通道接收值1并赋值给变量 x ,所以会在终端打印出 1;
  • 后续的 for 循环以此类推会依次打印出3、5、7、9

简单而言就是,当i为偶数的时候,执行的通道里输出。

通道错误示例

示例1

// demo1 通道误用导致的bug
func demo1() {wg := sync.WaitGroup{}ch := make(chan int, 10)for i := 0; i < 10; i++ {ch <- i}close(ch)wg.Add(3)for j := 0; j < 3; j++ {go func() {for {task := <-ch// 这里假设对接收的数据执行某些操作fmt.Println(task)}wg.Done()}()}wg.Wait()
}

匿名函数所在的 goroutine 并不会按照预期在通道被关闭后退出。

因为task := <- ch的接收操作在通道被关闭后会一直接收到零值,而不会退出。此处的接收操作应该使用task, ok := <- ch ,通过判断布尔值ok为假时退出;或者使用select 来处理通道。
修改后:

for j := 0; j < 3; j++ {go func() {for {task, ok := <-chfmt.Println(task)if !ok {break}}wait.Done()}()}wait.Wait()

其实不需要嵌套外循环的。不过为了方便观看就这样也了。

// demo2 通道误用导致的bug
func demo2() {ch := make(chan string)go func() {// 这里假设执行一些耗时的操作time.Sleep(3 * time.Second)ch <- "job result"}()select {case result := <-ch:fmt.Println(result)case <-time.After(time.Second): // 设置的超时时间return}
}

分析代码可以知道,此时有两个goroutine ,主方法走select,而另一个 goroutine 会走给通道输入值的操作。此时就有一个问题。从协程goroutine会等待三秒,而主协程,指挥等待一秒,然后按照超时操作弹出。

而这种问题的存在不是因为我们没有达到想要的结果,而是可能导致 goroutine 泄露(goroutine 并未按预期退出并销毁)

由于 select 命中了超时逻辑,导致通道没有消费者(无接收操作),而其定义的通道为无缓冲通道,因此 goroutine 中的ch <- "job result"操作会一直阻塞,最终导致 goroutine 泄露。


上一章漏下的内容讲完


并发安全和锁

场景:可能会存在多个 goroutine 同时操作一个资源(临界区)的情况,这种情况下就会发生竞态问题(数据竞态)

问题描述

package mainimport ("fmt""sync"
)var (x int64wg sync.WaitGroup // 等待组
)// add 对全局变量x执行5000次加1操作
func add() {for i := 0; i < 5000; i++ {x = x + 1}wg.Done()
}func main() {wg.Add(2)go add()go add()wg.Wait()fmt.Println(x)
}

每次执行都会生成不同的结果
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
原因:
我们开启了两个 goroutine 分别执行 add 函数,这两个 goroutine 在访问和修改全局的x变量时就会存在数据竞争,某个 goroutine 中对全局变量x的修改可能会覆盖掉另一个 goroutine 中的操作,所以导致最后的结果与预期不符

互斥锁

互斥锁是一种常用的控制共享资源访问的方法,它能够保证同一时间只有一个 goroutine 可以访问共享资源。Go 语言中使用sync包中提供的Mutex类型来实现互斥锁。

sync.Mutex提供了两个方法:

方法名功能
func (m *Mutex) Lock()获取互斥锁
func (m *Mutex) Unlock()释放互斥锁

通过锁修改

package mainimport ("fmt""sync"
)// sync.Mutexvar (x int64wg sync.WaitGroup // 等待组m sync.Mutex // 互斥锁
)// add 对全局变量x执行5000次加1操作
func add() {for i := 0; i < 5000; i++ {m.Lock() // 修改x前加锁x = x + 1m.Unlock() // 改完解锁}wg.Done()
}func main() {wg.Add(2)go add()go add()wg.Wait()fmt.Println(x)
}

此时就会达到我们的预期结果。

使用互斥锁能够保证同一时间有且只有一个 goroutine 进入临界区,其他的 goroutine 则在等待锁;当互斥锁释放后,等待的 goroutine 才可以获取锁进入临界区,多个 goroutine 同时等待一个锁时,唤醒的策略是随机的

读写互斥锁

互斥锁是完全互斥的,但是实际上有很多场景是读多写少的。

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

读写锁在 Go 语言中使用sync包中的RWMutex类型

方法名功能
func (rw *RWMutex) Lock()获取写锁
func (rw *RWMutex) Unlock()释放写锁
func (rw *RWMutex) RLock()获取读锁
func (rw *RWMutex) RUnlock()释放读锁
func (rw *RWMutex) RLocker() Locker返回一个实现Locker接口的读写锁

读写锁分为两种:读锁写锁

  • 当一个 goroutine 获取到读锁之后,其他的 goroutine 如果是获取读锁会继续获得锁,如果是获取写锁就会等待。
  • 当一个 goroutine 获取写锁之后,其他的 goroutine 无论是获取读锁还是写锁都会等待。
package mainimport ("fmt""sync""time"
)var (x       int64wg      sync.WaitGroupmutex   sync.MutexrwMutex sync.RWMutex
)// writeWithLock 使用互斥锁的写操作
func writeWithLock() {mutex.Lock() // 加互斥锁x = x + 1time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒mutex.Unlock()                    // 解互斥锁wg.Done()
}// readWithLock 使用互斥锁的读操作
func readWithLock() {mutex.Lock()                 // 加互斥锁time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒mutex.Unlock()               // 释放互斥锁wg.Done()
}// writeWithLock 使用读写互斥锁的写操作
func writeWithRWLock() {rwMutex.Lock() // 加写锁x = x + 1time.Sleep(10 * time.Millisecond) // 假设读操作耗时10毫秒rwMutex.Unlock()                  // 释放写锁wg.Done()
}// readWithRWLock 使用读写互斥锁的读操作
func readWithRWLock() {rwMutex.RLock()              // 加读锁time.Sleep(time.Millisecond) // 假设读操作耗时1毫秒rwMutex.RUnlock()            // 释放读锁wg.Done()
}func do(wf, rf func(), wc, rc int) {start := time.Now()// wc个并发写操作for i := 0; i < wc; i++ {wg.Add(1)go wf()}//  rc个并发读操作for i := 0; i < rc; i++ {wg.Add(1)go rf()}wg.Wait()cost := time.Since(start)fmt.Printf("x:%v cost:%v\n", x, cost)}
func main() {// 使用互斥锁,10并发写,1000并发读do(writeWithLock, readWithLock, 10, 1000) // x:10 cost:1.466500951s// 使用读写互斥锁,10并发写,1000并发读do(writeWithRWLock, readWithRWLock, 10, 1000) // x:10 cost:117.207592ms
}

从最终的执行结果可以看出,使用读写互斥锁在读多写少的场景下能够极大地提高程序的性能。

不过需要注意的是如果一个程序中的读操作和写操作数量级差别不大,那么读写互斥锁的优势就发挥不出来

有一点要注意在在这个实验中要明确一个已经知道的属性,那就是读操作,一定比写操作快。

在并发的时候说了,GO本体的标准库有个一个专门为并发实现的包sync

sync

sync.WaitGroup

在代码中生硬的使用time.Sleep肯定是不合适的,Go语言中可以使用sync.WaitGroup来实现并发任务同步在这里插入代码片

方法名功能
func (wg * WaitGroup) Add(delta int)计数器 +delta
func (wg *WaitGroup) Done()计数器 -1 (这个要搭配defer使用)
func (wg *WaitGroup) Wait()阻塞直到计数器变为 0

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加减少

当我们启动了 N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用 Done 方法将计数器减1。通过调用 Wait 等待并发任务执行完,当计数器值为 0 时,表示所有并发任务已经完成。几乎同时(人类看来)输出结果

需要注意:sync.WaitGroup是一个结构体,进行参数传递的时候要传递指针。## sync.Once

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

在某些场景下我们需要确保某些操作即使在高并发的场景下也只会被执行一次,例如只加载一次配置文件等。

Go语言中的sync包中提供了一个针对只执行一次场景的解决方案——sync.Oncesync.Once只有一个Do方法

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

注意:如果要执行的函数f需要传递参数就需要搭配闭包来使用。

加载配置文件示例

延迟一个开销很大的初始化操作到真正用到它的时候再执行是一个很好的实践。

因为预先初始化一个变量(比如在init函数中完成初始化)会增加程序的启动耗时,而且有可能实际执行过程中这个变量没有用上,那么这个初始化操作不是必须要做的。

var icons map[string]image.Imagefunc loadIcons() {icons = map[string]image.Image{"left":  loadIcon("left.png"),"up":    loadIcon("up.png"),"right": loadIcon("right.png"),"down":  loadIcon("down.png"),}
}func loadIcon(s string) image.Image {return nil
}// Icon 被多个goroutine调用时不是并发安全的
func Icon(name string) image.Image {if icons == nil {loadIcons()}return icons[name]
}

多个 goroutine 并发调用Icon函数时不是并发安全的,现代的编译器和CPU可能会在保证每个 goroutine 都满足串行一致的基础上自由地重排访问内存的顺序。(指令重排序)

loadIcons函数可能会被重排为以下结果:

func loadIcons() {icons = make(map[string]image.Image)icons["left"] = loadIcon("left.png")icons["up"] = loadIcon("up.png")icons["right"] = loadIcon("right.png")icons["down"] = loadIcon("down.png")
}

千万别看这个顺序和前面定义的一样。那是每个goroutine的出来的结果。并不一个得出的。

在这种情况下就会出现即使判断了icons不是nil也不意味着变量初始化完成了。考虑到这种情况,我们能想到的办法就是添加互斥锁,保证初始化icons的时候不会被其他的 goroutine 操作,但是这样做又会引发性能问题。

所以此时就考虑:sync.Once

import ("image""sync"
)var icons map[string]image.Imagevar loadIconsOnce sync.Oncefunc loadIcons() {icons = map[string]image.Image{"left":  loadIcon("left.png"),"up":    loadIcon("up.png"),"right": loadIcon("right.png"),"down":  loadIcon("down.png"),}
}func loadIcon(s string) image.Image {return nil
}// Icon 是并发安全的
func Icon(name string) image.Image {loadIconsOnce.Do(loadIcons)return icons[name]
}
func main() {}

并发安全的单例模式

package singletonimport ("sync"
)type singleton struct {}var instance *singleton
var once sync.Oncefunc GetInstance() *singleton {once.Do(func() {instance = &singleton{}})return instance
}

sync.Once其实内部包含一个互斥锁一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记录初始化是否完成

这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次

sync.Map

sync.Map

Go 语言中内置的 map 不是并发安全的.

错误例子

package mainimport ("fmt""strconv""sync"
)var m = make(map[string]int)func get(key string) int {return m[key]
}func set(key string, value int) {m[key] = value
}func main() {wg := sync.WaitGroup{}for i := 0; i < 10; i++ {wg.Add(1)go func(n int) {key := strconv.Itoa(n)set(key, n)fmt.Printf("k=:%v,v:=%v\n", key, get(key))wg.Done()}(i)}wg.Wait()
}

在这里插入图片描述
将上面的代码编译后执行,会报出fatal error: concurrent map writes错误。我们不能在多个 goroutine 中并发对内置的 map 进行读写操作,否则会存在数据竞争问题。其实大家,自己运行一下就知道了。其实不一定会出现这个错误,但是有概率出现。所以能加锁就枷锁。

这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版 map——sync.Map

开箱即用表示其不用像内置的 map 一样使用 make 函数初始化就能直接使用。

方法名功能
func (m *Map) Store(key, value interface{})存储key-value数据
func (m *Map) Load(key interface{}) (value interface{}, ok bool)查询key对应的value
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)查询或存储key对应的value
func (m *Map) LoadAndDelete(key interface{}) (value interface{}, loaded bool)查询并删除key
func (m *Map) Delete(key interface{})删除key
func (m *Map) Range(f func(key, value interface{}) bool)对map中的每个key-value依次调用f
package mainimport ("fmt""strconv""sync"
)// 并发安全的map
var m = sync.Map{}func main() {wg := sync.WaitGroup{}// 对m执行20个并发的读写操作for i := 0; i < 20; i++ {wg.Add(1)go func(n int) {key := strconv.Itoa(n)m.Store(key, n)         // 存储key-valuevalue, _ := m.Load(key) // 根据key取值fmt.Printf("k=:%v,v:=%v\n", key, value)wg.Done()}(i)}wg.Wait()
}

在这里插入图片描述
此时就安全了

说到枷锁操作,就不得不说一个东西。原子性操作

原子操作

针对整数数据类型(int32、uint32、int64、uint64)我们还可以使用原子操作来保证并发安全,通常直接使用原子操作比使用锁操作效率更高。

Go语言中原子操作由内置的标准库sync/atomic提供。(具体需要的话可以去看相关文档)

读取操作

func LoadInt32(addr *int32) (val int32)
func LoadInt64(addr *int64) (val int64)
func LoadUint32(addr *uint32) (val uint32)
func LoadUint64(addr *uint64) (val uint64)
func LoadUintptr(addr *uintptr) (val uintptr)
func LoadPointer(addr *unsafe.Pointer) (val unsafe.Pointer)

写入操作

func StoreInt32(addr *int32, val int32)
func StoreInt64(addr *int64, val int64)
func StoreUint32(addr *uint32, val uint32)
func StoreUint64(addr *uint64, val uint64)
func StoreUintptr(addr *uintptr, val uintptr)
func StorePointer(addr *unsafe.Pointer, val unsafe.Pointer)

修改操作

func AddInt32(addr *int32, delta int32) (new int32)
func AddInt64(addr *int64, delta int64) (new int64)
func AddUint32(addr *uint32, delta uint32) (new uint32)
func AddUint64(addr *uint64, delta uint64) (new uint64)
func AddUintptr(addr *uintptr, delta uintptr) (new uintptr)

交换操作

func SwapInt32(addr *int32, new int32) (old int32)
func SwapInt64(addr *int64, new int64) (old int64)
func SwapUint32(addr *uint32, new uint32) (old uint32)
func SwapUint64(addr *uint64, new uint64) (old uint64)
func SwapUintptr(addr *uintptr, new uintptr) (old uintptr)
func SwapPointer(addr *unsafe.Pointer, new unsafe.Pointer) (old unsafe.Pointer)

比较并交换操作

func CompareAndSwapInt32(addr *int32, old, new int32) (swapped bool)
func CompareAndSwapInt64(addr *int64, old, new int64) (swapped bool)
func CompareAndSwapUint32(addr *uint32, old, new uint32) (swapped bool)
func CompareAndSwapUint64(addr *uint64, old, new uint64) (swapped bool)
func CompareAndSwapUintptr(addr *uintptr, old, new uintptr) (swapped bool)
func CompareAndSwapPointer(addr *unsafe.Pointer, old, new unsafe.Pointer) (swapped bool)

atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用

除了某些特殊的底层应用,使用通道或者 sync 包的函数/类型实现同步更好。

例子

package mainimport ("fmt""sync""sync/atomic""time"
)type Counter interface {Inc()Load() int64
}// 普通版
type CommonCounter struct {counter int64
}func (c CommonCounter) Inc() {c.counter++
}func (c CommonCounter) Load() int64 {return c.counter
}// 互斥锁版
type MutexCounter struct {counter int64lock    sync.Mutex
}func (m *MutexCounter) Inc() {m.lock.Lock()defer m.lock.Unlock()m.counter++
}func (m *MutexCounter) Load() int64 {m.lock.Lock()defer m.lock.Unlock()return m.counter
}// 原子操作版
type AtomicCounter struct {counter int64
}func (a *AtomicCounter) Inc() {atomic.AddInt64(&a.counter, 1)
}func (a *AtomicCounter) Load() int64 {return atomic.LoadInt64(&a.counter)
}func test(c Counter) {var wg sync.WaitGroupstart := time.Now()for i := 0; i < 1000; i++ {wg.Add(1)go func() {c.Inc()wg.Done()}()}wg.Wait()end := time.Now()fmt.Println(c.Load(), end.Sub(start))
}func main() {c1 := CommonCounter{} // 非并发安全test(c1)c2 := MutexCounter{} // 使用互斥锁实现并发安全test(&c2)c3 := AtomicCounter{} // 并发安全且比互斥锁效率更高test(&c3)
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/175642.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网站安全攻防战:守护数据的钢铁堡垒

在数字化时代&#xff0c;网站的安全性至关重要&#xff0c;因为它不仅关乎用户的隐私信息&#xff0c;还涉及到业务的正常运行。一旦网站受到攻击&#xff0c;可能导致数据泄露、服务中断等问题&#xff0c;因此网站安全应成为企业和个人关注的焦点。本文将探讨网站安全的重要…

前端AJAX入门到实战,学习前端框架前必会的(ajax+node.js+webpack+git)(四)

你可以的&#xff0c;去飞吧&#xff01; 同步代码和异步代码 回调函数地狱和 Promise 链式调用 回调函数地狱 缔造“回调地狱”↓ 制造里层回调错误&#xff0c;却在最外层接收错误→无法捕获 axios源码抛出异常&#xff08;未捕获&#xff09; <!DOCTYPE html> <ht…

万宾科技内涝积水监测仪效果,预警城市积水

当城市之中出现强降雨或者大暴雨&#xff0c;可能会导致雨水不断堆积到城市排水管网之中&#xff0c;可能还会淹没城市的排水系统时&#xff0c;这种现象被称为城市之中的内涝&#xff0c;并且在许多城市之中内涝问题日益引起人们的关注。 内涝积水监测仪的出现成为了希望的灯塔…

leetcode:876. 链表的中间结点

一、题目 函数原型&#xff1a; struct ListNode* middleNode(struct ListNode* head) 二、思路 要找到链表的中间结点&#xff0c;有两种思路&#xff1a; 暴力解法&#xff1a;先遍历一遍链表&#xff0c;计算出链表的长度&#xff0c;再次遍历链表&#xff0c;找到中间结点。…

Window MongoDB安装

三种NOSQL的一种,Redis MongoDB ES 应用场景: 1.社交场景:使用Mongodb存储用户信息,以及用户发表的朋友圈信息,通过地理位置索引实现附近的人,地点等功能 2.游戏场景:使用Mongodb存储游戏用户信息,用户的装备,积分等直接以内嵌文档的形式存储,方便查询,高效率存储和访问…

[工业自动化-20]:西门子S7-15xxx编程 - 软件编程 - 基本编程指令与梯形图基本元素:位逻辑指令、定时器指令、计数器指令、触发器指令

目录 一、PLC编程的基本指令 1.1 什么是PLC指令 1.2 PLC指令的分类 1.3 PLC指令与梯形图基本元素的关系 三、基本的位运算指令 四、边沿触发指令 4.1 什么是沿 4.2 沿的持续时间 4.3 使用场景 五、定时器指令 六、计数器指令 七、触发器指令 一、PLC编程的基本指令…

WPS数组

一、创建数组方法和数组的读取、修改、写入 数组是值的有序集合&#xff0c;其中的值叫作元素。每个元素有一个数值表示的位置&#xff0c;叫作索引&#xff0c;数组中的不同元素可以是不同数据类型。 function demo(){var arr1[99,"人","abc",[3,4,5]];…

CSS实现图片滑动对比

实现效果图如下&#xff1a; css代码&#xff1a; 知识点&#xff1a;resize: horizontal; 文档地址 <style>.image-slider {position: relative;display: inline-block;width: 500px;height: 300px;}.image-slider>div {position: absolute;top: 0;bottom: 0;left: …

react中间件的理解

一、是什么&#xff1f; 中间件&#xff08;Middleware&#xff09;在计算机中&#xff0c;是介于应用系统和系统软件之间的一类软件&#xff0c;它使用系统软件所提供的基础服务&#xff08;功能&#xff09;&#xff0c;衔接网络应用上的各个部分或不同的应用&#xff0c;能…

【python】Django——templates模板、静态文件、django模板语法、请求和响应

笔记为自我总结整理的学习笔记&#xff0c;若有错误欢迎指出哟~ 【Django专栏】 Django——django简介、django安装、创建项目、快速上手 Django——templates模板、静态文件、django模板语法、请求和响应 templates模板按app顺序寻找模板全局模板 静态文件jqueryBootstrap dja…

【Linux】gitee仓库的注册使用以及在Linux上远程把代码上传到gitee上的方法

君兮_的个人主页 即使走的再远&#xff0c;也勿忘启程时的初心 C/C 游戏开发 Hello,米娜桑们&#xff0c;这里是君兮_&#xff0c;今天为大家介绍一个在实际工作以及项目开发过程中非常实用的网站gitee&#xff0c;并教如何正确的使用这个网站以及常见问题的解决方案&#xf…

C语言--指针与数组--遍历数组的n种方式【详细】

一.一维数组名的含义 arr一般表示数组的其实地址&#xff08;除了两种例外&#xff09; 1.在定义数组的同一个函数中(不是形参),求sizeof(arr),求整个数组的字节数 2.在定义数组的同一个函数中(不是形参),&arr1,加整个数组的大小 (经常考试) 3.除上面以外,arr都表示数组的…