iOS——线程安全、线程同步与线程通信

news2024/12/24 20:36:12

线程安全和线程同步

  • 线程安全:如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作(更改变量),一般都需要考虑线程同步,否则的话就可能影响线程安全。

  • 线程同步:可理解为线程 A 和 线程 B 一块配合,A 执行到一定程度时要依靠线程 B 的某个结果,于是停下来,示意 B 运行;B 依言执行,再将结果给 A;A 再继续操作。

多线程的安全隐患

  • 资源共享
    1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源
    比如多个线程访问同一个对象、同一个变量、同一个文件

  • 当多个线程访问同一块资源时,很容易引发数据错乱和数据安全问题

例子:
如以下的抢票代码:

@interface ViewController ()
@property (assign, nonatomic) int ticketsCount;
@end

@implementation ViewController

- (void) viewDidLoad {
    [super viewDidLoad];
    
    self.ticketsCount = 100;
    [self test];
}

- (void) test {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_async(queue, ^{
        for (int i = 0; i < 100; i++) {
            [NSThread sleepForTimeInterval:0.1];
            [self cunTicket];
        }
    });
    
    dispatch_async(queue, ^{
        for (int i = 0; i < 100; i++) {
            [NSThread sleepForTimeInterval:0.1];
            [self quTicket];
        }
    });
}

- (void) cunTicket {
    int oldTicket = self.ticketsCount;
    self.ticketsCount = oldTicket + 1;
    NSLog(@"加1票:%d", self.ticketsCount);
}

- (void) quTicket {
    int oldTicket = self.ticketsCount;
    self.ticketsCount = oldTicket - 1;
    NSLog(@"减1票:%d", self.ticketsCount);
}

@end

执行结果:
请添加图片描述

数据发生了错误

多线程安全隐患分析

请添加图片描述

  • 解决方案:使用线程同步技术(同步,就是协同步调,按预定的先后次序进行)
  • 常见的线程同步技术是:加锁

请添加图片描述

线程A访问Integer的时候先加锁,访问完成后解锁
在线程B访问加锁,完成后解锁。

线程同步

iOS中的线程同步方案

  • OSSpinLock
  • os_unfair_lock
  • pthread_mutex
  • dispatch_semaphore
  • dispatch_queue(DISPATCH_QUEUE_SERIAL) //串行队列
  • NSLock
  • NSRecursiveLock
  • NSCondition
  • NSConditionLock
  • @synchronized

OSSpinLock(自旋锁)

自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是 否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名。

虽然它的效率比互斥锁高,但是它也有些不足之处:

  • 自旋锁的等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源。如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁。
  • 使用自旋锁不容易解决优先级反转问题。
  • 在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。

自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。

优先级反转问题

例子:
请添加图片描述

线程A在一个比较低的优先级上工作, 假设是10吧。然后在时间点T1的时候,线程A锁定了一把互斥锁,并开始操作互斥数据。
这时有个高优线级线程C(比如优先级20)在时间点T2被唤醒,它也也需要操作互斥数据。当它加锁互斥锁时,因为互斥锁在T1被线程A锁掉了,所以线程C放弃CPU进入阻塞状态,而线程A得以占据CPU,继续执行。
事情到这一步还是正确的,虽然优先级10的A线程看上去抢了优先级20的C线程的时间,但因为程序逻辑,C确实需要退出CPU等完成互斥数据操作后,才能获得CPU。
但是,假设我们有个线程B在优先级15上,在T3时间点上醒了过来,因为他比当前执行的线程A优先级高,所以它会立即抢占CPU。而线程A被迫进入READY状态等待。
一直到时间点T4,线程B放弃CPU,这时优先级10的线程A是唯一READY线程,它再次占据CPU继续执行,最后在T5解锁了互斥锁。
在T5,线程A解锁的瞬间,线程C立即获取互斥锁,并在优先级20上等待CPU。因为它比线程A的优先级高,系统立刻调度线程C执行,而线程A再次进入READY状态。

