【iOS开发】—— 初识锁

news2024/9/21 4:34:32

【iOS开发】—— 初识锁

  • 线程安全
  • 锁的种类
    • 自旋锁
      • 定义
      • 原理
      • 自旋锁缺点
      • OSSpinLock(自旋锁)
    • 互斥锁
      • os_unfair_lock
      • pthread_mutex
      • NSLock
      • NSRecusiveLock
      • Semaphore信号量
      • @synchronized
  • 总结
    • 两种之间的区别和联系:

线程安全

当一个线程访问数据的时候,其他的线程不能对其进行访问,直到该线程访问完毕。简单来讲就是在同一时刻,对同一个数据操作的线程只有一个。 而线程不安全,则是在同一时刻可以有多个线程对该数据进行访问,从而得不到预期的结果。 在iOS中, UIKit是绝对线程安全的,因为UIKit都是在主线程操作的,单线程没有线程当然没有线程安全问题,但除此之外,其他都要考虑线程安全问题

iOS解决线程安全的途径其原理大同小异,都是通过锁来使关键代码保证同步执行,从而确保线程安全性,这一点和多线程的异步执行任务是不冲突的。

注: 不要将过多的其他操作代码放到锁里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了

下方我们就详细讲解iOS相关锁,本博客采用一个经典的售票例子:

此处展示的是不加锁(即不考虑线程安全)的情况:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController
@property (nonatomic, assign) NSInteger ticketCount;

@end

#import "ViewController.h"

@interface ViewController ()

@end
int cnt = 0;
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.ticketCount = 50;
    __weak typeof (self) weakSelf = self;
    
    //一号售卖口
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [weakSelf saleTick];
    });
    
    //二号售卖口
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [weakSelf saleTick];
    });
}

- (void)saleTick {
    while (1) {
        if (self.ticketCount > 0) {
            self.ticketCount--;
            cnt++;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
            
        } else {
            NSLog(@"所有车票已售完,共计售出%d张", cnt);
            break;
        }
    }
} 
@end

运行结果的部分截图:
在这里插入图片描述
从上图可以发现,输出的顺序是乱序的,而且还显示卖出了54张票。对于上述情况,我们就可以通过加锁来实现修正错误问题。

锁的种类

iOS中的锁有两大类:自旋锁、互斥锁。

自旋锁

定义

自旋锁是一种同步机制,用于在多线程环境中保护共享资源的访问。它通过循环忙等待的方式,而不是阻塞线程,来实现对共享资源的互斥访问。

原理

线程一直是running(加锁——>解锁),死循环检测锁的标志位,机制不复杂。

自旋锁缺点

  • 调用者在未获得锁的情况下,一直运行--自旋,所以占用着CPU资源,如果不能在很短的时间内获得锁,会使CPU效率降低。所以自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下。
  • 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁。

OSSpinLock(自旋锁)

OSSpinLock是在libkern库中,使用之前需要引入头文件<libkern/OSAtomic.h>,使用时会出现警告⚠️。
在这里插入图片描述
这是因为OSSpinLock存在缺陷,从iOS10开始已经不建议使用了。官方建议使用os_unfair_lock来替代。
下面是使用os_unfair_lock的实例:

// 初始化
spinLock = OS_SPINKLOCK_INIT;
// 加锁
OSSpinLockLock(&spinLock);
// 解锁
OSSpinLockUnlock(&spinLock);

#import "ViewController.h"

#import <os/lock.h>

@interface ViewController ()

@property (nonatomic, assign) os_unfair_lock spinLock;
@end

- (void)saleTick {
    while (1) {
         // 加锁
        OSSpinLockLock(&_spinLock);
        if (self.ticketCount > 0) {
            self.ticketCount--;
            cnt++;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
            
        } else {
            NSLog(@"所有车票已售完,共计售出%d张", cnt);
            break;
        }
        // 解锁
        OSSpinLockUnlock(&_spinLock);
    }
} 
@end

