【iOS】线程同步读写安全技术(锁、信号量、同步串行队列)

目录

  • 多线程安全隐患
    • 存钱取钱问题
    • 卖票问题
  • 解决方案
    • 1. 锁
      • 自旋锁
        • OSSpinLock
        • os_unfair_lock
        • atomic
      • 互斥锁
        • pthread_mutex_t
        • 条件pthread_cond_t(线程检查器)
        • NSLock&NSRecursiveLock(递归锁)
        • NSCondition(条件锁)&NSConditionLock
        • @synchronized
    • 2. 信号量
    • 3. 同步串行队列
  • 读写安全
    • 读写锁
      • pthread_rwlock_t
      • 异步栅栏函数dispatch_barrier_async
  • 锁的性能对比
  • 总结


多线程安全隐患

资源共享:一块资源可能会被多个线程共享

当多个线程可能会访问同一块资源(对象、变量、文件)时,易引起数据错乱和数据安全问题

比如很经典的存取钱和买票问题:

存钱取钱问题

在这里插入图片描述

初始余额有1000元,存1000元取500元,按理说应剩下1500元,可两条线程同时对余额进行操作的结果就是数据错乱,最后剩下500元

以下是代码实现:

/*存取钱问题*/- (void)saveMoney {NSInteger oldMoney = self.money;sleep(.5);oldMoney += 50;self.money = oldMoney;NSLog(@"存50元还剩%ld元 --- %@", oldMoney, [NSThread currentThread]);}- (void)drawMoney {NSInteger oldMoney = self.money;sleep(.5);oldMoney -= 20;self.money = oldMoney;NSLog(@"取20元还剩%ld元 --- %@", oldMoney, [NSThread currentThread]);}- (void)moneyTest {self.money = 100;dispatch_queue_t queue = dispatch_get_global_queue(0, 0);dispatch_async(queue, ^{for (int i = 0; i < 5; ++i) {[self saveMoney];}});dispatch_async(queue, ^{for (int i = 0; i < 5; ++i) {[self drawMoney];}});
}

多条线程同时存取钱导致数据错乱:

在这里插入图片描述

卖票问题

在这里插入图片描述

两边同时卖出一张票,应剩下998张票,两条线程同时对票数操作的结果就是数据错乱,剩下999张

代码实现:

/*卖票问题*/// sleep()让多个线程尽可能拿到相同的值
// 卖1张票
- (void)saleTicket {NSInteger oldTicketCount = self.ticketsCount;sleep(.7);oldTicketCount--;self.ticketsCount = oldTicketCount;NSLog(@"还剩%ld张票 --- %@", oldTicketCount, [NSThread currentThread]);}// 每条线程卖5张票
- (void)saleTickets {self.ticketsCount = 15;dispatch_queue_t queue = dispatch_get_global_queue(0, 0);dispatch_async(queue, ^{for (int i = 0; i < 5; ++i) {[self saleTicket];}});dispatch_async(queue, ^{for (int i = 0; i < 5; ++i) {[self saleTicket];}});dispatch_async(queue, ^{for (int i = 0; i < 5; ++i) {[self saleTicket];}});}

多条线程同时卖票导致数据错乱:

在这里插入图片描述

解决方案

线程同步:让线程按照预定的先后顺序来执行

加锁是为了保证当前正在访问的只有我这条线程,一加锁,别的线程就没办法访问了(就会忙等待或是休眠),一解锁,别的线程才能去访问
或者使用GCD中的信号量、同步串行队列也可以实现线程同步(协同步调),下面展开分析

1. 锁

当上一个线程的任务没有执行完毕的时候(被锁住),那么下一个线程就不会执行,当上一个线程的任务执行完毕,下一个线程会立即执行
加锁也就是保证同一时间只有一个线程在执行

锁分两大类,自旋锁互斥锁
锁的归类其实基本的锁就包括了三类: ⾃旋锁、互斥锁、读写锁,其他的⽐如条件锁、递归锁、信号量都是上层的封装和实现