上面这个时序里,线程B从T3到T4占据CPU运行的行为,就是事实上的优先级反转。一个优先级15的线程B,通过压制优线级10的线程A,而事实上导致高优先级线程C无法正确得到CPU。这段时间是不可控的,因为线程B可以长时间占据CPU(即使轮转时间片到时,线程A和B都处于可执行态,但是因为B的优先级高,它依然可以占据CPU),其结果就是高优先级线程C可能长时间无法得到 CPU。

优先级反转问题和自旋锁

优先级反转问题的出现跟自旋锁没有关系
不使用自旋锁时也可能出现优先级反转问题。只要是线程或任务有多个优先级,理论上就可能有反转问题。操作系统在优先级反转发生时通常都会有自动的解决方案,比如提高低优先级线程的优先级等

在使用iOS中的OSSpinLock自旋锁时,由于这种锁不会记录持有它的线程信息,所有当发生优先级反转时,系统找不到低优先级的线程,可能因此无法通过提高优先级解决优先级反转问题。再加上,高优先级线程使用自旋锁进行轮训等待锁时在一直占用CPU时间片,使得低优先级线程拿到时间片的概率降低

总结下来是

  • 优先级反转问题的出现跟自旋锁没有关系
  • 但一旦出现优先级反转问题,自旋锁会让优先级反转问题不容易解决,甚至造成更严重的线程等待问题

互斥锁

互斥锁和自旋锁的区别是,当线程获取锁操作失败的时候,线程会进入睡眠,即它是sleep-waiting,而自旋锁是会使线程进入忙等busy-waiting。如果线程是使用pthread_spin_lock操作去请求自旋锁,那么线程就会一直进行忙等待并不停的进行锁请求,直到得到这个锁为止。

互斥锁用于临界区持锁时间比较长的操作,比如下面这些情况都可以考虑

  1. 临界区有IO操作
  2. 临界区代码复杂或者循环量大
  3. 临界区竞争非常激烈
  4. 单核处理器

两种锁的加锁原理

自旋锁:线程一直是running(加锁——>解锁),死循环检测锁的标志位,机制不复杂。
互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。

对比 :互斥锁的起始原始开销要高于自旋锁,但是基本是一劳永逸,临界区持锁时间的大小并不会对互斥锁的开销造成影响,而自旋锁是死循环检测,加锁全程消耗cpu,起始开销虽然低于互斥锁,但是随着持锁时间,加锁的开销是线性增长。

两种锁的使用

自旋锁的使用

在使用自旋锁之前,需要导入头文件#import<libkern/OSAtomic.h>

//初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
//加锁
OSSpinLockLock(&lock);
//解锁
OSSpinLockUnlock(&lock);

互斥锁的使用

OSSpinLock在iOS10.0以后就被弃用了,可以使用os_unfair_lock替代:
需要导入头文件#import <os/lock.h>:

//初始化
os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;
//加锁
os_unfair_lock_lock(&lock);
//解锁
os_unfair_lock_unlock(&lock);

pthread_mutex
mutex叫做”互斥”,等待锁的线程会处于休眠状态。需要导入头文件#import <pthread.h>

1、初始化锁的属性
//声明一个互斥锁属性变量
pthread_mutexattr_t attr;
//初始化该属性变量,使其包含默认值
pthread_mutexattr_init(&attr);
//将互斥锁属性设置为递归锁类型
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);

/*
* Mutex type attributes
*/

#define PTHREAD_MUTEX_NORMAL        0   // 普通锁
#define PTHREAD_MUTEX_ERRORCHECK    1   // 错误检查锁
#define PTHREAD_MUTEX_RECURSIVE     2   // 递归锁
#define PTHREAD_MUTEX_DEFAULT       PTHREAD_MUTEX_NORMAL // 默认锁类型

2、初始化锁
pthread_mutex_init(mutex, &attr);

3、初始化锁结束以后,销毁属性
pthread_mutexattr_destroy(&attr);

4、加锁解锁
pthread_mutex_lock(&_mutex);
pthread_mutex_unlock(&_mutex);

5// 初始化条件
pthread_cond_t condition
pthread_cond_init(&_cond, NULL);

6// 等待条件
pthread_cond_wait(&_cond, &_mutex);

7//激活一个等待该条件的线程
pthread_cond_signal(&_cond);
//激活所有等待该条件的线程
pthread_cond_broadcast(&_cond);