运行结果:
在这里插入图片描述
结果就是按照顺序非常规范地卖出了这50张票。
刚才提到了OSSpinLock存在缺陷,其实它的缺陷主要存在两点:

  • OSSpinLock不会记录持有它的线程信息,当发生优先级反转的时候,系统找不到低优先级的线程,导致系统可能无法通过提高优先级解决优先级反转问题
  • 高优先级线程使用自旋锁忙等待的时候一直在占用CPU时间片,导致低优先级线程拿到时间片的概率降低。

值得注意的是: 自旋锁和优先级反转没有关系,但是正因为有上面两点,所以自旋锁会导致优先级反转问题更难解决,甚至造成更为严重的线程等待问题,所以苹果就废除了OSSpinLock,转而推荐人们使用os_unfair_lock来替代,由于os_unfair_lock是一个互斥锁,所以我们将对其的讲解放到互斥锁中去。

互斥锁

保证在任何时候,都只有一个线程访问对象。当获取锁操作失败时,线程会进入睡眠,等待锁释放时被唤醒。
互斥锁原理
线程会从sleep(加锁)——> running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销,所以效率是要低于自旋锁的。

互斥锁分为两种: 递归锁、非递归锁

  • 递归锁:可重入锁,同一个线程在锁释放前可再次获取锁,即可以递归调用。
  • 非递归锁:不可重入,必须等锁释放后才能再次获取锁。

os_unfair_lock

上面讲过现在苹果采用os_unfair_lock来代替不安全的OSSpinLock,且由于os_unfair_lock会休眠而不是忙等,所以属于 互斥锁 ,且是非递归互斥锁,下面来看一下它的用法:

os_unfair_lock 在os库中,使用之前需要导入头文件<os/lock.h>

//创建一个锁
os_unfair_lock unfairLock;
//初始化
unfairLock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&unfairLock);
//解锁
os_unfair_lock_unlock(&unfairLock);

实际使用方法:

- (void)saleTick {
    while (1) {
        //OSSpinLockLock(&_spinklock);
        os_unfair_lock_lock(&_lock);
        if (self.ticketCount > 0) {
            self.ticketCount--;
            cnt++;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
            
        } else {
            NSLog(@"所有车票已售完,共计售出%d张", cnt);
            os_unfair_lock_unlock(&_lock);
            break;
        }
        os_unfair_lock_unlock(&_lock);
        //OSSpinLockUnlock(&_spinklock);
    }
}

运行结果:
在这里插入图片描述
对于它的定义:
这是对于已经废弃的OSSpinkLock的替换,这个函数不会在争用时旋转,而是在内核中等待被解锁唤醒。与OSSpinLock一样,这个函数并不强制公平或锁排序一例如,解锁程序可能会在唤醒的服务程序获得获得锁的机会之前立即重新获得锁。这可能有利于性能的提高,但也可能导致等待者短缺。不是旋转(忙等),而是休眠,等待被唤醒,所以os_unfair_lock理应是互斥锁。

pthread_mutex

pthread_mutex就是 互斥锁 本身——当锁被占用,而其他线程申请锁时,不是使用忙等,而是阻塞线程并睡眠,另外pthread_mutex也是非递归的锁。

使用时我们需要先引用这个头文件:#import <pthread.h>
具体使用如下:

// 全局声明互斥锁
pthread_mutex_t _lock;
// 初始化互斥锁
pthread_mutex_init(&_lock, NULL);
// 加锁
pthread_mutex_lock(&_lock);
// 这里做需要线程安全操作
// ...
// 解锁 
pthread_mutex_unlock(&_lock);
// 释放锁
pthread_mutex_destroy(&_lock);

结果如下:
在这里插入图片描述
结果就是按照顺序非常规范地卖出了这50张票。

NSLock

我们的Foundation框架内部也是有一把NSLock锁的,使用起来非常方便,基于互斥锁pthroad_mutex封装而来,是一把互斥非递归锁。
使用如下:

//初始化NSLock
NSLock *lock = [[NSLock alloc] init];
//加锁
[lock lock];
...
//线程安全执行的代码
...
//解锁
[lock unlock];

实际使用(在卖票例子中):

- (void)saleTick {
    while (1) {
        //OSSpinLockLock(&_spinklock);
        //os_unfair_lock_lock(&_lock);
        [self.lock lock];
        if (self.ticketCount > 0) {
            self.ticketCount--;
            cnt++;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
            
        } else {
            NSLog(@"所有车票已售完,共计售出%d张", cnt);
            //os_unfair_lock_unlock(&_lock);
            [self.lock unlock];
            break;
        }
        
        //OSSpinLockUnlock(&_spinklock);
        //os_unfair_lock_unlock(&_lock);
        [self.lock unlock];
    }
}

运行结果如下:
在这里插入图片描述
结果就是按照顺序非常规范地卖出了这50张票。

如果对非递归锁强行使用递归调用,就会在调用时发生线程阻塞,而并非是死锁,第一次加锁之后还没出锁就进行递归调用,第二次加锁就堵塞了线程。

苹果官方文档的描述如下::
在这里插入图片描述
可以看到在同一线程上调用两次NSLock的lock方法将会永久锁定线程。同时也重点提醒向NSLock对象发生解锁消息时,必须确保消息时从发送初始锁定消息的同一个线程发送的,否则就会产生未知问题。

非递归互斥锁导致线程阻塞的例子:

- (void)saleTickWithNSLock {
    while(1) {
        // 加锁
        [lock lock];
        if (self.ticketSurplusCount > 0) {  // 如果还有票,继续售卖
            self.ticketSurplusCount--;
            cnt++;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { // 如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完,共售出%d张票", cnt);
            // 解锁
            break;
        }
        // 解锁
    }
}

运行结果如下:
在这里插入图片描述
可以看到,因为我们对当前这个线程在执行lock操作后还未unlock的情况下,又进行了NSLock的重复lock加锁操作,所以当前线程发生了阻塞,只进行了一次卖票操作就再不执行其他操作了。

NSRecusiveLock

NSRecursiveLock使用和NSLock类似,不过NSRecursiveLock是递归互斥锁。

//初始化NSLock
NSRecusiveLock *recusiveLock = [[NSRecusiveLock alloc] init];
//加锁
[recusiveLock lock];
...
//线程安全执行的代码
...
//解锁
[recusiveLock unlock];

下面我们举一个NSRecursiveLock递归使用的例子:

#import "ViewController.h"
#import <libkern/OSAtomic.h>
#import <os/lock.h>
@interface ViewController ()

//@property (nonatomic, assign) OSSpinLock spinklock;
//@property (nonatomic, assign) os_unfair_lock lock;
//@property (nonatomic, strong) NSLock* lock;
@property (nonatomic, strong) NSRecursiveLock* recursiveLlock;

@end

int cnt;
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.ticketCount = 50;
        __weak typeof (self) weakSelf = self;
        
    //self.spinklock = OS_SPINLOCK_INIT;
    //self.lock = OS_UNFAIR_LOCK_INIT;
    //self.lock = [[NSLock alloc] init];
    self.recursiveLlock = [[NSRecursiveLock alloc] init];
    
    
//    //一号售卖口
//    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//        [weakSelf saleTick];
//    });
//
//    //二号售卖口
//    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//        [weakSelf saleTick];
//    });
    for (int i = 0; i < 10; ++i) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            [weakSelf saleTick];
        });
    }
    
}