自旋锁: 下一个线程反复检查上一个线程是否解锁,等待过程中线程保持执行(while),因此是一种忙等待
所谓忙等,即在访问被锁资源时,调用者线程不会休眠,而是一直地不停循环在那里,直到被锁资源释放锁

互斥锁: 下一个线程在等待上一个线程解锁的过程中(即获取锁失败)处于休眠状态,当互斥锁被释放,线程被唤醒执行任务,该任务也不会立刻执行,而是成为可执行状态(就绪)
所谓休眠,即在访问被锁资源时,调用者线程会休眠,此时CPU可以调度其他线程工作,直到被锁资源释放锁,此时会唤醒休眠线程

自旋锁

自旋锁优缺点

优点在于,因为自旋锁不会引起调用者睡眠,所以不会进行线程调度(唤醒线程、切换线程)、CPU时间片轮转等耗时操作,所以如果等待锁的时间非常短暂,就没必要让线程睡眠,不然更耗时间,自旋锁的效率远高于互斥锁

缺点在于,自旋锁一直占用CPU,他在未获得锁的情况下,一直运行自旋,所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低。自旋锁不能实现递归调用

OSSpinLock
#import <libkern/OSAtomic.h>
// 初始化
OSSpinLock lock = OS_SPINLOCK_INIT;
// 尝试加锁(如果需要等待就不加锁,直接返回false,如果不需要等待就加锁,返回true)
bool flag = OSSpinLockTry(&lock);
// 加锁
OSSpinLockLock(&lock);
// 解锁
OSSpinLockUnlock(&lock);

在协调同步这些不同的线程时,为什么要使用同一把锁?
如果每条线程下都创建一个新锁,那么这个锁开始永远都是未加锁状态,故下一个线程不会等待上一个线程结束(线程不会阻塞),而是直接进行,锁不一样,意味着不同线程可以同时进行

初始化同一把锁,有以下几种办法:

静态局部变量

{
static OSSpinLock _lock = OS_SPINLOCK_INIT;// 如果初始化是动态调用函数的,就不能使用static
// 因为函数调用是在运行时确定的,而statis右边的变量必须要在编译时确定
static OSSpinLock _lock = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{_lock = test();
});
}

静态全局变量

// 属性
@property (nonatomic, assign)OSSpinLock lock;
self.ticketLock = OS_SPINLOCK_INIT;// 静态全局变量
static OSSpinLock lock_;
// 让此全局变量只初始化一次
+ (void)initialize {if (self == [OSSpinLockClass class]) {static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{lock_ = OS_SPINLOCK_INIT;});}
}

注意:OSSpinLock目前已不再安全,Apple已不推荐使用

在这里插入图片描述

原因是可能会出现优先级反转问题如果等待锁的线程优先级较高,它会一直占用着CPU资源,前面优先级低的线程就无法释放锁

os_unfair_lock

此自旋锁的出现就是为了解决优先级反转问题

#import <os/lock.h>
// 初始化
os_unfair_lock ticketLock = OS_UNFAIR_LOCK_INIT;
// 尝试加锁
os_unfair_lock_trylock(&lock);
// 加锁
os_unfair_lock_lock(&lock);
// 解锁
os_unfair_lock_unlock(lock);

若最后没有解锁,那么其他线程就拿不到锁,即死锁

atomic

自旋锁的实际应用,自动生成的setter方法会根据修饰符不同调用不同方法,最后统一调用reallySetProperty方法,其中就有一段关于atomic修饰词的代码:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{if (offset == 0) {object_setClass(self, newValue);return;}id oldValue;id *slot = (id*) ((char*)self + offset);if (copy) {newValue = [newValue copyWithZone:nil];} else if (mutableCopy) {newValue = [newValue mutableCopyWithZone:nil];} else {if (*slot == newValue) return;newValue = objc_retain(newValue);}if (!atomic) {oldValue = *slot;*slot = newValue;} else {spinlock_t& slotlock = PropertyLocks[slot];slotlock.lock();oldValue = *slot;*slot = newValue;        slotlock.unlock();}objc_release(oldValue);
}

getter方法也是如此:

id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {if (offset == 0) {return object_getClass(self);}// Retain release worldid *slot = (id*) ((char*)self + offset);if (!atomic) return *slot;// Atomic retain release worldspinlock_t& slotlock = PropertyLocks[slot];slotlock.lock();id value = objc_retain(*slot);slotlock.unlock();// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.return objc_autoreleaseReturnValue(value);
}

可以看到属性如果被atomic修饰,setter/getter操作会进行加spinlock锁处理

注意:atomic关键字只能保证setter、getter操作的线程安全,并不能保证使用属性的过程是安全的

比如不能保证self.index+1也是安全的,如果改成self.index=i(单纯调用setter方法)是能保证setter方法的线程安全的

互斥锁

互斥锁又分为递归锁、非递归锁

pthread_mutex_t
#import <pthread/pthread.h>// PTHREAD_MUTEX_INITIALIZER宏定义是一个结构体:{_PTHREAD_MUTEX_SIG_init, {0}}
// 下面两段代码与结构体基本语法不符(结构体必须在定义变量的同时赋值,静态初始化)
self.mutex = PTHREAD_MUTEX_INITIALIZER;pthread_mutex_t mutex;
mutex = PTHREAD_MUTEX_INITIALIZER;// 静态初始化,是正确的语法
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

另有方法初始化锁:

// 初始化锁
pthread_mutex_init(&mutex, NULL);
// 加锁
pthread_mutex_lock(&mutex);
// 解锁
pthread_mutex_unlock(&mutex);
// 销毁锁
pthread_mutex_destroy(&mutex);

初始化一个mutex递归锁

// 初始化锁的属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);// 初始化递归锁
// 第二个参数传NULL,等效于PTHREAD_MUTEX_DEFAULT:pthread_mutex_init(mutex, NULL);
pthread_mutex_init(&mutex, &attr);// 销毁属性
pthread_mutexattr_destroy(&attr);

锁属性attributes的类型type

在这里插入图片描述

如果加锁的任务执行有递归调用,那么是不是会发生堵塞?还没等解锁就又要同步加锁
这里给换用递归锁即可解决,递归锁允许同步加锁,即只允许同一个线程下对一把锁进行重复加锁,不会出现阻塞,最后解锁次数与加锁次数相同即可

void test(void) {pthread_mutex_lock(&mutex);NSLog(@"%s", __func__);test();  //递归pthread_mutex_unlock(&mutex);
}
条件pthread_cond_t(线程检查器)

条件condition用于解决类似于生产者 - 消费者模式的问题,实现跨线程执行

// 初始化条件
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);// 等待条件(进入休眠,放开mutex锁;被唤醒后,会再次对mutex加锁)
pthread_cond_wait(&cond, &mutex);
// 唤醒一个正在等待该条件的线程(发送一个信号)
pthread_cond_signal(&cond);
// 唤醒所有正在等待该条件的线程
pthread_cond_broadcast(&cond);
// 销毁条件
pthread_cond_destroy(&cond);

此模式的情景是,要等待生产者产出商品才能消费,如果无商品存在,就无法消费:

void consume(void) {pthread_mutex_lock(&mutex);if (/*无商品*/) {// 等待,放开锁,让生产线程执行pthread_cond_wait(&cond, &mutex);}pthread_mutex_unlock(&mutex);
}void produce(void) {pthread_mutex_lock(&mutex);// 产出了商品pthread_mutex_unlock(&mutex);// 有了商品后,唤醒一个正在等待该条件的consume线程(发送一个信号)pthread_cond_signal(&cond);
}
NSLock&NSRecursiveLock(递归锁)

NSLock是对pthread_mutex_t普通锁的封装,所以它是一个非递归锁
如果对NSLock强行使用递归调用,就会在调用时发生堵塞,并非死锁,第一次加锁之后还没出锁就进行递归调用,第二次加锁就堵塞了线程。(因为不会查询缓存)

@interface NSLock : NSObject <NSLocking>
- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name;
@end@protocol NSLocking
// 加锁
- (void)lock;
// 解锁
- (void)unlock;
@end

OC对象的初始化就无需多言了

NSRecursiveLock是对mutex递归锁的封装,API跟NSLock基本一致
注意:NSRecursiveLock虽然有递归性,但没有多线程特性

- (void)test {NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];for (int i = 0; i < 10; i++) {dispatch_async(dispatch_get_global_queue(0, 0), ^{static void (^block)(int);block = ^(int value) {[lock lock];if (value > 0) {NSLog(@"value——%d", value);block(value - 1);}[lock unlock];};block(10);});}
}