8//销毁资源
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_cond);

备注:我们可以不初始化属性,在传属性的时候直接传NULL,表示使用默认属性PTHREAD_MUTEX_NORMAL。pthread_mutex_init(mutex, NULL);

PTHREAD_MUTEX_RECURSIVE递归锁:允许同一个线程同一把锁进行重复加锁。要看着重点同一个线程和同一把锁

NSLock:
NSLock是对mutex普通锁的封装。pthread_mutex_init(mutex, NULL);
NSLock 遵循 NSLocking 协议。Lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO

其他实现同步的方法

NSRecursiveLock

NSRecursiveLock是对mutex递归锁的封装,API跟NSLock基本一致

NSCondition

NSCondition是对mutex和cond的封装,更加面向对象,我们使用起来也更加的方便简洁

NSConditionLock

NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值

@interface NSConditionLock : NSObject <NSLocking>

// 初始化方法,根据给定的条件初始化锁
- (instancetype)initWithCondition:(NSInteger)condition;
// 只读属性,返回当前锁的条件
@property (readonly) NSInteger condition;
// 当条件满足时锁定
- (void)lockWhenCondition:(NSInteger)condition;
// 尝试锁定,如果锁已经被其他线程占用则返回 NO
- (BOOL)tryLock;
// 当条件满足时尝试锁定,如果锁已经被其他线程占用或条件不满足则返回 NO
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
// 解锁并设置新的条件
- (void)unlockWithCondition:(NSInteger)condition;
// 在指定的时间之前尝试锁定,如果成功返回 YES,否则返回 NO
- (BOOL)lockBeforeDate:(NSDate *)limit;
// 在指定的时间之前当条件满足时尝试锁定,如果成功返回 YES,否则返回 NO
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
// 锁的名称,可用于调试
@property (nullable, copy) NSString *name;

@end

dispatch_semaphore

semaphore叫做”信号量”
信号量的初始值,可以用来控制线程并发访问的最大数量
信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步

dispatch_semaphore_create(5);
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);

dispatch_queue

直接使用GCD的串行队列,也是可以实现线程同步的

dispatch_queue_t queue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);

dispatch_sync(queue, ^{
    // 追加任务1
    for (int i = 0; i < 2; ++i) {
        NSLog(@"1---%@",[NSThread currentThread]);
    }
});

dispatch_sync(queue, ^{
    // 追加任务2
    for (int i = 0; i < 2; ++i) {
        NSLog(@"2---%@",[NSThread currentThread]);
    }
});

@synchronized

@synchronized是对mutex递归锁的封装,
@synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作
使用例子:

//假设有段使用锁的方法是这样写的
- (void)push:(id)element
{
    [_lock lock];
    [_elements addObject:element];
    [_lock unlock];
}

它等同于这样使用@synchronized:

- (void)increment
{
    @synchronized (self) {
        [_elements addObject:element];
    }
}

你可以给任何 Objective-C 对象上加个 @synchronized。那么我们也可以在上面的例子中用 @synchronized(_elements) 来替代 @synchronized(self),效果是相同的。

@synchronized底层实现

@synchronized block 会变成 objc_sync_enter objc_sync_exit 的成对儿调用。

// 定义 SyncData 结构体
typedef struct SyncData {
    //我们传给 @synchronized 的对象
    id object;
    //与 object 关联的递归锁                     
    recursive_mutex_t mutex;    
    //指向下一个 SyncData 对象的指针,形成一个链表结构    
    struct SyncData* nextData;   
    //当前使用或等待这个 SyncData 对象锁的线程数量   
    int threadCount;                
} SyncData;

// 定义 SyncList 结构体
typedef struct SyncList {
  //指向 SyncData 链表头部的指针
    SyncData *data;                
    //一个自旋锁,用于防止多个线程并发修改这个列表 
    spinlock_t lock;                
} SyncList;

// 定义宏和全局变量
 //定义了数组的大小(16)
#define COUNT 16 