- (void)saleTick {
    while (1) {
        //OSSpinLockLock(&_spinklock);
        //os_unfair_lock_lock(&_lock);
        //[self.lock lock];
        [self.recursiveLlock lock];
        if (self.ticketCount > 0) {
            self.ticketCount--;
            cnt++;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%ld 窗口:%@", self.ticketCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
            
        } else {
            NSLog(@"所有车票已售完,共计售出%d张", cnt);
            //os_unfair_lock_unlock(&_lock);
            //[self.lock unlock];
            [self.recursiveLlock unlock];
            break;
        }
        
        //OSSpinLockUnlock(&_spinklock);
        //os_unfair_lock_unlock(&_lock);
        //[self.lock unlock];
        [self.recursiveLlock unlock];
    }
}

@end

结果如下:
在这里插入图片描述
可以看到向同一个线程多次获取递归锁NSRecusiveLock并不会导致程序死锁,而是正常的线程安全地加锁执行。

苹果官方文档的描述如下:
在这里插入图片描述

Semaphore信号量

Semaphore信号量也可以解决线程安全问题,GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能:计数小于 0 时需要等待,不可通过。计数为 0 或大于 0 时,不用等待可通过。计数大于 0 且计数减 1 时不用等待,可通过。

Dispatch Semaphore 提供了三个方法:

dispatch_semaphore_create://创建一个 Semaphore 并初始化信号的总量
dispatch_semaphore_signal://发送一个信号,让信号总量加 1
dispatch_semaphore_wait://可以使总信号量减 1,信号总量小于 0 时就会一直等待(阻塞所在线程),否则就可以正常执行。

注意:
信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量

Dispatch Semaphore 在实际开发中主要用于:

  • 保持线程同步,将异步执行任务转换为同步执行任务。
  • 保证线程安全,为线程加锁。

@synchronized

@synchronized可能是日常开发中用的比较多的一种递归互斥锁,因为它的使用比较简单,但并不是在任意场景下都能使用@synchronized,且它的性能较低。

使用方法如下:

@synchronized (obj) {}

下面我们来探索一下@synchronized的源码:

  • 通过汇编能发现@synchronized就是实现了objc_sync_enter和 objc_sync_exit两个方法。
  • 通过符号断点能知道这两个方法都是在objc源码中的。
  • 通过clang也能得到一些信息。
#pragma clang assume_nonnull end

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        { id _rethrow = 0; id _sync_obj = (id)__null; objc_sync_enter(_sync_obj);
try {
	struct _SYNC_EXIT { _SYNC_EXIT(id arg) : sync_exit(arg) {}
	~_SYNC_EXIT() {objc_sync_exit(sync_exit);}
	id sync_exit;
	} _sync_exit(_sync_obj);

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_6p_mn3hwpz14_7dg_gr79rtm4n80000gn_T_main_59328a_mi_0);
        } catch (id e) {_rethrow = e;}
{ struct _FIN { _FIN(id reth) : rethrow(reth) {}
	~_FIN() { if (rethrow) objc_exception_throw(rethrow); }
	id rethrow;
	} _fin_force_rethow(_rethrow);}
}

    }
    return 0;
}

总结

两种之间的区别和联系:

1.区别:

  1. 等待机制互斥锁是阻塞锁,当锁被其他线程占用时,请求线程会被阻塞;自旋锁是忙等待锁,请求线程会循环忙等待,不断检查锁的状态。
  2. CPU占用自旋锁是忙等待,当线程持有自旋锁时间较长时,其他等待线程会一直忙等待,浪费CPU资源互斥锁是阻塞,当线程请求锁时,会被阻塞,释放CPU资源给其他线程
  3. 适用场景自旋锁适用于多核心CPU、共享资源占用时间较短的情况;互斥锁适用于共享资源占用时间较长的情况。

2.联系

  1. 保护共享资源:自旋锁和互斥锁都用于保护共享资源,确保多线程环境下对共享资源的访问安全。
  2. 互斥性质:自旋锁和互斥锁都是互斥的,同一时间只能有一个线程持有锁,其他线程必须等待
  3. 锁的操作:自旋锁和互斥锁都具有获取锁和释放锁的操作,线程在获取锁后可以访问共享资源,完成操作后释放锁,让其他线程获取锁。

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

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

