【Go】-go中的锁机制
目录
一、锁的基础知识
1. 互斥量/互斥锁
2. CAS(compare and swap)
3. 自旋锁
4. 读写锁
5. 乐观锁 & 悲观锁
6. 死锁
二、go中锁机制
1. Mutex-互斥锁
2. RWMutex-读写锁
2.1 RWMutex流程概览
2.2 写锁饥饿问题
2.3. golang的读写锁源码剖析
2.3.1 读锁实现
2.3.2 写锁实现
2.3.3 关键核心机制
3. 常见问题
一、锁的基础知识
1. 互斥量/互斥锁
互斥量(Mutex), 又称为互斥锁, 是一种用来保护临界区的特殊变量, 它可以处于锁定(locked) 状态, 也可以处于解锁(unlocked) 状态。
在编程中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为" 互斥锁" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。
2. CAS(compare and swap)
解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。"
CAS机制执行流程:
CAS存在的问题:
1. ABA问题
CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化就更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了 —> 这就是所谓的ABA问题。
ABA问题的解决思路其实也很简单,就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A了。
2. 循环时间长开销大
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销
3. 只能保证一个共享变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
3. 自旋锁
自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。
自旋锁可能存在的2个问题:
-
试图递归地获得自旋锁必然会引起死锁:递归程序的持有实例在第二个实例循环,以试图获得相同自旋锁时,不会释放此自旋锁。
在递归程序中使用自旋锁应遵守下列策略:递归程序决不能在持有自旋锁时调用它自己,也决不能在递归调用时试图获得相同的自旋锁。 -
过多占用cpu资源。如果不加限制,由于申请者一直在循环等待,因此自旋锁在锁定的时候,如果不成功,不会睡眠,会持续的尝试,单cpu的时候自旋锁会让其它process动不了. 因此,一般自旋锁实现会有一个参数限定最多持续尝试次数. 超出后, 自旋锁放弃当前time slice. 等下一次机会。
由此可见,自旋锁比较适用于锁使用者保持锁时间比较短的情况。正是由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁。
4. 读写锁
读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。
这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。在读写锁保持期间也是抢占失效的。
如果读写锁当前没有读者,也没有写者,那么写者可以立刻获得读写锁,否则它必须自旋在那里,直到没有任何写者或读者。如果读写锁没有写者,那么读者可以立即获得该读写锁,否则读者必须自旋在那里,直到写者释放该读写锁。
5. 乐观锁 & 悲观锁
乐观锁其实主要就是一种思想,因为乐观锁的操作过程中其实没有没有任何锁的参与,乐观锁只是和悲观锁相对,严格的说乐观锁不能称之为锁。
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。
乐观锁适用于写比较少的情况下(多读场景),即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果是多写的情况,一般会经常产生冲突,这就会导致上层应用会不断的进行retry,这样反倒是降低了性能,所以一般多写的场景下用悲观锁就比较合适。
乐观锁常见的两种实现方式:
1. 版本号机制
CAS机制保证了在更新数据的时候没有被修改为其他数据的同步机制,版本机制就保证了没有被修改过的同步机制
2. CAS机制
当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败。
6. 死锁
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
虽然进程在运行过程中,可能发生死锁,但死锁的发生也必须具备一定的条件,死锁的发生必须具备以下四个必要条件:
- 互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。
- 请求和保持条件:指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放。
- 不剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。
- 环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。
二、go中锁机制
在 Golang 里有专门的方法来实现锁,就是 sync 包,这个包有两个很重要的锁类型。一个叫 Mutex, 利用它可以实现互斥锁。一个叫 RWMutex,利用它可以实现读写锁。
sync.Mutex
的锁只有一种锁:Lock()
,它是互斥锁,同一时间只能有一个锁。sync.RWMutex
叫读写锁,它有两种锁:RLock()
和Lock()
:RLock()
叫读锁。它不是绝对锁,可以有多个读者同时获取此锁(调用mu.RLock
)。Lock()
叫写锁,它是个绝对锁,就是说,如果一旦某人拿到了这个锁,别人就不能再获取此锁了。
1. Mutex-互斥锁
Mutex 的实现主要借助了 CAS 指令 + 自旋 + 信号量
数据结构:
type Mutex struct {state int32sema uint32
}
上述两个加起来只占 8 字节空间的结构体表示了 Go语言中的互斥锁
状态:
在默认情况下,互斥锁的所有状态位都是 0,int32 中的不同位分别表示了不同的状态:
- 1位表示是否被锁定
- 1位表示是否有协程已经被唤醒
- 1位表示是否处于饥饿状态
- 剩下29位表示阻塞的协程数
正常模式和饥饿模式
正常模式:正常模式下waiter都是先入先出,在队列中等待的waiter被唤醒后不会直接获取锁,因为要和新来的goroutine 进行竞争,新来的goroutine相对于被唤醒的waiter是具有优势的,新的goroutine 正在cpu上运行,被唤醒的waiter还要进行调度才能进入状态,所以在并发的情况下waiter大概率抢不过新来的goroutine,这个时候waiter会被放到队列的头部,如果等待的时间超过了1ms,这个时候Mutex就会进入饥饿模式。
饥饿模式:当Mutex进入饥饿模式之后,锁的所有权会从解锁的goroutine移交给队列头部的goroutine,这几个时候新来的goroutine会直接放入队列的尾部,这样很好的解决了老的goroutine一直抢不到锁的场景。
对于两种模式,正常模式下的性能是最好的,goroutine可以连续多次获取锁,饥饿模式解决了取锁公平的问题,但是性能会下降,其实是性能和公平的一个平衡模式。所以在lock的源码里面,当队列只剩本省goroutine一个并且等待时间没有超过1ms,这个时候Mutex会重新恢复到正常模式。
Lock函数
// 加锁
// 如果锁已经被使用,调用goroutine阻塞,直到锁可用
func (m *Mutex) Lock() {// 快速路径:没有竞争直接获取到锁,修改状态位为加锁if atomic.CompareAndSwapInt32(&m.state, 0, mutexLocked) {// 开启-race之后会进行判断,正常情况可忽略if race.Enabled {race.Acquire(unsafe.Pointer(m))}return}// 慢路径(以便快速路径可以内联)m.lockSlow()
}
来看看Lock函数,分为两个部分, 快速路径,先通过CAS尝试直接获取锁,如果能获取到直接返回,否则进入慢路径的方法,这里的代码注释提到了内联
tips:方法内联
简单的说方法内联就是将被调用方函数代码“复制”到调用方函数中,减少函数调用开销,在2018年之前的go版本中,所有的逻辑都在Lock函数中,并没有拆出来,2018年之后Go开发者将slow path拆出来,当lock方法被频繁调用的时候,有两种情况,如果直接获得锁走的是fast path,这个时候内联就只有fast path 的代码,这样会减少方法调用的堆栈空间和时间的消耗 ,如果处于自旋,锁竞争的情况下,走的是slow path,这个时候才会把lock slow 的方法内联进来,这样方便了编译器做内联。
lockSlow 函数
func (m *Mutex) lockSlow() {var waitStartTime int64 //记录请求锁的初始时间starving := false //饥饿标记awoke := false //唤醒标记iter := 0 //自旋次数old := m.state //当前锁的状态for {//锁处于正常模式还没有释放的时候,尝试自旋if old&(mutexLocked|mutexStarving) == mutexLocked && runtime_canSpin(iter) { //在临界区耗时很短的情况下提高性能if !awoke && old&mutexWoken == 0 && old>>mutexWaiterShift != 0 && atomic.CompareAndSwapInt32(&m.state, old, old|mutexWoken) {awoke = true}runtime_doSpin()iter++//更新锁的状态old = m.statecontinue}new := old// 非饥饿状态进行加锁if old&mutexStarving == 0 {new |= mutexLocked}// 等待着数量+1if old&(mutexLocked|mutexStarving) != 0 {new += 1 << mutexWaiterShift}// 加锁的情况下切换为饥饿模式if starving && old&mutexLocked != 0 {new |= mutexStarving}//goroutine 唤醒的时候进行重置标志if awoke {if new&mutexWoken == 0 {throw("sync: inconsistent mutex state")}new &^= mutexWoken}// 设置新的状态if atomic.CompareAndSwapInt32(&m.state, old, new) {if old&(mutexLocked|mutexStarving) == 0 {break }// 判断是不是第一次加入队列// 如果之前就在队列里面等待了,加入到队头queueLifo := waitStartTime != 0if waitStartTime == 0 {waitStartTime = runtime_nanotime()}// 阻塞等待runtime_SemacquireMutex(&m.sema, queueLifo, 1)// 检查锁是否处于饥饿状态starving = starving || runtime_nanotime()-waitStartTime > starvationThresholdNsold = m.state// 如果锁处于饥饿状态,直接抢到锁if old&mutexStarving != 0 {if old&(mutexLocked|mutexWoken) != 0 || old>>mutexWaiterShift == 0 {throw("sync: inconsistent mutex state")}// 设置标志,进行加锁并且waiter-1delta := int32(mutexLocked - 1<<mutexWaiterShift)// 如果是最后一个的话清除饥饿标志if !starving || old>>mutexWaiterShift == 1 {// 退出饥饿模式 delta -= mutexStarving}atomic.AddInt32(&m.state, delta)break}awoke = trueiter = 0} else {old = m.state}}// -race开启检测冲突,可以忽略if race.Enabled {race.Acquire(unsafe.Pointer(m))}
}
Unlock函数
//如果对没有lock 的Mutex进行unlock会报错
//unlock和goroutine是没有绑定的,对于一个Mutex,可以一个goroutine加锁,另一个goroutine进行解锁
func (m *Mutex) Unlock() {if race.Enabled {_ = m.staterace.Release(unsafe.Pointer(m))}// 快速之路,直接解锁,去除加锁位的标记new := atomic.AddInt32(&m.state, -mutexLocked)if new != 0 {// 解锁失败进入慢路径//同样的对慢路径做了单独封装,便于内联m.unlockSlow(new)}
}
unlockSlow函数
func (m *Mutex) unlockSlow(new int32) {//解锁一个未加锁的Mutex会报错(可以想想为什么,Mutex使用状态位进行标记锁的状态的)if (new+mutexLocked)&mutexLocked == 0 {throw("sync: unlock of unlocked mutex")}if new&mutexStarving == 0 {old := newfor {//正常模式下,没有waiter或者在处理事情的情况下直接返回if old>>mutexWaiterShift == 0 || old&(mutexLocked|mutexWoken|mutexStarving) != 0 {return}//如果有等待者,设置mutexWoken标志,waiter-1,更新statenew = (old - 1<<mutexWaiterShift) | mutexWokenif atomic.CompareAndSwapInt32(&m.state, old, new) {runtime_Semrelease(&m.sema, false, 1)return}old = m.state}} else {// 饥饿模式下会直接将mutex交给下一个等待的waiter,让出时间片,以便waiter执行runtime_Semrelease(&m.sema, true, 1)}
}
同样的,在unlock也有fastpath和slowpath,fastpath尝试解锁,解锁成功就返回,否则进入slowpath,slowpath分为正常模式的处理和饥饿模式的处理,饥饿模式直接将锁的控制权交给队列中等待的waiter,正常模式分两种情况 如果当前没有waiter,只有自己本身,直接解锁返回,如果有waiter,解锁后唤醒下个等待者。
2. RWMutex-读写锁
RWMutex
是一个读/写互斥锁,在某一时刻只能由任意数量的 reader
持有 或者 一个 writer
持有。也就是说,要么放行任意数量的 reader
,多个 reader
可以并行读;要么放行一个 writer
,多个 writer
需要串行写。
RWMutex 对外暴露的方法有五个:
RLock()
:读操作获取锁,如果锁已经被 writer 占用,会一直阻塞直到 writer 释放锁;否则直接获得锁;RUnlock()
:读操作完毕之后释放锁;Lock()
:写操作获取锁,如果锁已经被 reader 或者 writer 占用,会一直阻塞直到获取到锁;否则直接获得锁;Unlock()
:写操作完毕之后释放锁;RLocker()
:返回读操作的 Locker 对象,该对象的 Lock() 方法对应 RWMutex 的 -RLock()
,Unlock() 方法对应 RWMutex 的 RUnlock() 方法。
一旦涉及到多个 reader 和 writer ,就需要考虑优先级问题,是 reader 优先还是 writer 优先。
2.1 RWMutex流程概览
可以想象 RWMutex
有两个队伍,一个是包含 所有reader 和你获得准入权writer 的 队列A,一个是还没有获得准入权 writer 的 队列B。
队列 A 最多只允许有 一个writer,如果有其他 writer,需要在 队列B 等待;
当一个 writer 到了 队列A 后,只允许它 之前的reader 执行读操作,新来的 reader 需要在 队列A 后面排队;
当前面的 reader 执行完读操作之后,writer 执行写操作;
writer 执行完写操作后,让 后面的reader 执行读操作,再唤醒队列B 的一个 writer 到 队列A 后面排队。
初始时刻 队列A 中 writer W1 前面有三个 reader,后面有两个 reader,队列B中有两个 writer
并发读 多个 reader 可以同时获取到读锁,进入临界区进行读操作;writer W1 在 队列A 中等待,同时又来了两个 reader,直接在 队列A 后面排队
写操作 W1 前面所有的 reader 完成后,W1 获得锁,进入临界区操作
获得准入权 W1 完成写操作退出,先让后面排队的 reader 进行读操作,然后从 队列B 中唤醒 W2 到 队列A 排队。W2 从 队列B 到 队列A 的过程中,R8 先到了 队列A,因此 R8 可以执行读操作。R9、R10、R11 在 W2 之后到的,所以在后面排队;新来的 W4 直接在队列B 排队。
从上面的示例可以看出,RWMutex
可以看作是没有优先级,按照先来先到的顺序去执行,只不过是 多个reader 可以 并行去执行罢了。
2.2 写锁饥饿问题
因为读锁是共享的,所以如果当前已经有读锁,那后续goroutine继续加读锁正常情况下是可以加锁成功,但是如果一直有读锁进行加锁,那尝试加写锁的goroutine则可能会长期获取不到锁,这就是因为读锁而导致的写锁饥饿问题
go通过引入以下特性避免出现写锁饥饿:
- 当写锁阻塞时,新的读锁是无法申请的
即在sync.RWMutex
的使用中,一个线程请求了他的写锁(mx.Lock()
)后,即便它还没有取到该锁(可能由于资源已被其他人锁定),后面所有的读锁的申请,都将被阻塞,只有取写锁的请求得到了锁且用完释放后,读锁才能去取。
这种特性可以有效防止写锁饥饿。如果一个线程因为某种原因,导致长时间得不到CPU时间片,这种状态被称之为饥饿。
2.3. golang的读写锁源码剖析
成员变量
结构体
type RWMutex struct {w Mutex // held if there are pending writerswriterSem uint32 // 用于writer等待读完成排队的信号量readerSem uint32 // 用于reader等待写完成排队的信号量readerCount int32 // 读锁的计数器readerWait int32 // 等待读锁释放的数量
}
写锁计数
读写锁中允许加读锁的最大数量是4294967296,在go里面对写锁的计数采用了负值进行,通过递减最大允许加读锁的数量从而进行写锁对读锁的抢占
const rwmutexMaxReaders = 1 << 30
2.3.1 读锁实现
读锁加锁逻辑
func (rw *RWMutex) RLock() {if race.Enabled {_ = rw.w.staterace.Disable()}// 累加reader计数器,如果小于0则表明有writer正在等待if atomic.AddInt32(&rw.readerCount, 1) < 0 {// 当前有writer正在等待读锁,读锁就加入排队runtime_SemacquireMutex(&rw.readerSem, false)}if race.Enabled {race.Enable()race.Acquire(unsafe.Pointer(&rw.readerSem))}
}
读锁释放逻辑
func (rw *RWMutex) RUnlock() {if race.Enabled {_ = rw.w.staterace.ReleaseMerge(unsafe.Pointer(&rw.writerSem))race.Disable()}// 如果小于0,则表明当前有writer正在等待if r := atomic.AddInt32(&rw.readerCount, -1); r < 0 {if r+1 == 0 || r+1 == -rwmutexMaxReaders {race.Enable()throw("sync: RUnlock of unlocked RWMutex")}// 将等待reader的计数减1,证明当前是已经有一个读的,如果值==0,则进行唤醒等待的if atomic.AddInt32(&rw.readerWait, -1) == 0 {// The last reader unblocks the writer.runtime_Semrelease(&rw.writerSem, false)}}if race.Enabled {race.Enable()}
}
2.3.2 写锁实现
加写锁实现
func (rw *RWMutex) Lock() {if race.Enabled {_ = rw.w.staterace.Disable()}// 首先获取mutex锁,同时多个goroutine只有一个可以进入到下面的逻辑rw.w.Lock()// 对readerCounter进行进行抢占,通过递减rwmutexMaxReaders允许最大读的数量// 来实现写锁对读锁的抢占r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders// 记录需要等待多少个reader完成,如果发现不为0,则表明当前有reader正在读取,当前goroutine// 需要进行排队等待if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {runtime_SemacquireMutex(&rw.writerSem, false)}if race.Enabled {race.Enable()race.Acquire(unsafe.Pointer(&rw.readerSem))race.Acquire(unsafe.Pointer(&rw.writerSem))}
}
释放写锁
func (rw *RWMutex) Unlock() {if race.Enabled {_ = rw.w.staterace.Release(unsafe.Pointer(&rw.readerSem))race.Disable()}// 将reader计数器复位,上面减去了一个rwmutexMaxReaders现在再重新加回去即可复位r := atomic.AddInt32(&rw.readerCount, rwmutexMaxReaders)if r >= rwmutexMaxReaders {race.Enable()throw("sync: Unlock of unlocked RWMutex")}// 唤醒所有的读锁for i := 0; i < int(r); i++ {runtime_Semrelease(&rw.readerSem, false)}// 释放mutexrw.w.Unlock()if race.Enabled {race.Enable()}
}
2.3.3 关键核心机制
写锁对读锁的抢占
加写锁的抢占
// 在加写锁的时候通过将readerCount递减最大允许加读锁的数量,来实现对加读锁的抢占r := atomic.AddInt32(&rw.readerCount, -rwmutexMaxReaders) + rwmutexMaxReaders
加读锁的抢占检测
// 如果没有写锁的情况下读锁的readerCount进行Add后一定是一个>0的数字,这里通过检测值为负数
//就实现了读锁对写锁抢占的检测
if atomic.AddInt32(&rw.readerCount, 1) < 0 {// A writer is pending, wait for it.runtime_SemacquireMutex(&rw.readerSem, false)}
写锁抢占读锁后后续的读锁就会加锁失败,但是如果想加写锁成功还要继续对已经加读锁成功的进行等待
if r != 0 && atomic.AddInt32(&rw.readerWait, r) != 0 {// 写锁发现需要等待的读锁释放的数量不为0,就自己自己去休眠了runtime_SemacquireMutex(&rw.writerSem, false)
}
写锁既然休眠了,则必定要有一种唤醒机制其实就是每次释放锁的时候,当检查到有加写锁的情况下,就递减readerWait,并由最后一个释放reader lock的goroutine来实现唤醒写锁
if atomic.AddInt32(&rw.readerWait, -1) == 0 {// The last reader unblocks the writer.runtime_Semrelease(&rw.writerSem, false)
}
3. 常见问题
- 不可复制
和 Mutex 一样,RWMutex 也是不可复制。不能复制的原因和互斥锁一样。一旦读写锁被使用,它的字段就会记录它当前的一些状态。这个时候你去复制这把锁,就会把它的状态也给复制过来。但是,原来的锁在释放的时候,并不会修改你复制出来的这个读写锁,这就会导致复制出来的读写锁的状态不对,可能永远无法释放锁。 - 不可重入
不可重入的原因是,获得锁之后,还没释放锁,又申请锁,这样有可能造成死锁。比如 reader A 获取到了读锁,writer B 等待 reader A 释放锁,reader 还没释放锁又申请了一把锁,但是这把锁申请不成功,他需要等待 writer B。这就形成了一个循环等待的死锁。 - 加锁和释放锁一定要成对出现,不能忘记释放锁,也不能解锁一个未加锁的锁。
相关文章:
【Go】-go中的锁机制
目录 一、锁的基础知识 1. 互斥量/互斥锁 2. CAS(compare and swap) 3. 自旋锁 4. 读写锁 5. 乐观锁 & 悲观锁 6. 死锁 二、go中锁机制 1. Mutex-互斥锁 2. RWMutex-读写锁 2.1 RWMutex流程概览 2.2 写锁饥饿问题 2.3. golang的读写锁源…...
Scala学习记录,全文单词统计
package test32 import java.io.PrintWriter import scala.io.Source //知识点 // 字符串.split("分隔符":把字符串用指定的分隔符,拆分成多个部分,保存在数组中) object test {def main(args: Array[String]): Unit {//从文件1.t…...
重构项目架构
前言 我们上篇文章对整个项目进行一个整体的规划,其中对于APP类规划了类,本篇文章我们就来实现这个规划; class App {//加载页面constructor() {}//获取位置_getPosition() {}//接受位置_loadMap() {}//在地图上点击展现表单_showForm() {}/…...
一个开源轻量级的服务器资源监控平台,支持告警推送
大家好,今天给大家分享一款开源的轻量级服务器资源监控工具Beszel,提供历史数据记录、Docker容器统计信息监控以及多种警报功能,用于监控服务器资源。 项目介绍 Beszel由hub(中心服务器端应用,基于PocketBase构建&…...
介绍一下atof(arr);(c基础)
hi , I am 36 适合对象c语言初学者 atof(arr);是返回浮点数(double型),浮点数数是arr数组中字符中数字 格式 #include<stdio.h> atof(arr); 返回值arr数组中的数 未改变arr数组 #include<stdio.h> //atof(arr) 返 <stdlib> int…...
基于微信小程序的平价药房管理系统+LW参考示例
1.项目介绍 系统角色:管理员、医生、普通用户功能模块:用户管理、医生管理、药品分类管理、药品信息管理、在线问诊管理、生活常识管理、日常提醒管理、过期处理、订单管理等技术选型:SpringBoot,Vue,uniapp等测试环境…...
什么是 C++ 中的函数对象?它有什么特点?如何定义和使用函数对象?数对象与普通函数有什么区别?
在 C 中,函数对象(Function Object)也被称为仿函数(Functor),是一种可以像函数一样被调用的对象,是一个类的对象,该类重载了函数调用运算符operator()。 函数对象的特点: 与普通函数…...
JAVA篇05 —— 内部类(Local、Anonymous、Member、Static)
欢迎来到我的主页:【一只认真写代码的程序猿】 本篇文章收录于专栏【小小爪哇】 如果这篇文章对你有帮助,希望点赞收藏加关注啦~ 目录 1 内部类Inner Class 1.1 局部内部类 1.2 匿名内部类(※※) 1.3 匿名类最佳实践…...
vmware安装ubuntu22.04 复制黏贴 上网
1、ubuntu下载 [Download - 清华镜像站]: 点击 清华大学开源软件镜像站 - Ubuntu 22.04.4 下载 页面中的 ubuntu-22.04.4-desktop-amd64.iso清华大学开源软件镜像站 - Ubuntu 22.04.4 下载 2、安装向导 3、网络设置 sudo netplan try sudo netplan apply4、复制…...
Spring Bean初始化流程
首先: 加载Bean定义(Configuration) 然后对于每个Bean: 1、实例化Bean(应该是从Bean方法中获取,Bean方法里面包含new这个类型的代码)2、依赖注入(所依赖的Bean要经历相同的流程)、调用Setter…...
C 语言函数递归探秘:从基础概念到复杂问题求解的进阶之路
我的个人主页 我的专栏:C语言,希望能帮助到大家!!!点赞❤ 收藏❤ 目录 什么是函数递归递归的基本组成递归的工作原理递归的优缺点递归的经典案例 5.1 阶乘计算5.2 斐波那契数列5.3 汉诺塔问题5.4 二分查找 递归的高级…...
【Zookeeper】三,Zookeeper的安装与基本操作
文章目录 安装Zookeeper下载解压解压后的目录结构运行Zookeeper 基本操作 安装Zookeeper 下载 官网下载Zookeeper,会得到一个tar包,如:apache-zookeeper-3.8.4-bin.tar.gz 解压 tar -xvf apache-zookeeper-3.8.4-bin.tar.gz -C /usr/loca…...
STL算法之数值算法<stl_numeric.h>
这一节介绍的算法,统称为数值(numeric)算法。STL规定,欲使用它们,客户端必须包含头文件<numeric>.SGI将它们实现与<stl_numeric.h>文件中。 目录 运用实例 accumulate adjacent_difference inner_product partial_sum pow…...
Git 入门超简单指南
1. 什么是 Git? Git 是一个分布式版本控制系统,由 Linus Torvalds 于 2005 年创建。它的主要目的是帮助开发者有效地管理和跟踪项目的历史版本。通过使用 Git,你可以轻松地记录每一次代码的修改,回滚到以前的版本,以及…...
UE5 和 UE4 中常用的控制台命令总结
调用控制台 按下键盘上的 ~ 键可以调用控制台命令。 技巧 使用键盘的 ↑ 键可以查看之前输入过的指令。控制台指令并不需要打全名,输入空格后跟随指令的部分字符可以进行模糊搜索。按下 Ctrl Shift , 打开 GPUProfile 面板。 命令如下: 调试类 s…...
[护网杯 2018]easy_tornado
这里有一个hint点进去看看,他说md5(cookie_secretmd5(filename)),所以我们需要获得cookie_secret的value 根据题目tornado,它可能是tornado的SSTI 这里吧filehash改为NULL. 是tornado的SSTI 输入{{handler.settings}} (settings 属性是一个字典&am…...
论 ONLYOFFICE:开源办公套件的深度探索
公主请阅 引言第一部分:ONLYOFFICE 的历史背景1.1 开源软件的崛起1.2 ONLYOFFICE 的发展历程 第二部分:ONLYOFFICE 的核心功能2.1 文档处理2.2 电子表格2.3 演示文稿 第三部分:技术架构与兼容性3.1 技术架构3.2 兼容性 第四部分:部…...
华为OD机试真题---幼儿园篮球游戏
华为OD机试真题中的“幼儿园篮球游戏”是一道有趣的逻辑模拟题。以下是该题目的详细描述及解题思路: 题目描述 幼儿园里有一个放倒的圆桶,它是一个线性结构。允许在桶的右边将篮球放入,可以在桶的左边和右边将篮球取出。每个篮球有单独的编…...
C#基础控制台程序
11.有一个54的矩阵,要求编程序求出其中值最大的那个元素的值,以及其所在的行号和列号。 12.从键盘输入一行字符,统计其中有多少个单词,单词之间用空格分隔开。 13.输入一个数,判断它是奇数还是偶数,如果…...
CASS插入多行文字
问题描述 有时在DWG文件中需要标注多行文字,文字注记只是单行的。 解决办法 工具栏中选择文字——>写文字。快捷键是 MTEXT 可以自行换行,并设置相关格式。 当需要把多行文字转换成单行文字的时候,使用下列功能。可以将多行文字变成…...
【青牛科技】D1671 75Ω 带4级低通滤波的单通道视频放大电 路芯片介绍
概 述 : D1671是 一 块 带 4级 低 通 滤 波 的 单 通 道 视 频 放 大 电 路 , 可 在3V或5V的 低 电 压 下 工 作 。 该 电 路 用 在 有 TV影 象 输 出 功 能 的 产 品 上 面,比如 机 顶 盒 ,监 控 摄 象 头 ,DVD&#…...
基于stm32的智能教室管理系统/智能家居系统
基于stm32的智能教室管理系统/智能家居系统 持续更新,欢迎关注!!! ** 基于stm32的智能教室管理系统/智能家居系统 ** 目前,物联网已广泛应用在我们的生活中。智慧校园是将校园中的生活、学习、工作等相关的资源联系在一起,实现管理的智能化…...
3.10 内核 BUG_ON() at xfs_vm_writepage() -> page_buffers()
目录 前言 问题分析 page buffers创建 page buffers丢失 Write-Protect Dirty Page w/o Buffers 问题解决 前言 这个问题发生在3.10.0-514.el7上,并且在RHEL的知识库中快速找到了对应的案例以及解决方案,但是,理解问题如何发生和解决…...
理解Java集合的基本用法—Collection:List、Set 和 Queue,Map
本博文部分参考 博客 ,强烈推荐这篇博客,写得超级全面!!! 图片来源 Java 集合框架 主要包括两种类型的容器,一种是集合(Collection),存储一个元素集合(单列…...
SQL for XML
关系数据模型与SQL SQL for XML 模式名功能RAW返回的行作为元素,列值作为元素的属性AUTO返回表名对应节点名称的元素,每列的属性作为元素的属性输出输出,可形成简单嵌套结构EXPLICIT通过SELECT语法定义输出XML结构PATH列名或列别名作为XPAT…...
RabbitMQ 篇-深入了解延迟消息、MQ 可靠性(生产者可靠性、MQ 可靠性、消费者可靠性)
??博客主页:【_-CSDN博客】** 感谢大家点赞??收藏评论** 文章目录 ???1.0 RabbitMQ 的可靠性 ? ? ? ? 2.0 发送者的可靠性 ? ? ? ? 2.1 生产者重试机制 ? ? ? ? 2.2 生产者确认机制 ? ? ? ? 2.2.1 开启生产者确认机制 ? ? ? ? 2.2…...
Java设计模式 —— 【创建型模式】原型模式(浅拷贝、深拷贝)详解
文章目录 前言原型模式一、浅拷贝1、案例2、引用数据类型 二、深拷贝1、重写clone()方法2、序列化 总结 前言 先看一下传统的对象克隆方式: 原型类: public class Student {private String name;public Student(String name) {this.name name;}publi…...
LightRAG - 更快更便宜的GraphRAG
检索增强生成(Retrieval-Augmented Generation, RAG)已经成为提升大型语言模型(LLMs)能力的重要方法之一,通过整合外部知识,显著改善了生成内容的质量和相关性。 RAG 的局限性 传统的 RAG 系统虽然表现优…...
基于STM32的智能风扇控制系统
基于STM32的智能风扇控制系统 持续更新,欢迎关注!!! ** 基于STM32的智能风扇控制系统 ** 近几年,我国电风扇市场发展迅速,产品产出持续扩张,国家产业政策鼓励电风扇产业向高技术产品方向发展,国内企业新增投资项目投…...
Java面试问答FAQ
目录: 1、post为什么会发送两次请求?2、单核CPU支持多线程吗?3、ConcurrentHashMap 如何保证线程的安全性? 1、post为什么会发送两次请求? A:那是因为浏览器的安全策略(同源策略)决…...
PHP中类名加双冒号的作用
在 PHP 中,类名加双冒号(::) 是一种用于访问类的静态成员和常量的语法。它也可以用来调用类的静态方法和访问 PHP 的类相关关键词(如 parent、self 和 static)。以下是详细的解释和用法。 1. 用途概述 :: 被称为作用域…...
[极客大挑战 2019]PHP
访问www.zip拿到源码. 绕过这三处. 构造exp <?php class Name{private $username admin;private $password 100;}$select new Name();$resserialize($select); echo $res ?>O:4:"Name":2:{s:14:"%00Name%00username";s:5:"admin"…...
【versal】【petalinux】添加LED驱动
versal 添加LED驱动 ` 提示:本文使用外部kernel与uboot`一、LED1.1 LED功能1.2 LED节点1.3 LED操作命令1.3.1 点LED1.3.2 关闭LED二、LED驱动2.1 驱动文件2.2 设备树兼容属性三、 LED设备树配置3.1 设备树配置信息3.2 设备树配置信息讲解四、提示4.1 正确4.2 错误4.3提示:本文…...
【前端】JavaScript中的字面量概念与应用详解
博客主页: [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: 前端 文章目录 💯前言💯字面量1. 数字字面量2. 字符串字面量3. 布尔字面量4. 空值字面量(null)5. 对象字面量6. 数组字面量7. 正则表达式字面量8. 特殊值字面量9. 函数字…...
工作学习:切换git账号
概括 最近工作用的git账号下发下来了,需要切换一下使用的账号。因为是第一次弄,不熟悉,现在记录一下。 打开设置 路径–git—git remotes,我这里选择项是Manage Remotes,点进去就可以了。 之后会出现一个输入框&am…...
python-解决一元一次方程
【题目】解决一元一次方程数学问题 【问题描述】 我们要解决一个较为复杂的一元一次方程数学问题,方程形如:ax b c,其中a、b、c为已知常数,x为未知数。 现在我们要通过编程的方式解决这个问题,即找到方程的解x。 【…...
在线音乐播放器 —— 测试报告
自动化脚本源代码:Java: 利用Java解题与实现部分功能及小项目的代码集合 - Gitee.com 目录 前言 一、项目简介 1.项目背景 2.应用技术 (1)后端开发 (2)前端开发 (3)数据库 二、项目功能…...
6.算法移植第六篇 YOLOV5/rknn生成可执行文件部署在RK3568上
接上一篇文章best-sim.rknn模型生成好后,我们要将其转换成可执行文件运行在RK3568上,这一步需要在rknpu上进行,在强调一遍!!rknpu的作用是可以直接生成在开发板上运行的程序 退出上一步的docker环境 exit1.复制best-…...
【贪心算法第七弹——674.最长连续递增序列(easy)】
目录 1.题目解析 题目来源 测试用例 2.算法原理 3.实战代码 代码分析 1.题目解析 题目来源 674.最长递增子序列——力扣 测试用例 2.算法原理 贪心思路 3.实战代码 class Solution { public:int findLengthOfLCIS(vector<int>& nums) {int n nums.size();in…...
剖析前后端 API 接口参数设计:JSON 数据结构化全攻略
在当今软件开发领域,前后端分离架构已成为主流趋势。而 API 接口作为前后端之间数据交互的桥梁,其设计的合理性对系统的可维护性和扩展性起着至关重要的作用。JSON(JavaScript Object Notation)作为一种轻量级的数据交换格式&…...
【RISC-V CPU debug 专栏 2.3 -- Run Control】
文章目录 Run ControlHart 运行控制状态位状态信号操作流程时间与实现注意事项Run Control 在 RISC-V 调试架构中,运行控制模块通过管理多个状态位来对硬件线程(harts)的执行进行调节和控制。这些状态位帮助调试器请求暂停或恢复 harts,并在 hart 复位时进行控制。以下是运…...
力扣887:鸡蛋掉落问题
题目描述: 给你 k 枚相同的鸡蛋,并可以使用一栋从第 1 层到第 n 层共有 n 层楼的建筑。 已知存在楼层 f ,满足 0 < f < n ,任何从 高于 f 的楼层落下的鸡蛋都会碎,从 f 楼层或比它低的楼层落下的鸡蛋都不会破。…...
Matlab与python数据处理对比
MATLAB 和 Python 都是数据分析和科学计算中常用的编程语言,各自有其优点和应用场景。它们在数据处理方面有许多相似之处,但也有一些关键差异。以下是 MATLAB 和 Python 在数据处理方面的对比与总结。 ### 1. **语法与语言特性** - **MATLAB**: - 以…...
【系统架构设计师】高分论文:论软件架构的生命周期
更多内容请见: 备考系统架构设计师-专栏介绍和目录 文章目录 摘要正文摘要 2022 年5月,本人所在的某集团公司承接了财务共享服务平台综合管理系统的项目开发,该项目主要实现财务系统主流业务的集成共享。本人担任项目组成员中的系统架构设计师一职,全面负责项目的全生命周…...
gitlab工作笔记
gitlab常用操作 gitlab常用笔记docker 安装模式pull imagerun一个gitlab container atttach入containerdocker run 之后要等几分钟安装之后的初始配置初始密码在哪里 :第一次登录创建和检查第一个工程能否正常clone gitlab常用笔记 装过几次gitlab,但每…...
shell完结
声明! 学习视频来自B站up主 **泷羽sec** 有兴趣的师傅可以关注一下,如涉及侵权马上删除文章,笔记只是方便各位师傅的学习和探讨,文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关&a…...
优维HAO案例:全球TOP15汽车零件供应商「IT运维自动化」创新工程
撰文:鹿小U / 制图:脾气超好 又是一家很厉害的客户。 YADT是全(hu)球(zhēn)领(ji)先(sh)的汽车座椅供应商,拥有从汽车座椅零部件到整椅的完整生产制造能力,为中国几乎所有的汽车制造商提供汽车整椅产品和服务。 YADT在国内拥…...
语义版本控制
注意: 本文内容于 2024-11-27 22:25:05 创建,可能不会在此平台上进行更新。如果您希望查看最新版本或更多相关内容,请访问原文地址:语义版本控制。感谢您的关注与支持! 由于自己平时喜欢写点小玩意,自然而…...
C++设计模式行为模式———策略模式
文章目录 一、引言二、策略模式三、总结 一、引言 策略模式是一种行为设计模式, 它能让你定义一系列算法, 并将每种算法分别放入独立的类中, 以使算法的对象能够相互替换。与模板方法模式类似,都是以扩展的方式来支持未来的变化。…...
Pgsql:json字段查询与更新
1.查询json字段的值 SELECT attribute_data->>设施类别 mycol, * FROM gis_coord_data WHERE attribute_data->>设施类别阀门井 查询结果如下: 2.更新json字段中的某个属性值 UPDATE gis_coord_data SET attribute_data(attribute_data::jsonb ||{&quo…...