// HASH(obj) 是一个哈希函数,将对象的指针地址转换为数组的索引。这个哈希函数通过右移对象指针五位,再与 15(即 COUNT - 1)进行按位与操作来计算索引。
#define HASH(obj) ((((uintptr_t)(obj)) >> 5) & (COUNT - 1))

// LOCK_FOR_OBJ(obj) 宏获取与对象关联的锁
#define LOCK_FOR_OBJ(obj) sDataLists[HASH(obj)].lock 

// LIST_FOR_OBJ(obj) 宏获取与对象关联的 SyncData 链表头
#define LIST_FOR_OBJ(obj) sDataLists[HASH(obj)].data 

//SyncList结构体数组,大小为 16
static SyncList sDataLists[COUNT];                 

一开始,我们有一个 struct SyncData 的定义。这个结构体包含一个 object(即我们传给 @synchronized 的对象)和一个关联的 recursive_mutex_t,它就是那个跟 object 关联在一起的锁。每个 SyncData 也包含一个指向另一个 SyncData 对象的指针,叫做 nextData,所以你可以把每个 SyncData 结构体看作是链表中的一个元素。最后,每个 SyncData 包含一个 threadCount,这个 SyncData 对象中的锁会被一些线程使用或等待,threadCount 就是此时这些线程的数量。它很有用处,因为 SyncData 结构体会被缓存,threadCount==0 就暗示了这个 SyncData 实例可以被复用。

下面是 struct SyncList 的定义。正如我在上面提过,你可以把 SyncData 当做是链表中的节点。每个 SyncList 结构体都有个指向 SyncData 节点链表头部的指针,也有一个用于防止多个线程对此列表做并发修改的锁。

上面代码块的最后一行是 sDataLists 的声明 - 一个 SyncList 结构体数组,大小为 16。通过定义的一个哈希算法将传入对象映射到数组上的一个下标。值得注意的是这个哈希算法设计得很巧妙,是将对象指针在内存的地址转化为无符号整型并右移五位,再跟 0xF 做按位与运算,这样结果不会超出数组大小。LOCK_FOR_OBJ(obj)LIST_FOR_OBJ(obj) 这两个宏就更好理解了,先是哈希出对象的数组下标,然后取出数组对应元素的 lockdata。一切都是这么顺理成章哈。

当你调用 objc_sync_enter(obj) 时,它用 obj 内存地址的哈希值查找合适的 SyncData,然后将其上锁。当你调用 objc_sync_exit(obj) 时,它查找合适的 SyncData 并将其解锁。

源码:

BOOL objc_sync_try_enter(id obj)
{
    BOOL result = YES;

    if (obj) {
        SyncData* data = id2data(obj, SyncKind::atSynchronize, ACQUIRE);
        ASSERT(data);
        result = data->mutex.tryLock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
        if (DebugNilSync == Fatal)
            _objc_fatal("@synchronized(nil) is fatal");
    }

    return result;
}


// End synchronizing on 'obj'. 
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = _objc_sync_exit_kind(obj, SyncKind::atSynchronize);
    if (result != OBJC_SYNC_SUCCESS)
        OBJC_DEBUG_OPTION_REPORT_ERROR(DebugSyncErrors,
            "objc_sync_exit(%p) returned error %d", obj, result);
    return result;
}

当一个对象在 @synchronized block 当中被释放或设为 nil 时会发生什么
会注意到 objc_sync_enter 里面没有 retain 和 release。所以它要么没有保持传递给它的对象,要么或是在 ARC 下被编译。我们可以用下面的代码来做个测试:

NSDate *test = [NSDate date];
//打印 test 的 retainCount,它应当是 1。
NSLog(@"%@", @([test retainCount]));

@synchronized (test) {
  //进入 @synchronized 块,并打印 test 的 retainCount。
//如果 @synchronized 对 test 进行了 retain,retainCount 应为 2。否则,仍为 1。
    NSLog(@"%@", @([test retainCount]));
}

运行结果显示两次输出都是 1,这表明 @synchronized 没有对传入的对象进行 retain 操作。
@synchronized 块中没有 retain 操作,对象在同步块内被释放后,其内存地址可能被重用。这会导致其他线程在同步同一地址的对象时发生阻塞。
但是这是设计者预见并接受的情况,因为在内存地址相同的情况下,阻塞等待确保了同步块的正确性。添加 retain 操作可能会增加复杂性和性能开销,但没有特别好的替代方案来避免此问题。