相关文章

深入理解WPF的ResourceDictionary

深入理解WPF的ResourceDictionary 介绍 在WPF中&#xff0c;ResourceDictionary用于集中管理和共享资源&#xff08;如样式、模板、颜色等&#xff09;&#xff0c;从而实现资源的重用和统一管理。本文详细介绍了ResourceDictionary的定义、使用和合并方法。 定义和用法 Res…

Android Hanlder 揭密之路- 深入理解异步消息传递机制Looper、Handler、Message三者关系

在Android开发中&#xff0c;Handler作为实现线程间通信的桥梁&#xff0c;扮演着至关重要的角色。无论是在主线程执行UI操作&#xff0c;还是在子线程进行耗时任务&#xff0c;Handler都可以高效地将异步消息分派到对应的线程中执行。 本文将全方位解析Handler的工作原理及实现…

2024数维杯数学建模C题思路代码

2024年数维杯&电工杯思路代码在线文档​https://www.kdocs.cn/l/cdlol5FlRAdE 这道题想要做出好的结果&#xff0c;必须要结合插值法和分布函数来做&#xff0c;主要还是因为勘探点太少&#xff0c;直接用插值法效果不太好&#xff0c;以下是我做的&#xff0c;函数分布可…

Python的while循环

目录 while循环的结构 示例 关键字 break continue while循环的结构 while condition&#xff08;循环条件&#xff09;: # 循环的内容 循环内容的执行与结束需要通过循环条件控制。 在执行循环之前需要设立一个循环条件的初始值&#xff0c;以便while循环体判断循环条件。…

Loongnix系统替换内核操作

Loongnix系统替换内核操作 一、终端下执行命令 sudo apt search linux-image* 返回结果中格式如: linux-image-4.19.0-19-loongson-3 为最新的内核源码。 二、下载内核源码包 sudo apt source linux-image-4.19.0-19-loongson-3 如提示&#xff1a;E: 您必须在 sources.li…

网络安全等级保护的发展历程

1994年国务院147号令第一次提出&#xff0c;计算机信息系统实行安全等级保护&#xff0c;这也预示着等保的起步。 2007年《信息安全等级保护管理办法》的发布之后。是等保在各行业深耕落地的时代。 2.0是等保版本的俗称&#xff0c;不是等级。等保共分为五级&#xff0c;二级…

C#语音播报(通过CoreAudioAPI完成对扬声器的控制)

1&#xff0c;效果&#xff1a; 作用&#xff1a; 可对当前内容&#xff08;例如此例中的重量信息&#xff09;进行语音合成播报 。可设置系统扬声器音量与状态(是否静音),同时根据扬声器状态同步更新当前控件状态与值&#xff0c;实现强制PC扬声器按照指定的音量进行播报&…

Ansible常用变量【上】

转载说明&#xff1a;如果您喜欢这篇文章并打算转载它&#xff0c;请私信作者取得授权。感谢您喜爱本文&#xff0c;请文明转载&#xff0c;谢谢。 在Ansible中会用到很多的变量&#xff0c;Ansible常用变量包括以下几种&#xff1a; 1. 自定义变量——在playbook中用户自定义…

函数重载和函数模板

