近万字深入讲解iOS常见锁及线程安全

什么是锁?

在程序中,当多个任务(或线程)同时访问同一个资源时,比如多个操作同时修改一份数据,可能会导致数据不一致。这时候,我们需要“锁”来确保同一时间只有一个任务能够操作这个数据,避免“抢占”问题。简单来说,锁就是一种机制,它能帮助你控制多个任务按顺序来操作资源

按照锁的功能来进行分类,iOS常见的锁:自旋、互斥、递归、条件。。。

一、自旋锁

自旋锁的意思就是当资源被占有时,自旋锁不会引起其他调用者休眠,而是让其他调用者自旋,不停的循环访问自旋锁导致调用者处于busy-wait(忙等状态),直到自旋锁的保持者释放锁。自旋锁是为了实现保护共享资源一种锁机制,在任何时刻只能有一个保持者,也就是说在任何时刻只能有一个可执行单元获得锁。也正是因为其他调用者会保持自旋状态,使得在锁的保持者释放锁时能够即刻获得锁,效率非常高。但我们说调用者时刻自旋也是消耗CPU资源的,所以如果自旋锁的使用者保持锁的时间比较短的话,使用自旋锁是非常合适的,因为在锁释放之后省去了唤醒调用者的时间。

1.OSSpinLock(已弃用)

  • OSSpinLock 是一种轻量级的锁。当一个线程获取不到锁时,它不会进入睡眠状态,而是一直循环检查锁是否可用,这叫“自旋”。
  • 缺点OSSpinLock 已经被弃用,因为它容易导致“优先级反转”(低优先级线程获取锁,高优先级线程等待锁释放,造成高优先级线程无法执行)。

2.os_unfair_lock

  • os_unfair_lockOSSpinLock 的替代品。它解决了优先级反转问题,当一个线程无法获取锁时,会立即休眠而不是自旋。

  • 适用场景:用于短时间的锁定操作,轻量、快速。

var unfairLock = os_unfair_lock_s()

func safeMethod() {
    os_unfair_lock_lock(&unfairLock)
    // 执行共享资源的操作
    os_unfair_lock_unlock(&unfairLock)
}

用GCD模拟多线程,看是否输出是否按顺序输出:

import Foundation

// 初始化不公平锁
var unfairLock = os_unfair_lock_s()

// 共享资源(例子:计数器)
var sharedCounter = 0

// 线程安全的方法,增量计数器
func safeIncrement() {
    // 加锁,确保只有一个线程可以访问共享资源
    os_unfair_lock_lock(&unfairLock)
    
    // 临界区:操作共享资源
    sharedCounter += 1
    print("计数器增加: \(sharedCounter)") // 输出当前计数器的值
    
    // 解锁,允许其他线程访问共享资源
    os_unfair_lock_unlock(&unfairLock)
}

// 使用DispatchQueue模拟多线程
let queue = DispatchQueue.global()

// 测试线程安全的方法
for _ in 1...10 {
    queue.async {
        safeIncrement() // 多个线程同时操作计数器
    }
}

输出:

二、互斥锁

互斥锁和自旋锁类似,都是为了解决对某项资源的互斥使用,并且在任意时刻最多只能有一个执行单元获得锁,与自旋锁不同的是,互斥锁在被持有的状态下,其他资源申请者只能进入休眠状态,当锁被释放后,CPU会唤醒资源申请者,然后获得锁并访问资源。

1.pthread_mutex_t

  • pthread_mutex_t 是 POSIX 线程库提供的底层互斥锁,能确保同一时刻只有一个线程访问共享资源。
  • 适用场景:高效多线程编程,适合对性能要求高的场景。
var mutex = pthread_mutex_t()
pthread_mutex_init(&mutex, nil)

func safeMethod() {
    pthread_mutex_lock(&mutex)
    // 执行共享资源的操作
    pthread_mutex_unlock(&mutex)
}

同样的GCD模拟多线程,看是否输出是否按顺序输出:

import Foundation
//初始化互斥锁(Mutex)
var mutex = pthread_mutex_t()
pthread_mutex_init(&mutex, nil)

// 共享资源(例子:计数器)
var sharedCounter = 0

// 线程安全的方法,增量计数器
func safeIncrement() {
    // 加锁,确保只有一个线程可以访问共享资源
    pthread_mutex_lock(&mutex)
    
    // 临界区,操作共享资源
    sharedCounter += 1
    print("计数器增加: \(sharedCounter)") // 输出当前计数器的值

    // 解锁,允许其他线程访问共享资源
    pthread_mutex_unlock(&mutex)
}