假如对象在 “synchronized block” 中被设成 nil 呢

NSString *test = @"test";
@try {
    //给 test 分配一个锁并锁定它。
    objc_sync_enter(test);
    //然后将test设为 nil。
    test = nil;
} @finally {
    //调用 objc_sync_exit(test)。因为 test 已被设为 nil,传递给 objc_sync_exit 的是 nil,这会导致锁不会被解锁或释放。
    objc_sync_exit(test);   
}

在 @synchronized 块中将对象设为 nil 的潜在问题:传递 nil 给objc_sync_exit是一个空操作,导致锁不会被释放。

在 @synchronized 块中将对象设为 nil 不会导致死锁,因为编译器确保 objc_sync_enter 和 objc_sync_exit 传入的是相同对象。
如果传递给 @synchronized 的对象为 nil,不会获得任何锁,代码不会是线程安全的。

这个机制确保了在 @synchronized 块中将对象设为 nil 时不会导致锁未释放的问题。
验证了 @synchronized 在这种情况下的正确性,确保了多线程环境下的线程安全性。

  • 你调用 sychronized 的每个对象,Objective-C runtime 都会为其分配一个递归锁并存储在哈希表中。
  • 如果在 sychronized 内部对象被释放或被设为 nil 看起来都 OK。不过这没在文档中说明,所以我不会再生产代码中依赖这条。
  • 注意不要向你的 sychronized block 传入 nil!这将会从代码中移走线程安全。你可以通过在 objc_sync_nil 上加断点来查看是否发生了这样的事情。
@synchronized(id)参数问题

有如下代码:

- (void)lg_crash{
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (_testArray) {
                _testArray = [NSMutableArray array];
            }
        });
    }
}

_testArray在某个时刻可能会变为nil,而当_testArray变为nil时,多线程下的操作会导致@synchronized传入的参数为nil而调用objc_sync_nil函数,也就没有了加锁的效果了,从而导致崩溃的出现。
所以我们在使用@synchronized确保多线程数据安全的时候,要保证传入的绝对不能如上例子为nil的情况。修改成如下则不会再有问题:

- (void)lg_crash{
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (self) {
                _testArray = [NSMutableArray array];
            }
        });
    }
}

atomic

  • atomic用于保证属性setter、getter的原子性操作,相当于在getter和setter内部加了线程同步的锁
  • 它并不能保证使用属性的过程是线程安全的, 只能保证存取值的完整性

pthread_rwlock:读写锁

pthread_rwlock经常用于文件等数据的读写操作,需要导入头文件#import <pthread.h>
iOS中的读写安全方案需要注意一下场景:(多读单写)

  1. 同一时间,只能有1个线程进行写的操作
  2. 同一时间,允许有多个线程进行读的操作
  3. 同一时间,不允许既有写的操作,又有读的操作

锁的性能比较

性能从高到低排序

  1. os_unfair_lock
  2. OSSpinLock
  3. dispatch_semaphore
  4. pthread_mutex
  5. dispatch_queue(DISPATCH_QUEUE_SERIAL)
  6. NSLock
  7. NSCondition
  8. pthread_mutex(recursive)
  9. NSRecursiveLock
  10. NSConditionLock
  11. @synchronized

补充

CPU抢占

CPU 由操作系统统一进行分配,每个进程根据进程优先级的高低都有机会得到 CPU,但是,如果运行时间超过了一定的时间,操作系统会暂停该进程,将 CPU 资源分配给其他等待运行的进程。这种 CPU 的分配方式即所谓的** 抢占式**,操作系统可以强制剥夺 CPU 资源并且分配给它认为目前最需要的进程,如果操作系统分配给每个进程的时间都很短,即 CPU 在多个进程间快速的切换,从而造成了很多进程在同时运行的假象,这个就是并行的实质。

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

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

相关文章

18055 主对角线上的元素之和

