Go语言并发编程-同步和锁

同步和锁

概述

同步是并发编程的基本要素之一,我们通过channel可以完成多个goroutine间数据和信号的同步。

除了channel外,我们还可以使用go的官方同步包sync,sync/atomic 完成一些基础的同步功能。主要包含同步数据、锁、原子操作等。

一个同步失败的示例:

 func SyncErr() {wg := sync.WaitGroup{}// 计数器counter := 0// 多个goroutine并发的累加计数器gs := 100wg.Add(gs)for i := 0; i < gs; i++ {go func() {defer wg.Done()// 累加for k := 0; k < 100; k++ {counter++// ++ 操作不是原子的// counter = counter + 1// 1. 获取当前的counter变量// 2. +1// 3. 赋值新值到counter}}()}// 统计计数结果wg.Wait()fmt.Println("Counter:", counter)}

Lock解决方案:

 func SyncLock() {n := 0wg := sync.WaitGroup{}​lk := sync.Mutex{}for i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()for i := 0; i < 100; i++ {lk.Lock()n++lk.Unlock()}}()}wg.Wait()fmt.Println("n:", n)}​// runn: 100000

互斥锁Mutex的使用

sync包提供了两种锁:

  • 互斥锁,Mutex

  • 读写互斥锁,RWMutex

互斥锁,同一时刻只能有一个goroutine申请锁定成功,不区分读、写操作。也称为:独占锁、排它锁。

提供了如下方法完成锁操作:

 type Mutex// 锁定锁m, 若锁m已是锁定状态,调用的goroutine会被阻塞,直到可以锁定func (m *Mutex) Lock()// 解锁锁m,若m不是锁定状态,会导致运行时错误func (m *Mutex) Unlock()// 尝试是否可以加锁,返回是否成功func (m *Mutex) TryLock() bool

注意:锁与goroutine没有关联,意味着允许一个goroutine加锁,在另一个goroutine中解锁。但是不是最典型的用法。

典型的锁用法:

 var lck sync.Mutexfunc () {lck.Lock()// 互斥执行的代码defer lck.Unlock()}

锁的流程:

image.png

示例:

 func SyncMutex() {wg := sync.WaitGroup{}var lck sync.Mutexfor i := 0; i < 4; i++ {wg.Add(1)go func(n int) {defer wg.Done()fmt.Println("before lock: ", n)lck.Lock()fmt.Println("locked: ", n)time.Sleep(1 * time.Second)lck.Unlock()fmt.Println("after lock: ", n)}(i)}wg.Wait()}

某次输出结果:

 before lock:  3locked:  3   before lock:  2before lock:  1before lock:  0after lock:  3locked:  2after lock:  2locked:  1after lock:  1locked:  0after lock:  0

可以发现,before lock 都是先执行的,而Lock() 操作,必须要等到其他goroutineUnlock()才能成功。

注意,如果其他goroutine没有通过相同的锁(1没用锁,2用了其他锁)去操作资源,那么是不受锁限制的,例如:

 func SyncLockAndNo() {n := 0wg := sync.WaitGroup{}lk := sync.Mutex{}for i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()for i := 0; i < 100; i++ {lk.Lock()n++lk.Unlock()}}()}wg.Add(1)go func() {defer wg.Done()for i := 0; i < 10000; i++ {n++}}()​// 其他锁//var lk2 sync.Mutex//go func() {//    defer wg.Done()//    for i := 0; i < 10000; i++ {//        lk2.Lock()//        n++//        lk2.Unlock()//    }//}()​wg.Wait()fmt.Println("n:", n)}​// 其中一次结果n: 109876

我们在第一个counter的例子上,增加了一个goroutine同去累加计数器counter,但没有使用前面的Mutex(不使用或使其他锁)。可见,出现了资源争用的情况。因此要注意:如果要限制资源的并发争用,要全部的资源操作都使用同一个锁。