// 使用DispatchQueue模拟多线程
let queue = DispatchQueue.global()

// 测试线程安全的方法
for _ in 1...10 {
    queue.async {
        safeIncrement() // 多个线程同时操作计数器
    }
}

输出:

2.NSLock

  • NSLock 是 Cocoa 提供的更高级的互斥锁,它比 pthread_mutex_t 更易于使用。
let lock = NSLock()

func safeMethod() {
    lock.lock()
    // 执行代码
    lock.unlock()
}

GCD模拟多线程:

import Foundation
//初始化互斥锁(NSLock)
var lock = NSLock()

// 共享资源(例子:计数器)
var sharedCounter = 0

// 线程安全的方法,增量计数器
func safeIncrement() {
    // 加锁,确保只有一个线程可以访问共享资源
    lock.lock()
    
    // 临界区,操作共享资源
    sharedCounter += 1
    print("计数器增加: \(sharedCounter)") // 输出当前计数器的值

    // 解锁,允许其他线程访问共享资源
    lock.unlock()
}

// 使用DispatchQueue模拟多线程
let queue = DispatchQueue.global()

// 测试线程安全的方法
for _ in 1...10 {
    queue.async {
        safeIncrement() // 多个线程同时操作计数器
    }
}

3.@synchronized(仅支持 Objective-C)

  • 这是 Objective-C 中提供的自动锁机制,是OC的语法糖,Swift 中无法直接使用。它可以帮助你简化锁定的逻辑。
@synchronized(self) {
    // 执行共享资源操作
}

三、递归锁

递归锁可以被同一线程多次请求,而不会引起死锁,即在多次被同一个线程进行加锁时,不会造成死锁。这主要是用在循环或递归操作中。

递归锁也是通过 pthread_mutex_lock 函数来实现,在函数内部会判断锁的类型,如果显示是递归锁,就允许递归调用,仅仅将一个计数器加一,等到递归完毕之后,所有锁都会释放

1.NSRecursiveLock

  • NSRecursiveLock 是一种递归锁,允许同一个线程多次获取同一把锁而不会导致死锁。这是 NSLock 无法做到的。
  • 适用于需要多次锁定同一资源的场景。
let recursiveLock = NSRecursiveLock()

func recursiveFunction(count: Int) {
    recursiveLock.lock()
    if count > 0 {
        print("Count: \(count)")
        recursiveFunction(count: count - 1)
    }
    recursiveLock.unlock()
}

2.pthread_mutex_t (递归锁)

  • pthread_mutex_t 也可以被设置为递归模式,用法类似 NSRecursiveLock。

四、条件锁

条件是信号量的另一种类型,当某个条件为true时,它允许线程相互发信号。条件通常用于指示资源的可用性或确保任务以特定顺序执行。当线程测试条件时,除非该条件已经为真,否则它将阻塞。它保持阻塞状态,直到其他线程显式更改并发出条件信号为止。条件和互斥锁之间的区别在于,可以允许多个线程同时访问该条件。 

1.pthread_cond_t

  • pthread_cond_t 是一种条件锁,常与 pthread_mutex_t 搭配使用,允许线程在满足特定条件时进行等待或唤醒。
  • 适用场景:用于需要等待某个条件满足的多线程场景。

2.NSCondition

  • NSCondition 是一个高级条件锁,可以让线程根据某些条件来等待或唤醒。
  • NSCondition 的底层是通过条件变量(condition variable) pthread_cond_t 来实现的。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程,比如常见的生产者-消费者模式。生产者-消费者模式可以查看我的另一篇博客:iOS--生产者-消费者模式理解(附GCD信号量代码实现)-CSDN博客
let condition = NSCondition()
var isReady = false

func producer() {
    condition.lock()
    isReady = true
    condition.signal()  // 唤醒等待中的线程
    condition.unlock()
}

func consumer() {
    condition.lock()
    while !isReady {
        condition.wait()  // 等待条件满足
    }
    // 执行消费操作
    condition.unlock()
}

模拟:

import Foundation

let condition = NSCondition()
var isReady = false

// 生产者方法
func producer() {
    condition.lock()
    print("生产者正在准备资源...")
    isReady = true
    print("资源准备完成,通知消费者")
    condition.signal()  // 唤醒等待的消费者线程
    condition.unlock()
}