### 思路 1. 输入一个3行4列的整数矩阵。 2. 计算主对角线上的元素之和。 3. 输出主对角线上的元素之和。 ### 伪代码 1. 初始化一个3行4列的矩阵 matrix。 2. 输入矩阵的元素。 3. 初始化一个变量 sum 为0&#xff0c;用于存储主对角线元素之和。 4. 遍历矩阵的行&#xff0c…

【Day08-IO-文件字节流】

File 1. 概述 File对象既可以代表文件、也可以代表文件夹。它封装的对象仅仅是一个路径名&#xff0c;这个路径可以存在&#xff0c;也可以不存在 构造器 说明 public File​(String pathname) 根据文件路径创建文件对象 public File​(String parent, String child) 根据…

vscode中使用go环境配置细节

1、在docker容器中下载了go的sdk 2、在/etc/profile.d/go.sh里填入如下内容: #!/bin/bashexport GOROOT=/home/ud_dev/go export PATH=$GOROOT/bin:$PATH 3、设置go env go env -w GOPROXY=https://goproxy.cn,direct go env -w GO111MODULE=on 4、重启这个容器,使得vscod…

DBAPI如何使用内存缓存

背景 在使用DBAPI创建API的时候&#xff0c;有时候SQL查询比较耗时&#xff0c;如果业务上对数据时效性要求不高&#xff0c;这种耗时的SQL可以使用缓存插件来将数据缓存起来&#xff0c;避免重复查询。 一般来说&#xff0c;可以使用redis memcache等缓存服务来存储缓存数据。…

活动|华院计算宣晓华受邀出席“AI引领新工业革命”大会,探讨全球科技的最新趋势

8月31日&#xff0c;“AI引领新工业革命”大会于上海图书馆圆满落幕。本次大会由TAA校联会和台协科创工委会联合主办&#xff0c;得到上海市台办、上海市台联、康师傅的大力支持。大会邀请了NVIDIA全球副总裁、亚太区企业营销负责人刘念宁&#xff0c;元禾厚望资本创始合伙人潘…

ispunct函数讲解 <ctype.h>头文件函数

目录 1.头文件函数 2.ispunct函数使用 小心&#xff01;VS2022不可直接接触&#xff0c;否则..!没有这个必要&#xff0c;方源一把抓住VS2022&#xff0c;顷刻 炼化&#xff01; 1.头文件函数 以上函数都需要包括头文件<ctype.h> &#xff0c;其中包括 ispunct 函数 #…

esp8266+sg90实现远程开关灯(接线问题)

1需要准备的设备 首先需要的设备 硬件&#xff1a;esp8266开发板和sg90舵机&#xff0c;还有公对母的杜邦线&#xff0c;以及一根usb程序下载线。 软件&#xff1a;Arduino IDE 因为sg90舵机接口是三个连着的&#xff0c;只能用公对母的杜邦线把三条信号线接到esp8266的不同引…

Linux驱动.之字符设备驱动框架,新内核框架,设备树(二)

第一篇比较长&#xff0c;第二篇&#xff0c;继续写&#xff0c;内容有重复 一、字符设备驱动框架 在用户空间中调用open&#xff0c;打开一个字符设备&#xff0c;执行流程如下&#xff1a;最终会执行chrdev中的ops对应的open函数。

【python计算机视觉编程——8.图像内容分类】

python计算机视觉编程——8.图像内容分类 8.图像内容分类8.1 K邻近分类法&#xff08;KNN&#xff09;8.1.1 一个简单的二维示例8.1.2 用稠密SIFT作为图像特征8.1.3 图像分类:手势识别 8.2贝叶斯分类器用PCA降维 8.3 支持向量机8.3.2 再论手势识别 8.4 光学字符识别8.4.2 选取特…

面试官:你是怎么处理vue项目中的错误的?

一、错误类型 任何一个框架&#xff0c;对于错误的处理都是一种必备的能力 在Vue 中&#xff0c;则是定义了一套对应的错误处理规则给到使用者&#xff0c;且在源代码级别&#xff0c;对部分必要的过程做了一定的错误处理。 主要的错误来源包括&#xff1a; 后端接口错误代…

网络原理之TCP协议(万字详解!!!)