实操时,锁除了直接调用外,还经常性出现在结构体中,以某个字段的形式出现,用于包含struct字段不会被多gorutine同时修改,例如我们 cancelCtx:

 type cancelCtx struct {Context​mu       sync.Mutex            // protects following fieldsdone     atomic.Value          // of chan struct{}, created lazily, closed by first cancel callchildren map[canceler]struct{} // set to nil by the first cancel callerr      error                 // set to non-nil by the first cancel call}

我们通常也会这么做,示例:

 type Post struct {Subject string// 赞Likes   int// 操作锁定mu sync.Mutex}​func (p *Post) IncrLikes() *Post {p.mu.Lock()defer p.mu.Unlock()p.Likes++​return p}​func (p *Post) DecrLikes() *Post {p.mu.Lock()defer p.mu.Unlock()p.Likes--​return p}

读写RWMutex的使用

读写互斥锁,将锁操作类型做了区分,分为读锁和写锁,由sync.RWMutex类型实现:

  • 读锁,Read Lock,共享读,阻塞写

  • 写锁,Lock,独占操作,阻塞读写

并发
支持不支持
不支持不支持

之所以减小锁的粒度,因为实际操作中读操作的比例要远高于写操作的比例,增加了共享读操作锁后,可以更大程度的提升读的并发能力。

sync.RWMutex 提供了如下方法完成操作:

 type RWMutex// 写锁定func (rw *RWMutex) Lock()// 写解锁func (rw *RWMutex) Unlock()​// 读锁定func (rw *RWMutex) RLock()// 读解锁func (rw *RWMutex) RUnlock()​// 尝试加写锁定func (rw *RWMutex) TryLock() bool// 尝试加读锁定func (rw *RWMutex) TryRLock() bool

写锁定,与互斥锁Mutex的语法和操作结果一致,都是保证互斥的独占操作。

读锁定,可以在已经存在读锁的情况下,加锁成功。

如图所示:

image.png

读锁示例:

 func SyncRLock() {wg := sync.WaitGroup{}// 模拟多个goroutinevar rwlck sync.RWMutexfor i := 0; i < 10; i++ {wg.Add(1)go func() {defer wg.Done()////rwlck.Lock()rwlck.RLock()// 输出一段内容fmt.Println(time.Now())time.Sleep(1 * time.Second)////rwlck.Unlock()rwlck.RUnlock()}()}​wg.Add(1)go func() {defer wg.Done()//rwlck.Lock()//rwlck.RLock()// 输出一段内容fmt.Println(time.Now(), "Lock")time.Sleep(1 * time.Second)//rwlck.Unlock()//rwlck.RUnlock()}()​wg.Wait()}

其中,使用读锁,输出操作会全部立即执行,然后集体sleep1s后全部结束。使用写锁,输出和Sleep会间隔1s依次执行。

实操示例:

 type Article struct {Subject string// 赞likes int// 操作锁定mu sync.RWMutex}​func (a Article) Likes() int {a.mu.RLock()defer a.mu.RUnlock()return a.likes}​func (a *Article) IncrLikes() *Article {a.mu.Lock()defer a.mu.Unlock()a.likes++return a}

同步Map sync.Map

Go中Map是非线程(goroutine)安全的。并发操作 Map 类型时,会导致 fatal error: concurrent map read and map write错误:

 func SyncMapErr() {m := map[string]int{}// 并发map写go func() {for {m["key"] = 0}}()// 并发map读go func() {for {_ = m["key"]}}()// 阻塞select {}}

之所以Go不支持Map的并发安全,是因为Go认为Map的典型使用场景不需要在多个Goroutine间并发安全操作Map。

并发安全操作Map的方案:

  • 锁 + Map,自定义Map操作,增加锁的控制,可以选择 Mutex和RWMutex。

  • sync.Map,sync包提供的安全Map.

锁+Map示例,在结构体内嵌入sync.Mutex:

 func SyncMapLock() {myMap := struct {sync.RWMutexData map[string]int}{Data: map[string]int{},}​// writemyMap.Lock()myMap.Data["key"] = 0myMap.Unlock()​// readmyMap.RLock()_ = myMap.Data["key"]myMap.RUnlock()}

sync.Map 的使用

 type Map// 最常用的4个方法:// 存储func (m *Map) Store(key, value any)// 遍历 mapfunc (m *Map) Range(f func(key, value any) bool)// 删除某个key元素func (m *Map) Delete(key any)// 返回key的值。存在key,返回value,true,不存在返回 nil, falsefunc (m *Map) Load(key any) (value any, ok bool)​// 若m[key]==old,执行删除。key不存在,返回falsefunc (m *Map) CompareAndDelete(key, old any) (deleted bool)// 若m[key]==old,执行交换, m[key] = newfunc (m *Map) CompareAndSwap(key, old, new any) bool​// 返回值后删除元素。loaded 表示是否load成功,key不存在,loaded为falsefunc (m *Map) LoadAndDelete(key any) (value any, loaded bool)// 加载,若加载失败则存储。返回加载或存储的值和是否加载func (m *Map) LoadOrStore(key, value any) (actual any, loaded bool)​// 存储新值,返回之前的值。loaded表示key是否存在func (m *Map) Swap(key, value any) (previous any, loaded bool)

sync.Map 不需要类型初始化,即可使用,可以理解为map[comparable]any。

使用示例,不会触发 fatal error: concurrent map read and map write

 func SyncSyncMap() {var m sync.Mapgo func() {for {m.Store("key", 0)}}()go func() {for {_, _ = m.Load("key")}}()select {}}

使用示例:

 func SyncSyncMapMethod() {wg := sync.WaitGroup{}var m sync.Mapfor i := 0; i < 10; i++ {wg.Add(1)go func(n int) {defer wg.Done()m.Store(n, fmt.Sprintf("value: %d", n))}(i)}​for i := 0; i < 10; i++ {wg.Add(1)go func(n int) {defer wg.Done()fmt.Println(m.Load(n))}(i)}​wg.Wait()m.Range(func(key, value any) bool {fmt.Println(key, value)return true})​//m.Delete(4)}

并发安全操作Map的方案的选择,统计的压测数据显示,相对而言:

  • 锁 + Map,写快,读慢

  • sync.Map,读快,写慢,删快,适合读多写少的场景

原子操作 sync/atomic

原子操作即是进行过程中不能被中断的操作,针对某个值的原子操作在被进行的过程中,CPU绝不会再去进行其他的针对该值的操作。为了实现这样的严谨性,原子操作仅会由一个独立的CPU指令代表和完成。原子操作是无锁的,常常直接通过CPU指令直接实现。 事实上,其它同步技术的实现常常依赖于原子操作。

原子操作是CPU指令级别实现的,比如在Intel的CPU上主要是使用总线锁的方式,AMD的CPU架构机器上就是使用MESI一致性协议的方式来保证原子操作。

go中 sync/atomic 包提供了原子操作的支持,用于同步操作整型(和指针类型):

  • int32

  • int64

  • uint32

  • uint64

  • uintptr

  • unsafe.Pointer

针对于以上类型,提供了如下操作:

 // Type 是以上的类型之一// 比较相等后交换 CASfunc CompareAndSwapType(addr *Type, old, new Type) (swapped bool)// 交换func SwapType(addr *Type, new Type) (old Type)// 累加func AddType(addr *Type, delta Type) (new Type)// 获取func LoadType(addr *Type) (val Type)// 存储func StoreType(addr *Type, val Type)

除了以上函数,还提供了对应的类型方法操作,以Int32为例:

 type Int32func (x *Int32) Add(delta int32) (new int32)func (x *Int32) CompareAndSwap(old, new int32) (swapped bool)func (x *Int32) Load() int32func (x *Int32) Store(val int32)func (x *Int32) Swap(new int32) (old int32)

除了以上几个整型,bool类型也提供了类型上的原子操作:

 type Boolfunc (x *Bool) CompareAndSwap(old, new bool) (swapped bool)func (x *Bool) Load() boolfunc (x *Bool) Store(val bool)func (x *Bool) Swap(new bool) (old bool)

示例:

 func SyncAtomicAdd() {// 并发的过程,没有加锁,Lock//var counter int32 = 0// type// atomic 原子的Int32, counter := 0counter := atomic.Int32{}wg := sync.WaitGroup{}for i := 0; i < 1000; i++ {wg.Add(1)go func() {defer wg.Done()for i := 0; i < 100; i++ {//atomic.AddInt32(&counter, 1)// type// 原子累加操作 , counter ++counter.Add(1)}}()}wg.Wait()//fmt.Println("counter:", atomic.LoadInt32(&counter))// typefmt.Println("counter:", counter.Load())}

以上示例不会出现不到10000的情况了。

除了预定义的整型的支持,还可以使用 atomic.Value 类型,完成其他类型的原子操作:

 type Valuefunc (v *Value) CompareAndSwap(old, new any) (swapped bool)func (v *Value) Load() (val any)func (v *Value) Store(val any)func (v *Value) Swap(new any) (old any)

使用方法:

 func SyncAtomicValue() {​var loadConfig = func() map[string]string {return map[string]string{// some config"title":   "马士兵Go并发编程","varConf": fmt.Sprintf("%d", rand.Int63()),}}​var config atomic.Value​// 每N秒加载一次配置文件go func() {for {config.Store(loadConfig())fmt.Println("latest config was loaded", time.Now().Format("15:04:05.99999999"))time.Sleep(time.Second)}}()​// 使用配置// 不能在加载的过程中使用配置for {go func() {c := config.Load()fmt.Println(c, time.Now().Format("15:04:05.99999999"))}()​time.Sleep(400 * time.Millisecond)}​select {}}

sync.Pool 并发安全池

image.png

池是一组可以单独保存和检索的可以复用的临时对象。存储在池中的任何项目可随时自动删除,无需通知。一个池可以安全地同时被多个goroutine使用。

典型特征:

  • sync.Pool 是并发安全的

  • 池中的对象由Go负责删除,内存由Go自己回收

  • 池中元素的数量由Go负责管理,用户无法干预

  • 池中元素应该是临时的,不应该是持久的。例如长连接不适合放入 sync.Pool 中

池的目的是缓存已分配但未使用的项目以供以后重用,从而减轻垃圾收集器的压力。也就是说,它使构建高效、线程安全的自由元素变得容易。

池的一个适当用途是管理一组临时项,这些临时项在包的并发独立客户端之间默默共享,并可能被其重用。池提供了一种在许多客户机上分摊分配开销的方法。

一个很好地使用池的例子是fmt包,它维护了临时输出缓冲区的动态大小存储。

池由 sync.Pool类型实现,具体三个操作:

  • 初始化Pool实例,需要提供池中缓存元素的New方法。

  • 申请元素,func (p *Pool) Get() any

  • 交回对象,func (p *Pool) Put(x any)

操作示例:

 func SyncPool() {// 原子的计数器var counter int32 = 0​// 定义元素的Newer,创建器elementNewer := func() any {// 原子的计数器累加atomic.AddInt32(&counter, 1)​// 池中元素推荐(强烈)是指针类型return new(bytes.Buffer)}​// Pool的初始化pool := sync.Pool{New: elementNewer,}​// 并发的申请和交回元素workerNum := 1024 * 1024wg := sync.WaitGroup{}wg.Add(workerNum)for i := 0; i < workerNum; i++ {go func() {defer wg.Done()// 申请元素,通常需要断言为特定类型buffer := pool.Get().(*bytes.Buffer)// 不用Pool//buffer := elementNewer().(*bytes.Buffer)// 交回元素defer pool.Put(buffer)// 使用元素_ = buffer.String()}()}​//wg.Wait()​// 测试创建元素的次数fmt.Println("elements number is :", counter)}​// elements number is : 12

测试的时候,大家可以发现创建的元素数量远远低于goroutine的数量。

DATA RACE 现象

当程序运行时,由于并发的原因会导致数据竞争使用,有时在编写代码时很难发现,要经过大量测试才会发现。可以用 go run -race ,增加-race选项,检测运行时可能出现的竞争问题。

测试之前的计数器累加代码:本例子需要 main.main 来演示,因为是 go run:

package mainimport ("fmt""sync"
)func main() {wg := sync.WaitGroup{}// 计数器counter := 0// 多个goroutine并发的累加计数器gs := 1000wg.Add(gs)for i := 0; i < gs; i++ {go func() {defer wg.Done()// 累加for k := 0; k < 100; k++ {counter++// ++ 操作不是原子的// counter = counter + 1// 1. 获取当前的counter变量// 2. +1// 3. 赋值新值到counter}}()}// 统计计数结果wg.Wait()fmt.Println("Counter:", counter)
}

结果:

# 没有使用 -race
PS D:\apps\goExample\concurrency> go run .\syncRace.go
n: 94077# 使用 -race
PS D:\apps\goExample\concurrency> go run -race .\syncRace.go
==================
WARNING: DATA RACE  
Read at 0x00c00000e0f8 by goroutine 9:main.main.func1()D:/apps/goExample/concurrency/syncMain.go:16 +0xa8Previous write at 0x00c00000e0f8 by goroutine 7:
Goroutine 9 (running) created at:main.main()D:/apps/goExample/concurrency/syncMain.go:13 +0x84Goroutine 7 (finished) created at:main.main()D:/apps/goExample/concurrency/syncMain.go:13 +0x84
==================
n: 98807
Found 1 data race(s)
exit status 66

该选项用于在开发阶段,检测数据竞争情况。

出现 data race情况,可以使用锁,或原子操作的来解决。

sync.Once

若需要保证多个并发goroutine中,某段代码仅仅执行一次,就可以使用 sync.Once 结构实现。

例如,在获取配置的时候,往往仅仅需要获取一次,然后去使用。在多个goroutine并发时,要保证能够获取到配置,同时仅获取一次配置,就可以使用sync.Once结构:

func SyncOnce() {// 初始化config变量config := make(map[string]string)// 1. 初始化 sync.Onceonce := sync.Once{}// 加载配置的函数loadConfig := func() {// 2. 利用 once.Do() 来执行once.Do(func() {// 保证执行一次config = map[string]string{"varInt": fmt.Sprintf("%d", rand.Int31()),}fmt.Println("config loaded")})}// 模拟多个goroutine,多次调用加载配置// 测试加载配置操作,执行了几次workers := 10wg := sync.WaitGroup{}wg.Add(workers)for i := 0; i < workers; i++ {go func() {defer wg.Done()// 并发的多次加载配置loadConfig()// 使用配置_ = config}()}wg.Wait()
}

核心逻辑:

  1. 初始化 sync.Once

  2. once.Do(func()) 可以确保func()仅仅执行一次

sync.Once 的实现很简单:

type Once struct {// 是否已处理,保证一次done uint32// 锁,保证并发安全m    Mutex
}

sync.Cond

sync.Cond是sync包提供的基于条件(Condition)的通知结构。

该结构提供了4个方法:

// 创建Cond
func NewCond(l Locker) *Cond
// 全部唤醒
func (c *Cond) Broadcast()
// 唤醒1个
func (c *Cond) Signal()
// 等待唤醒
func (c *Cond) Wait()

其中,创建时,需要1个Locker作为参数,通常是 sync.Mutext或sync.RWMutex。然后两个方法用来通知,一个方法用来等待。

使用逻辑很简单,通常是一个goroutine负责通知,多个goroutine等待处理,如图:

image.png

创建Cond,sync.NewCond() 需要提供锁,同时在等待操作和广播(信号)操作中,通常需要先申请锁,其中等待操作是必须的,而官博(信号)操作是可选的。,例如:

cond := sync.NewCond(&sync.Mutex{})
cond := sync.NewCond(&sync.RWMutex{})

还有,cond的广播和信号通知操作是并发安全的,可以重复调用的。

要注意Wait()操作,是会先解锁,等到广播信号后,再加锁。因此,Wait()操作前,要加锁。

示例代码:

  • 一个goroutine负责接收数据,完毕后,广播给处理数据的goroutine

  • 多个goroutine处理数据,在数据未处理完前,等待广播信号。信号来了,处理数据

func SyncCond() {wg := sync.WaitGroup{}dataCap := 1024 * 1024var data []intcond := sync.NewCond(&sync.Mutex{})for i := 0; i < 8; i++ {wg.Add(1)go func(c *sync.Cond) {defer wg.Done()c.L.Lock()for len(data) < dataCap {c.Wait()}fmt.Println("listen", len(data), time.Now())c.L.Unlock()}(cond)}wg.Add(1)go func(c *sync.Cond) {defer wg.Done()c.L.Lock()defer c.L.Unlock()for i := 0; i < dataCap; i++ {data = append(data, i*i)}fmt.Println("Broadcast")c.Broadcast()//c.Signal()}(cond)// 为什么 for { wait() }// 另外的广播goroutine//wg.Add(1)//go func(c *sync.Cond) {//    defer wg.Done()//    c.Broadcast()//}(cond)wg.Wait()
}

示例代码要点:

  • wait所在的goroutine要判定是否需要wait,所以wait要出现在条件中,因为goroutine调用的关系,不能保证wait在broadcast前面执行

  • wait要使用for进行条件判定,因为在wait返回后,条件不一定成立。因为Broadcast()操作可能被提前调用(通常是在其他的goroutine中。

  • Broadcast() 操作可选的是否加锁解锁

  • Wait() 操作前,一定要加锁。因为Wait()操作,会先解锁,接收到信号后,再加锁。

sync.Cond 基本原理

sync.Cond结构:

type Cond struct {// 锁L Locker// 等待通知goroutine列表notify  notifyList// 限制不能被拷贝noCopy noCopychecker copyChecker
}

结构上可见,Cond记录了等待的goroutine列表,这样就可以做到,广播到全部的等待goroutine。这也是Cond应该被复制的原因,否则这些goroutine可能会被意外唤醒。

Wait() 操作:

func (c *Cond) Wait() {// 检查是否被复制c.checker.check()// 更新 notifyList 中需要等待的 waiter 的数量// 返回当前需要插入 notifyList 的编号t := runtime_notifyListAdd(&c.notify)// 解锁c.L.Unlock()// 挂起,直到被唤醒runtime_notifyListWait(&c.notify, t)// 唤醒之后,重新加锁。// 因为阻塞之前解锁了。c.L.Lock()
}

核心工作就是,记录当goroutine到Cond的notifyList。之后解锁,挂起,加锁。因此要在Wait()前加锁,后边要解锁。

Broadcast()操作:

func (c *Cond) Broadcast() {// 检查 sync.Cond 是否被复制了c.checker.check()// 唤醒 notifyList 中的所有 goroutineruntime_notifyListNotifyAll(&c.notify)
}

核心工作就是唤醒 notifyList 中全部的 goroutine。

小结

同步类型:

  • 数据同步,保证数据操作的原子性

    • sync/atomic

    • sync.Map

    • sync.Mutex, sync.RWMutex

  • 操作同步

    • sync.Mutex, sync.RWMutex

锁的类型:

  • 互斥锁 sync.Mutex,完全独占

  • 读写互斥锁 sync.RWMutex,可以共享读操作

锁的不锁资源,只是锁定申请锁本身的操作。

sync包总结

  • 锁:sync.Mutex, sync.RWMutex

  • 数据:sync.Map, sync/atomic

  • sync.Pool

  • sync.Once

  • sync.Cond

使用Channel完成数据和信号的同步!

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

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

相关文章

13. C++继承 | 详解 | 虚拟继承及底层实现

目录 1.定义 1.1继承的概念 1.2 继承的定义 2. 对象赋值转换 3. 继承中的作用域 a. 隐藏/重定义 (Hiding/Redefinition) b. 重载 (Overloading) c. 重写/覆盖 (Overriding) d. 编译报错 (Compilation Error) 4. 派生类的默认成员函数 构造 拷贝构造 运算符重载 析…

win11将bat文件固定到“开始“屏幕

一、为bat文件创建快捷方式 (假设bat文件的全名为运行脚本.bat) 右键bat文件&#xff0c;点击显示更多选项 右键菜单选择发送到(N)-桌面快捷方式 二、获取快捷方式的路径 返回桌面&#xff0c;选中创建好的快捷方式&#xff0c;按AltEnter&#xff0c;切换到安全选项卡 鼠…

JCR一区级 | Matlab实现PSO-Transformer-LSTM多变量回归预测

JCR一区级 | Matlab实现PSO-Transformer-LSTM多变量回归预测 目录 JCR一区级 | Matlab实现PSO-Transformer-LSTM多变量回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现PSO-Transformer-LSTM多变量回归预测&#xff0c;粒子群优化Transformer结合LST…

Nginx的核心功能

1. Nginx的核心功能 1.1 nginx反向代理功能 正向代理 代理的为客户端&#xff0c;对于服务器不知道真实客户的信息。例如&#xff1a;翻墙软件 反向代理服务器 代理的为服务器端。对于客户来说不知道服务器的信息。例如&#xff1a;nginx 项目部署图 web项目部署的虚拟机和Ng…

鸿蒙语言基础类库:【@system.notification (通知消息)】

通知消息 说明&#xff1a; 从API Version 7 开始&#xff0c;该接口不再维护&#xff0c;推荐使用新接口[ohos.notification]。本模块首批接口从API version 3开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 import notification fro…

httpx 的使用

httpx 是一个可以支持 HTTP/2.0 的库 还有一个是&#xff1a; hyper 库 这里有一个由HTTP/2.0的网站&#xff1a; https://spa16.scrape.center/ 使用 requests 库 进行爬取 import requests url https://spa16.scrape.center/ response requests.get(url) print(response…

达梦数据库的系统视图v$arch_file

达梦数据库的系统视图v$arch_file 在达梦数据库中&#xff0c;V$ARCH_FILE 是一个动态性能视图&#xff0c;用于显示当前数据库的归档日志文件信息。这个视图可以帮助数据库管理员监控和管理归档日志文件&#xff0c;确保数据库的备份和恢复过程顺利进行。 查询本地归档日志信…

Unity UGUI Image Maskable

在Unity的UGUI系统中&#xff0c;Maskable属性用于控制UI元素是否受到父级遮罩组件的影响。以下是关于这个属性的详细说明和如何使用&#xff1a; Maskable属性 Maskable属性&#xff1a; 当你在GameObject上添加一个Image组件&#xff08;比如UI面板或按钮&#xff09;时&…

ctfshow-web入门-php特性(web127-web131)

目录 1、web127 2、web128 3、web129 4、web130 5、web131 1、web127 代码审计&#xff1a; $ctf_show md5($flag); 将 $flag 变量进行 MD5 哈希运算&#xff0c;并将结果赋值给 $ctf_show。 $url $_SERVER[QUERY_STRING]; 获取当前请求的查询字符串&#xff08;que…

开源防病毒工具--ClamAV

产品文档&#xff1a;简介 - ClamAV 文档 开源地址&#xff1a;Cisco-Talos/clamav&#xff1a;ClamAV - 文档在这里&#xff1a;https://docs.clamav.net (github.com) 一、引言 ClamAV&#xff08;Clam AntiVirus&#xff09;是一个开源的防病毒工具&#xff0c;广泛应用…

【算法专题】归并排序

目录 1. 排序数组 2. 交易逆序对的总数 3. 计算右侧小于当前元素的个数 4. 翻转对 总结 1. 排序数组 912. 排序数组 - 力扣&#xff08;LeetCode&#xff09; 今天我们使用归并排序来对数组进行排序&#xff0c;实际上&#xff0c;归并排序和快速排序是有一定相似之处的&a…

Android View的绘制流程

1.不管是View的添加&#xff0c;还是调用View的刷新方法invalidate()或者requestLayout()&#xff0c;绘制都是从ViewRootImpl的scheduleTraversals()方法开始 void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled true;mTraversalBarrier mHandler…

Linux中nohup(no hang up)不挂起,用于在系统后台不挂断地运行命令,即使退出终端也不会影响程序的运行。

nohup的英文全称是 no hang up&#xff0c;即“不挂起”。这个命令在Linux或Unix系统中非常有用&#xff0c;主要用于在系统后台不挂断地运行命令&#xff0c;即使退出终端也不会影响程序的运行。默认情况下&#xff08;非重定向时&#xff09;&#xff0c;nohup会将输出写入一…

C++之类与对象(1)

目录 前言 1.类的定义 1.1类定义的格式 1.2访问限定符 1.3类域 1.3.1类定义一个作用域 1.3.2类成员在类的作用域中 1.3.3在类体外定义成员 2.实例化 2.1实例化概念 2.2对象大小 3.this指针 4.选择题补充练习 结束语 前言 Hello&#xff0c;友友们&#xff0c;好久…

Linux安装mysql(超详细版)

步骤1&#xff1a;新建一个文件夹&#xff0c;专放从网络下载的文件 [rootiZ2zeh6vyxsq620zifz8jaZ home]#mkdir soft #在根目录下创建也可以 步骤2&#xff1a;切换目录&#xff0c;进入soft文件中 [rootiZ2zeh6vyxsq620zifz8jaZ /]# cd home/ #若第一步文件建在根目…

牛客周赛 Round 51

目录 A.小红的同余 B.小红的三倍数 C.小红充电 D.小红的gcd E.小红走矩阵 F.小红的数组 这次周赛题目比较简单&#xff0c;算法题也基本上是板子题&#xff0c;出得很好(&#xff5e;&#xffe3;▽&#xffe3;)&#xff5e; A.小红的同余 思路&#xff1a;签到题&am…

Android Studio 不再支持windows 7

Android Studio 一打开就报错&#xff1a; 无法找到入口 无法定位程序输入点 CreateAppContainerProfle 于动态链接库USERENV.dII 上。 截图如下&#xff1a; 经调查&#xff0c;是因为系统版本不兼容。 我目前的电脑环境&#xff1a;windows 7,但是现在的Android Studio要…

24年Hvv准备,6大方向,33篇技战法

进去不少小伙伴后台留言说需要技战法&#xff0c;因此小编对市面上的技战法进行了收集和总结&#xff0c;并对收集来的技战法进行了分类&#xff0c;总共分了6大类&#xff0c;共计33篇&#xff1a; 有需要的小伙伴关注我&#xff0c;点击在看&#xff0c;并私信回复“技战法”…

基于Java的原创歌曲分享平台

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBootMyEclipse 工具&#xff1a;MyEclipse、B/S架构 系统展示 首页 用户注册界面 音乐分享…

【python】OpenCV—Coordinates Sorted Clockwise

文章目录 1、需求介绍2、算法实现3、完整代码 1、需求介绍 调用 opencv 库&#xff0c;绘制轮廓的矩形边框&#xff0c;坐标顺序为右下→左下→左上→右上&#xff0c;我们实现一下转化为熟悉的 左上→右上→右下→左下 形式 按照这样的顺序组织边界框坐标是执行透视转换或匹…