因为for循环在block内部对同一个对象进行了多次锁操作,直到这个资源身上挂着N把锁,最后大家都无法一次性解锁,也就是找不到解锁的出口

线程1中加锁1、同时线程2中加锁2-> 解锁1等待解锁2 -> 解锁2等待解锁1 -> 无法结束解锁——形成死锁

此时我们可以通过@synchronized对对象进行锁操作,会先从缓存查找是否有锁syncData存在。如果有,直接返回而不加锁,保证锁的唯一性
@synchronized是符合递归和多线程特性的

同一线程可以多次获取而不会导致死锁的锁

NSCondition(条件锁)&NSConditionLock

NSCondition是对pthread_mutex_t和pthread_cond_t条件的封装

可以加锁的同时也可等待或唤醒线程:

@interface NSCondition : NSObject <NSLocking>
- (void)wait;  //等待
- (BOOL)waitUntilDate:(NSDate *)limit;
- (void)signal;  // 唤醒一个线程
- (void)broadcast;  // 唤醒所有线程
@property (nullable, copy) NSString *name;
@end

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

@interface NSConditionLock : NSObject <NSLocking>
- (instancetype)initWithCondition:(NSInteger)condition;
@property (readonly) NSInteger condition;
- (void)lockWhenCondition:(NSInteger)condition;
- (BOOL)tryLock;
- (BOOL)tryLockWhenCondition:(NSInteger)condition;
- (void)unlockWithCondition:(NSInteger)condition;
- (BOOL)lockBeforeDate:(NSDate *)limit;
- (BOOL)lockWhenCondition:(NSInteger)condition beforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name;
@end

通过设置condition的值,可以设置锁哥线程间的依赖,控制线程的执行顺序:

self.conditionLock = [[NSConditionLock alloc] initWithCondition: 1];// 三个线程同时进行
[[[NSThread alloc] initWithTarget: self selector: @selector(p_one) object: nil] start];
[[[NSThread alloc] initWithTarget: self selector: @selector(p_two) object: nil] start];
[[[NSThread alloc] initWithTarget: self selector: @selector(p_three) object: nil] start];- (void)p_one {[self.conditionLock lockWhenCondition: 1];NSLog(@"_one");[self.conditionLock unlockWithCondition: 2];
}- (void)p_two {[self.conditionLock lockWhenCondition: 2];sleep(1);NSLog(@"_two");
//    [self.conditionLock unlock];[self.conditionLock unlockWithCondition: 3];
}- (void)p_three {
//    [self.conditionLock lockWhenCondition: 2];[self.conditionLock lockWhenCondition: 3];NSLog(@"_three");[self.conditionLock unlock];
}

打印结果:_one、_two、_three,让本会并发执行的三个线程按照执行了

@synchronized

@synchronized是对mutex递归锁的封装

@synchronized (/*token obj*/) {// 任务
}

放进去的这个token对象要是同一个,才能保证同一把锁
一般会传入self、如果是不同实例调用且也要保证一把锁的需求,可传入[self class]
为保证唯一性,一般这样写:

static NSObject* lock;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{lock = [[NSObject alloc] init];
});
@synchronized (lock) {// 任务
}

@synchronized锁
@synchronized底层源码分析

2. 信号量

信号量(semaphore):是一种更高级的同步机制,互斥锁可以说是 semaphore 在仅取0/1时的特例。信号量可以有更多的取值空间,控制线程最大并发数量,用来实现更加复杂的同步,而不单单是线程间互斥