// 消费者方法
func consumer() {
    condition.lock()
    print("消费者等待资源...")
    while !isReady {
        condition.wait()  // 等待条件满足
    }
    print("资源已准备好,开始消费资源")
    // 执行消费操作
    condition.unlock()
}

// 模拟并发:使用DispatchQueue进行生产者和消费者的交互
let queue = DispatchQueue.global()

// 消费者等待资源
queue.async {
    consumer()
}

// 模拟生产者延迟生产资源
queue.asyncAfter(deadline: .now() + 2) {
    producer()
}

3.NSConditionLock

  • NSConditionLock 是 NSCondition 的一种变体,基于条件值进行锁定和解锁,适用于更复杂的线程同步场景。

五、信号量

信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。为了完成这个过程,需要创建一个信号量VI,然后将Acquire Semaphore VI以及Release Semaphore VI分别放置在每个关键代码段的首末端。确认这些信号量VI引用的是初始创建的信号。

其实本质上,它通过维护一个计数值来控制同时可以访问某一资源的线程数量

1.dispatch_semaphore_t

  • dispatch_semaphore_t 是 GCD 提供的信号量机制,用于控制同时访问某一资源的线程数量。
  • 适用场景:适合控制并发任务的数量。
let semaphore = DispatchSemaphore(value: 1)

func safeMethod() {
    semaphore.wait()   // 请求资源
    // 执行共享资源操作
    semaphore.signal() // 释放资源
}

2.pthread_mutex_t(作为信号量使用)

  • 可以通过将 pthread_mutex_tpthread_cond_t 配合使用,达到类似信号量的效果。

六、读写锁

读写锁实际是一种特殊的自旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进行读访问,写者则需要对共享资源进行写操作。这种锁相对于自旋锁而言,能提高并发性,因为在多处理器系统中,它允许同时有多个读者来访问共享资源,最大可能的读者数为实际的逻辑CPU数。写者是排他性的,一个读写锁同时只能有一个写者或多个读者(与CPU数相关),但不能同时既有读者又有写者。

读写锁适合于对数据结构的读次数比写次数多得多的情况. 因为, 读模式锁定时可以共享, 以写模式锁住时意味着独占, 所以读写锁又叫共享-独占锁。

1.pthread_rwlock_t

  • pthread_rwlock_t 是一种读写锁,它允许多个线程同时读取资源,但在写入时会排他性地锁定。
  • 适用场景:适合读多写少的场景。
var rwlock = pthread_rwlock_t()
pthread_rwlock_init(&rwlock, nil)

func readResource() {
    pthread_rwlock_rdlock(&rwlock)  // 加读锁
    // 读取资源
    pthread_rwlock_unlock(&rwlock)  // 解锁
}

func writeResource() {
    pthread_rwlock_wrlock(&rwlock)  // 加写锁
    // 写入资源
    pthread_rwlock_unlock(&rwlock)  // 解锁
}

七、栅栏

栅栏函数在GCD中常用来控制线程同步,在队列中它总是等栅栏之前的任务执行完,然后执行栅栏自己的任务,执行完自己的任务后,再继续执行栅栏后的任务。常用函数有同步栅栏函数(dispatch_barrier_sync)和异步栅栏函数(dispatch_barrier_async)。

import Foundation

// 创建一个并发队列
let queue = DispatchQueue(label: "com.example.concurrentQueue", attributes: .concurrent)

// 异步执行任务1
queue.async {
    print("Task 1")
}

// 异步执行任务2
queue.async {
    print("Task 2")
}

// 使用 barrier 标志的任务,确保在这个任务期间,队列中不会有其他任务执行
queue.async(flags: .barrier) {
    print("Barrier task")
    print("Barrier task2")  // 在屏障任务中执行第二个打印
}

// 异步执行任务3,等待 barrier 任务执行完毕后继续
queue.async {
    print("Task 3")
}

输出顺序

• Task 1 和 Task 2 可能无序输出,因为它们是并发执行的。

• 屏障任务会在之前的任务完成后执行。

• Task 3 将在屏障任务结束后执行。

总结:

线程安全

上述学习的各种常见的线程锁,都是为了更好地管理和调配线程,而在开发中我们往往不可避免地采用多线程并发,这虽然很便利但是存在的巨大的安全隐患

什么是线程安全?