目录 前言 TCP协议段格式 TCP协议相关特性 1.确认应答 2.超时重传 3.连接管理&#xff08;三次握手、四次挥手&#xff09; 三次握手&#xff08;建立TCP连接&#xff09; 四次挥手&#xff08;断开连接&#xff09; 4.滑动窗口 5.流量控制 6.拥塞控制 7.延迟应答…

(入门篇)JavaScript 网页设计案例浅析-简单的交互式图片轮播

网页设计已经成为了每个前端开发者的必备技能,而 JavaScript 作为前端三大基础之一,更是为网页赋予了互动性和动态效果。本篇文章将通过一个简单的 JavaScript 案例,带你了解网页设计中的一些常见技巧和技术原理。今天就说一说一个常见的图片轮播效果。相信大家在各类电商网…

使用vscode上传git远程仓库流程(Gitee)

目录 参考附件 git远程仓库上传流程 1&#xff0c;先将文件夹用VScode打开 2&#xff0c;第一次进入要初始化一下仓库 3&#xff0c;通过这个&#xff08;.gitignore&#xff09;可以把一些不重要的文件不显示 注&#xff1a;&#xff08;.gitignore中&#xff09;可屏蔽…

AI辅助编程里的 Atom Group 的概念和使用

背景 在我们实际的开发当中&#xff0c;一个需求往往会涉及到多个文件修改&#xff0c;而需求也往往有相似性。 举个例子&#xff0c;我经常需要在 auto-coder中需要添加命令行参数&#xff0c;通常是这样的&#xff1a; /coding 添加一个新的命令行参数 --chat_model 默认值为…

基于RAG和知识库的智能问答系统设计与实现

开局一张图&#xff0c;其余全靠编。 自己画的图&#xff0c;内容是由Claude根据图优化帮忙写的。 1. 引言 在当今数字化时代&#xff0c;智能问答系统已成为提升用户体验和提高信息获取效率的重要工具。随着自然语言处理技术的不断进步&#xff0c;特别是大型语言模型&#x…

Sonarqube 和 Sonar-scanner的安装和配置

SonarQube 简介 所谓sonarqube 就是代码质量扫描工具。 官网&#xff1a; https://www.sonarsource.com/sonarqube/ 在个人开发学习中用处不大&#xff0c; 我草&#xff0c; 我的代码质量这么高需要这玩意&#xff1f; 但是在公司项目中&#xff0c; 这个可是必须的&#x…

【高校主办,EI稳定检索】2024年人机交互与虚拟现实国际会议(HCIVR 2024)

会议简介 2024年人机交互与虚拟现实国际会议&#xff08;HCIVR 2024&#xff09;定于2024年11月15-17日在中国杭州召开&#xff0c;会议由浙江工业大学主办。人机交互&#xff0c;虚拟现实技术的发展趋势主要体现在系统将越来越实际化&#xff0c;也越来越贴近人类的感知和需求…

心觉:第一性原理思考和共情能力,怎么用效果更好

Hi&#xff0c;我是心觉&#xff0c;与你一起玩转潜意识、脑波音乐和吸引力法则&#xff0c;轻松掌控自己的人生&#xff01; 挑战每日一省写作163/1000天 我很佩服逻辑能力很强的人 也很佩服共情能力很强的人 他们都很厉害 我自己感觉逻辑能力更强一点&#xff0c;平时喜欢…

strlen函数模拟实现(嵌套函数调用)

目录 1.模拟实现strlen函数代码&#xff08;嵌套函数&#xff09; 2.代码解释 小心&#xff01;VS2022不可直接接触&#xff0c;否则..!没有这个必要&#xff0c;方源一把抓住VS2022&#xff0c;顷刻 炼化&#xff01; 1.模拟实现strlen函数代码&#xff08;嵌套函数&#x…

从零开始写论文:如何借助ChatGPT生成完美摘要?

AIPaperGPT&#xff0c;论文写作神器~ https://www.aipapergpt.com/ 在写论文的过程中&#xff0c;摘要是一个非常重要的部分&#xff0c;它能够帮助读者快速理解论文的核心内容&#xff0c;决定是否进一步阅读全文。但是许多学生在写摘要的时候常常感到困惑&#xff0c;不知…