// 初始化
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);
// 加锁
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
// 解锁
dispatch_semaphore_signal(semaphore);
/*
注: dispatch_semaphore  其他两个功能
1.还可以起到阻塞线程的作用
2.可以实现定时器功能,这里不做过多介绍
*/

控制最大并发数量:

// 控制最大并发数量:5个线程self.ticketSemaphore = dispatch_semaphore_create(5);- (void)otherTest {for (int i = 0; i < 20; ++i) {[[[NSThread alloc] initWithTarget: self selector: @selector(test) object: nil] start];}
}- (void)test {// 如果信号量的值 > 0,就-1,往下执行// 如果信号量的值 <= 0,当前线程就会进入休眠等待(知道信号量的值 > 0)dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);sleep(3);NSLog(@"test --- %@", [NSThread currentThread]);// 让信号量的值+1dispatch_semaphore_signal(self.semaphore);
}

3. 同步串行队列

dispatch_sync(dispatch_queue_create("serialQueue", DISPATCH_QUEUE_SERIAL), ^{// 任务
});

读写安全

I/O操作,即文件数据读写操作要实现读写安全,需实现以下需求:

  1. 多读单写(写写互斥)
  2. 不允许读写同时进行(读写互斥)
  3. 读写不能堵塞主线程,不能影响主线程

这就需要读写锁来实现

读写锁

读写锁实际是⼀种特殊的⾃旋锁,它把对共享资源的访问者划分成读者和写者,读者只对共享资源进⾏读访问,写者则需要对共享资源进⾏写操作

现有两种解决读写安全的方案:

pthread_rwlock_t

// 初始化锁
pthread_rwlock_t lock;
pthread_rwlock_init(&lock, NULL);
// 读加锁
pthread_rwlock_rdlock(&lock);
// 读尝试加锁
pthread_rwlock_tryrdlock(&lock);
// 写加锁
pthread_rwlock_wrlock(&lock);
// 写尝试加锁
pthread_rwlock_trywrlock(&lock);
// 解锁
pthread_rwlock_unlock(&lock);
// 销毁锁
pthread_rwlock_destroy(&lock);

当读写锁在读加锁状态时,所有试图以读模式对它进⾏加锁的线程都可以得到访问权

当读写锁是写加锁状态时,在这个锁被解锁之前,所有试图对这个锁加锁的线程都会被阻塞,必须直到所有的线程释放锁

读模式锁定时可以共享, 以写模式锁住时意味着独占,所以读写锁⼜叫共享-独占锁

异步栅栏函数dispatch_barrier_async

dispatch_queue_t concurrentQueue = dispatch_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);// 栅栏函数保证读写的互斥
dispatch_barrier_async(concurrentQueue, ^{NSLog(@"write");
});// 异步并行保证共享读线程
dispatch_async(concurrentQueue, ^{NSLog(@"read");
});

此处的异步栅栏函数传入的并发队列必须是通过create创建的
如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于dispatch_async

锁的性能对比

自旋锁效率肯定远高于互斥锁

以下是自旋锁性能排行:

在这里插入图片描述

总结

  • OSSpinLock不再安全,底层用os_unfair_lock替代
  • atomic只能保证setter、getter时线程安全,所以更多的使用nonatomic来修饰
  • 读写锁更多使用栅栏函数来实现
  • @synchronized在底层维护了一个哈希链表进行data的存储,使用recursive_mutex_t进行加锁
  • NSLock、NSRecursiveLock、NSCondition和NSConditionLock底层都是对pthread_mutex的封装
  • NSCondition和NSConditionLock是条件锁,当满足某一个条件时才能进行操作,和信号量dispatch_semaphore类似
  • 普通场景下涉及到线程安全,可以用NSLock
  • 循环调用时用NSRecursiveLock
  • 循环调用且有线程影响时,请注意死锁,如果有死锁问题请使用@synchronized

日常开发中若需要使用线程锁来保证线程安全,请多考虑一下再选择使用哪个锁,@synchronized并不是最优的选择。作为一名优秀的开发不但能让App正常运行,更要让它优质地运行、优化它的性能

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

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

相关文章

C++ | Leetcode C++题解之第307题区域和检索-数组可修改