线程安全指的是多个线程可能同时操作同一块内存,从而导致的异常情况,先举个例子:

class User {
    private(set) var name: String = ""
    func setName(_ name: String) {
        self.name = name
    }
}

let user = User()

let queue1 = DispatchQueue(label: "q1")
let queue2 = DispatchQueue(label: "q2")

queue1.async {
    user.setName("1")
    print(user.name)
}
queue2.async {
    user.setName("2")
    print(user.name)
}

这段代码因为多线程并发同时修改同一个变量,导致可能的结果是测试下来可能会是打印了两个 2,这就不符合我们的预期了,明明第一个 user.setName 传入的是 “1”,打印结果却为 2。

这种情况称为资源竞争,两个线程可能同时操作 user 对象,实际上,除了结果不符合预期外,还可能出现一个经典的崩溃 EXC_BAD_ACCESS,这是因为让两个线程尝试同时操作同一个内存地址导致的。

如何解决资源竞争问题?

很简单,用我们刚学完的锁就可以。这里就不举例子了。。。

其他并发问题

除了上边提到的资源竞争问题,在使用并发的时候还可能导致一些其他问题,也需要注意,比如:

  • 条件竞争:无法同步执行两个或多个线程,导致事件以错误的顺序执行
  • 死锁:两个线程相互等待,这意味着两者都无法继续,线程会卡死
  • 优先级倒置:低优先级任务持有高优先级任务所需的资源,导致执行延迟
  • 线程爆炸:程序中申请的线程数量过多,导致资源耗尽和系统性能下降
  • 线程匮乏:因为其他线程正在占用这个资源,导致其他线程无法访问,从而导致执行延迟

解决方法:

1. 条件竞争
  • 使用锁:使用互斥锁(如NSLock)或信号量(如DispatchSemaphore)确保对共享资源的安全访问。
  • 使用队列:使用串行队列或DispatchQueue的同步方法来控制对共享数据的访问顺序。
2. 死锁
  • 避免嵌套锁:尽量避免一个线程在持有锁时请求另一个锁。
  • 设置锁的顺序:确保所有线程按照相同的顺序获取锁。
  • 使用超时:设置锁的获取超时,防止无限等待。
3. 优先级倒置
  • 优先级提升:在需要时提升低优先级线程的优先级,让所需资源尽快释放。
  • 资源管理:确保高优先级线程能及时访问所需资源,比如使用锁机制。
4. 线程爆炸
  • 限制线程数量:使用线程池管理线程数量,避免创建过多线程。
  • 使用异步任务:采用GCD的队列,减少线程的创建。
5. 线程匮乏
  • 优化资源使用:检查资源访问和锁的使用,确保高效利用资源。
  • 调整线程设计:使用更灵活的线程模型,比如异步编程,减少对共享资源的依赖。

这里重点讲解一下什么是死锁。。。。。

死锁

在 Swift 中,当两个线程都在等待对方释放资源时,就会发生deadlock 死锁。这会导致线程都处于永久等待状态,当主线程死锁,应用的表现上就是崩溃,其他子线程死锁可能导致卡死。

举个例子:

class ViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        print(1)
        
        DispatchQueue.main.sync {
            print(2)
        }
        
        print(3)
    }
}

上述代码会输出1后程序崩溃。

原因:主线程是处理用户界面和交互的线程。在viewDidLoad中打印1后,想要在主线程上执行另一个任务(打印2)。使用sync意味着希望这个任务立刻完成,而主线程正在执行viewDidLoad方法。因为主线程正在等着这个新任务完成(打印2),但这个任务又在主线程上运行,所以主线程无法继续,导致死锁

再举一个因为互斥锁而引发死锁的例子:

import Foundation

// 创建两个锁
let lock1 = NSLock()
let lock2 = NSLock()

// 线程1任务
func thread1() {
    print("线程1 尝试获取 lock1")
    lock1.lock()
    print("线程1 获取了 lock1")
    
    // 模拟处理一些操作
    sleep(1)
    
    print("线程1 尝试获取 lock2")
    lock2.lock()  // 死锁发生在这里,因为线程2已经持有了 lock2
    
    print("线程1 获取了 lock2")  // 这行永远不会被执行
    lock2.unlock()
    lock1.unlock()
}