c语言中函数名字不可重复,但是可以写代码实现 普通的函数重载 这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同和返回值没有关系(因为就像我想调用Add(1,2),Add重载的几个函数仅仅返回值不同,编辑器就不知道去找哪一个,就有歧义了) 情况1-数组 int ave(int*pa,i…

用 Supabase CLI 进行本地开发环境搭建

文章目录 &#xff08;零&#xff09;前言&#xff08;一&#xff09;Supabase CLI&#xff08;1.1&#xff09;安装 Scoop&#xff08;1.2&#xff09;用 Scoop 安装 Supabase CLI &#xff08;二&#xff09;本地项目环境&#xff08;2.1&#xff09;初始化项目&#xff08;2…

【全开源】微凌客洗护小程序FastAdmin+Uniapp(源码搭建/上线/运营/售后/维护更新)

一款基于FastAdminUniapp开发的洗护小程序系统&#xff0c;适用于线上下单到店核销的业务场景&#xff0c;拥有会员卡、优惠券、充值提现、商户管理等功能&#xff0c;提供Uniapp后台无加密源代码。 线上线下融合&#xff1a;微凌客洗护小程序适用于线上下单到店核销的业务场景…

nacos命名空间的配置

给微服务配置namespace 给微服务配置namespace只能通过修改配置来实现。 例如&#xff0c;修改order-service的application.yml文件&#xff1a; spring:cloud:nacos:server-addr: localhost:8848discovery:cluster-name: HZnamespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f…

C语言数据结构 - 选择题集合(二叉树)

一生负气成今日 四海无人对夕阳 目录 树的专辑 树的专辑 1.有n个元素的完全二叉树的深度是&#xff08; &#xff09; A.nlogn B.nlogn1 C.logn D.logn1 答案&#xff1a;D 解析&#xff1a; 设完全二叉树的节点数为 N&#xff0c;高度为 h &#xff0c;高度为 h 时空的结点…

python零基础知识 - 定义列表的三种方式,循环列表索引值

这一小节&#xff0c;我们将从零基础的角度看一下&#xff0c;python都有哪些定义列表的方式&#xff0c;并且循环这个列表的时候&#xff0c;怎么循环&#xff0c;怎么循环他的索引值&#xff0c;怎么拿到的就是元素值。 说完循环&#xff0c;我们会说一说关键的break和contin…

分布式存储故障导致数据库无法启动故障处理---惜分飞

国内xx医院使用了国外医疗行业龙头的pacs系统,由于是一个历史库,存放在分布式存储中,由于存储同时多个节点故障,导致数据库多个文件异常,数据库无法启动,三方维护人员尝试通通过rman归档进行应用日志,结果发现日志有损坏报ORA-00354 ORA-00353,无法记录恢复,希望我们给予支持 M…

AI智能分析高精度烟火算法EasyCVR视频方案助力打造森林防火建设

一、背景 随着夏季的来临&#xff0c;高温、干燥的天气条件使得火灾隐患显著增加&#xff0c;特别是对于广袤的森林地区来说&#xff0c;一旦发生火灾&#xff0c;后果将不堪设想。在这样的背景下&#xff0c;视频汇聚系统EasyCVR视频融合云平台AI智能分析在森林防火中发挥着至…

人脸消费给传统食堂带来的变化

消费的技术基础是脸部识别&#xff0c;脸部识别是基于人的容貌特征信息进行认证的生物特征识别技术&#xff0c;其突出的特征是以非接触方式进行识别&#xff0c;避免个人信息的泄露。 面部识别和指纹识别、掌纹识别、视网膜识别、骨骼识别、心率识别等都是人体生物特征识别技术…

自然资源-城镇开发边界内详细规划编制技术指南解读

自然资源-城镇开发边界内详细规划编制技术指南解读

护眼台灯和普通台灯差别很大吗?专业护眼灯品牌有哪些?

随着科技的不断演进&#xff0c;台灯的设计也日益脱胎换骨&#xff0c;从曾经的笨重造型转变为如今轻盈雅致的外观。它们的功能同样经历了多样化的革新&#xff0c;变得更加人性化和便捷。作为学习、阅读和办公环境中不可或缺的照明工具&#xff0c;台灯所提供的光线舒适度至关…

redis抖动问题导致延迟或者断开的处理方案

目录&#xff1a; 1、使用背景2、redis重试机制3、redis重连机制4、其他一些解决redis抖动问题方案 1、使用背景 客户反馈文件偶现打不开&#xff0c;报错现象是session not exist&#xff0c;最终定位是redis抖动导致的延迟/断开的现象&#xff0c;最终研发团方案是加入redis…