题目&#xff1a; 题解&#xff1a; class NumArray { private:vector<int> tree;vector<int> &nums;int lowBit(int x) {return x & -x;}void add(int index, int val) {while (index < tree.size()) {tree[index] val;index lowBit(index);}}int p…

3.2.微调

微调 ​ 对于一些样本数量有限的数据集&#xff0c;如果使用较大的模型&#xff0c;可能很快过拟合&#xff0c;较小的模型可能效果不好。这个问题的一个解决方案是收集更多数据&#xff0c;但其实在很多情况下这是很难做到的。 ​ 另一种方法就是迁移学习(transfer learning…

FFplay介绍及命令使用指南

&#x1f60e; 作者介绍&#xff1a;欢迎来到我的主页&#x1f448;&#xff0c;我是程序员行者孙&#xff0c;一个热爱分享技术的制能工人。计算机本硕&#xff0c;人工制能研究生。公众号&#xff1a;AI Sun&#xff08;领取大厂面经等资料&#xff09;&#xff0c;欢迎加我的…

微软Win11 24H2最新可选更新补丁26100.1301来袭!

系统之家于7月31日发出最新报道&#xff0c;微软针对Win11 24H2用户推出七月最新的可选更新KB5040529&#xff0c;本次更新为开始菜单引入了全新的账号管理器&#xff0c;也改进了任务栏上的小组件图标。接下来跟随系统之家小编来看看本次更新的详细内容吧&#xff01;【推荐下…

不同类型游戏安全风险对抗概览(下)| FPS以及小游戏等外挂问题,一文读懂!

FPS 游戏安全问题 由于射击类游戏本身需要大量数值计算&#xff0c;游戏方会将部分计算存放于本地客户端&#xff0c;而这为外挂攻击者提供了攻击的温床。可以说&#xff0c;射击类游戏是所有游戏中被外挂攻击最为频繁的游戏类型。 根据网易易盾游戏安全部门检测数据显示&#…

【排序算法】Java实现三大非比较排序:计数排序、桶排序、基数排序

非比较排序概念 非比较排序是一种排序算法&#xff0c;它不通过比较元素之间的大小关系来进行排序&#xff0c;而是基于元素的特征或属性进行排序。这种方法在特定情况下可以比比较排序方法&#xff08;如快速排序、归并排序等&#xff09;更有效率&#xff0c;尤其是在处理大…

【原创】java+ssm+mysql医生信息管理系统设计与实现

个人主页&#xff1a;程序员杨工 个人简介&#xff1a;从事软件开发多年&#xff0c;前后端均有涉猎&#xff0c;具有丰富的开发经验 博客内容&#xff1a;全栈开发&#xff0c;分享Java、Python、Php、小程序、前后端、数据库经验和实战 开发背景&#xff1a; 随着信息技术的…

详解线程的几种状态?

详解线程的几种状态? 1. 新建状态&#xff08;New&#xff09;2. 就绪状态&#xff08;Runnable&#xff09;3. 运行状态&#xff08;Running&#xff09;4. 阻塞状态&#xff08;Blocked&#xff09;5. 死亡状态&#xff08;Dead&#xff09; &#x1f496;The Begin&#x1…

获客工具大揭秘:为何它能让获客如此轻松?

你是不是也觉得&#xff0c;现在的市场环境&#xff0c;获客越来越难了&#xff1f; 今天我要给大家分享一个实用且高效的获客工具&#xff0c;它简直是营销界的福音&#xff01; 1、关键词搜索 关键词搜索功能是获客工具的基础&#xff0c;也是其重要性不可小觑的原因。 这…

go-zero框架入门---认识微服务以及环境的安装

什么是微服务 微服务是一种软件架构风格&#xff0c;它将一个大型应用程序拆分成多个小型的、独立部署的服务&#xff0c;每个服务实现单一业务功能。每个服务运行在自己的进程中&#xff0c;并通过轻量级的通信机制&#xff08;通常是HTTP RESTful API&#xff09;相互协作。…

ubuntu 使用 freeplane