// 线程2任务
func thread2() {
    print("线程2 尝试获取 lock2")
    lock2.lock()
    print("线程2 获取了 lock2")
    
    // 模拟处理一些操作
    sleep(1)
    
    print("线程2 尝试获取 lock1")
    lock1.lock()  // 死锁发生在这里,因为线程1已经持有了 lock1
    
    print("线程2 获取了 lock1")  // 这行永远不会被执行
    lock1.unlock()
    lock2.unlock()
}

// 并发队列
let queue = DispatchQueue.global()

// 启动线程
queue.async {
    thread1()
}

queue.async {
    thread2()
}
  1.     线程1 首先获取 lock1,然后等待获取 lock2。
  2.     线程2 首先获取 lock2,然后等待获取 lock1。
  3.     由于两个线程互相等待对方释放锁,导致程序进入死锁,两个线程都无法继续执行。

造成死锁的四个条件:

  1. 互斥条件:某个资源一次只能被一个线程占用。
  2. 占有且等待:一个线程占有一个资源,同时等待其他资源。
  3. 不可剥夺:线程所持有的资源不能被强制剥夺。
  4. 循环等待:两个或多个线程形成一种循环等待关系。

解决死锁的策略

  •     避免锁的循环等待:通过统一的锁顺序,确保线程不会互相等待对方的资源
  •     使用超时机制:锁请求可以设置超时时间,防止无限等待
  •     使用NSRecursiveLock(递归锁):允许同一线程多次获取同一把锁,避免递归调用中的死锁问题

参考:

iOS - 线程中常见的几种锁_unlock tryluck-CSDN博客

谈谈 swift 中的线程安全 - 知乎 (zhihu.com)

讲讲 iOS 中的死锁 (qq.com)

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

文档翻译软件哪个好用?高效翻译看这里

文档翻译对打工人来说是件很头疼的事情吧?不仅是因为复杂的专业词汇,还因为不同语言之间的表达方式差异,使得翻译工作变得异常繁琐。 不过,幸运的是,现在有许多在线工具可以帮助我们轻松地翻译文档。 这些工具大多数…

【Linux】ubuntu 16.04 搭建jdk 11 环境(亲测可用)

目录 0.环境 1.题外话 2.详细 0.环境 windows11 主机 Virtual Box 7.0 ubuntu 16.04系统 想搭建个 jdk11的环境,用于项目 1.题外话 因为虚拟机与主机传输文件不方便,所以可以尝试用共享文件夹的方式传输,亲测可用,参考以下博…

C# 游戏引擎中的协程

前言 书接上回,我谈到了Unity中的协程的重要性,虽然协程不是游戏开发“必要的”,但是它可以在很多地方发挥优势。 为了在Godot找回熟悉的Unity协程开发手感,不得不自己做一个协程系统,幸运的是,有了Unity的…

探索顶级低代码开发平台,实现创新

文章盘点ZohoCreator、OutSystems等10款顶尖低代码开发平台,各平台以快速开发、集成、数据安全等为主要特点,适用于不同企业需求,助力数字化转型。 一、Zoho Creator Zoho Creator 是一个低代码开发平台,它简化了应用开发中的复杂…

PK过Google、Facebook,YouTube竟然是外贸引流营销的新前景

在如今的外贸行业中,广告投放已经成为商家吸引客户和提高销量的重要工具。众所周知,Facebook和谷歌是广告投放的两大巨头平台。这两者以其强大的用户基数和广告精准性在市场上占据主导地位。然而,随着互联网的发展和消费趋势的改变&#xff0…

MongoDB 工具包安装(mongodb-database-tools)

首先到官网下载工具包,进入下面页面,复制连接地址,使用wget下载 cd /usr/local/mongodb5.0.14/wget https://fastdl.mongodb.org/tools/db/mongodb-database-tools-rhel70-x86_64-100.6.1.tgz 安装 tar -zxvf mongodb-database-tools-rhel70-…

《北方牧业》是什么级别的期刊?是正规期刊吗?能评职称吗?

问题解答 问:《中国动物检疫》是不是核心期刊? 答:不是,是知网收录的正规学术期刊。 问:《中国动物检疫》级别? 答:省级。主管单位:河北省畜牧局 主办单…

【rust】 基于rust编写wasm,实现markdown转换为html文本

文章目录 背景转换预览核心代码前置依赖rustup换源 cargo本地路径修改(可选)cargo换源中科大 wasm-pack安装 背景 尝试用rust编写一款markdown转html的插件,通过wasm给html使用,不得不说体积挺小,约200K, …

spring cloud Gateway网关

网关是将所有面向用户的服务接口统一管理的代理服务器,所有内部服务的远程调用都是在局域网内部,而网关是在公网中。 一、依赖 通过访问网关调用项目中的服务,需要使用Eureka,网关服务器需要在Eureka服务注册它自己,本…

Clipboard.js实现复制文本到剪贴板功能

一、Clipboard.js简介 Clipboard.js是一个轻量级的实现复制文本到剪贴板功能的JavaScript插件,该插件可以将输入框,文本域,DOM节点元素中的文本内容复制到剪贴板中。 官网地址:Clipboard.js 浏览器兼容性:兼容Chrome、…

Ubuntu环境切换到服务器某个用户后source等命令和Tab快捷补全都用不了了,提示没找到,但root用户可以

以escs用户为例: 输入以下命令 grep root /etc/passwd grep escs /etc/passwd 对比发现,root用户配的是bash,而escs却是sh, 所以把escs的sh改成和root一样的bash,命令为 usermod -s /bin/bash escs 改好后就可以了。 …

VSCODE驯服日记(三):配置C++环境

1. 下载mingw64,解压后把bin并添加到环境变量 1>编译器介绍 mingw:专为windowsgcc:多平台msvc :windows,且配合vs使用更佳 注意与调试器gdb和lldb的区别 2. 安装vscode插件: 安装C/C插件 安装code ru…

测试管理新增视图与高级搜索功能,测试计划支持一键生成缺陷详情,MeterSphere开源持续测试工具v3.3版本发布

2024年9月29日,MeterSphere开源持续测试工具正式发布v3.3版本。 在这一版本中,接口测试方面,接口导入功能支持导入Postman、JMX、HAR和MeterSphere格式的文件,接口场景的自定义请求步骤支持cURL快捷导入;测试管理方面…

基于趋近律的滑模控制器设计、仿真(S-function)

目录 一、什么是滑模控制?1. 滑模面2. 控制策略3. 抗干扰和鲁棒性4. 滑模控制的应用 二、什么是趋近律?1. 趋近律三大设计目标2. 常见的趋近律形式1. 等速趋近律2. 指数趋近律3. 幂次趋近律 三、滑模控制器设计四、滑模仿真示例1. Simulink仿真框图2. 不同k值下的仿…

MAC如何获取文件数字签名和进程名称

1、安装需要查看数字签名和进程名称的软件包 2、打开终端命令行(Terminal) 3、查找数字签名 在终端命令行中输入: codesign -dvv 安装的软件路径 2>&1 | grep "Authority" | head -n 1 | cut -d -f2”回显即为进程的数…

kafka发送消费核心参数与设计原理详解

核心参数详解: 发送端参数: 发送方式:默认值一般都是1: 重试参数 : 批量参数: 消费端参数: 自动提交参数: 如果是false,就是说消费完后不提交位移。也就是说比如之前消费的1-5.第N次还是消费到1-5.如果是false。那么第一次消费1-3,第二次消费4-5:默认是true:我们…

HCIP和HCIE有什么区别呢?

HCIP和HCIE有什么区别呢?今天给大家介绍下两者的不同 ‌认证层次‌:HCIE屹立于华为认证体系的顶端,定位为专家级认证;而HCIP则位于中坚位置,属于中级认证。 难度与专业要求‌:通往HCIE之路布满挑战&…

机器学习实战26-一种基于LightGBM的股市涨跌预测系统与代码实现过程

大家好,我是微学AI,今天给大家介绍一下机器学习实战26-一种基于LightGBM的股市涨跌预测系统与代码实现过程。文章首先阐述了项目背景,随后详细解释了LightGBM模型的原理及其在股市预测中的应用。通过选取具有代表性的样例股票数据集&#xff…

深入浅出MongoDB(三)

深入浅出MongoDB(三) 文章目录 深入浅出MongoDB(三)复制副本集设置分片分片实例备份与恢复监控ObjectId 复制 复制时将数据同步在多个服务器的过程,提供了数据的冗余备份,在多个服务器上存储数据副本&#…

视频压缩怎么压缩更小?分享释放视频占用空间的小技巧

想问问同样在做视频自媒体博主的你,都是怎么解决占满内存的视频素材的? 是有听说有一部分博主会把录制好的,还未使用的视频素材,用工具压缩他们的体积,以减少占用内存空间。 小编也有试过,发现视频的体积是…