在知乎在过这个问题后 思维导图工具freemind和freeplane的区别&#xff1f; - 知乎。我选择使用 freeplane 作为思维导图的绘制软件。理由不外乎系统受限&#xff0c;和开源软件。 直接在软件商店里搜索 mind &#xff0c;其实也有其它的软件。第一个也蛮好用的。 安装 如果在…

【分享】HCIP-AI-EI Developer备考攻略

刚考完HCIP-AI-EI Developer就写了这篇热乎的笔记,主要是我在备考的时候发现网上没有相关经验帖,导致备考的时候心态不好。我从自身状态、考试介绍、备考建议、考试技巧等方面进行了总结,非常详细,希望我的这篇笔记能给大家提供一些帮助。 1 我的情况 备考前状态:学过一…

buu做题(11)

[CISCN2019 华东南赛区]Web11 抓个包可以发现是 Smarty框架 在页面可以观察到 一个 XFF头, 可以猜测注入点就在这 通过 if 标签执行命令 ,读取flag if system("cat /flag")}{/if} [极客大挑战 2019]FinalSQL 一个登录框, 上面的提示应该就是要你盲注了 点一下那…

Web : EL表达式 -15

EL表达式概述 EL 全名为Expression Language&#xff0c;用来替代<% %>脚本表达式。 基本结构为${表达式}。 获取数据 获取常量 <h1>获取常量</h1> ${123} ${123.32} ${"abc"} ${true} 获取变量 el会自动从四大作用域中搜寻域属性来使用 如果找不…

vue3后台管理系统 vue3+vite+pinia+element-plus+axios上

前言 项目安装与启动 使用vite作为项目脚手架 # pnpm pnpm create vite my-vue-app --template vue安装相应依赖 # sass pnpm i sass # vue-router pnpm i vue-router # element-plus pnpm i element-plus # element-plus/icon pnpm i element-plus/icons-vue安装element-…

《海军罪案调查处:起源》预告片介绍新角色莱罗伊·杰思罗·吉布斯

《海军罪案调查处&#xff1a;起源》的主演奥斯汀斯托威尔最近分享了这部备受期待的前传系列剧的一张新宣传照。虽然距离该剧上映还有几个月的时间&#xff0c;但这张照片将激起粉丝们的兴奋之情。 这张照片通过斯托维尔的官方社交账号分享&#xff0c;让观众们看到了年轻时的…

html+css+js前端作业和平精英官网1个页面(带js)

htmlcssjs前端作业和平精英官网1个页面&#xff08;带js&#xff09;有轮播图tab切换等功能 下载地址 https://download.csdn.net/download/qq_42431718/89597007 目录1 目录2 项目视频 htmlcssjs前端作业和平精英官网1个页面&#xff08;带js&#xff09; 页面1

国家超算互联网平台:模型服务体验与本地部署推理实践

目录 前言一、平台显卡选用1、显卡选择2、镜像选择3、实例列表4、登录服务器 二、平台模型服务【Stable Diffusion WebUI】体验1、模型运行2、端口映射配置3、体验测试 三、本地模型【Qwen1.5-7B-Chat】推理体验1、安装依赖2、加载模型3、定义提示消息4、获取model_inputs5、生…

前端-如何通过docker打包Vue服务成镜像并在本地运行(本地可以通过http://localhost:8080/访问前端服务)

1、下载安装docker&#xff0c;最好在vs code里安装docker的插件。 下载链接&#xff1a;https://www.docker.com/products/docker-desktop &#x1f389; Docker 简介和安装 - Docker 快速入门 - 易文档 (easydoc.net) 2、准备配置文件-dockerfile文件和nginx.conf文件 do…

【Redis 初阶】Redis 常见数据类型(Set、Zset、渐进式遍历、数据库管理)

一、Set 集合 集合类型也是保存多个字符串类型的元素的&#xff08;可以使用 json 格式让 string 也能存储结构化数据&#xff09;&#xff0c;但和列表类型不同的是&#xff0c;集合中&#xff1a; 元素之间是无序的。&#xff08;此处的 “无序” 是和 list 的有序相对应的…