【iOS】——锁

五类锁

锁作为一种非强制的机制,被用来保证线程安全。每一个线程在访问数据或者资源前,要先获取(Acquire)锁,并在访问结束之后释放(Release)锁。如果锁已经被占用,其它试图获取锁的线程会等待,直到锁重新可用。

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

锁总共分为五类:

  • 互斥锁
  • 自旋锁
  • 读写锁
  • 条件锁
  • 递归锁

img

互斥锁

在一个多线程环境中,互斥锁可以确保同一时刻只有一个线程能够访问临界区,即共享资源的代码段。

工作原理

  • 在访问共享资源之前进行加锁,访问完成后解锁。

  • 加锁后,任何其他试图加锁的线程会被阻塞,直到当前线程解锁。

  • 解锁时,如果有1个以上的线程阻塞,那么所有该锁上的线程变为就绪状态,第一个就绪的加锁,其他的又进入休眠。

自旋锁

跟互斥类似,只是自旋锁不会引起调用者睡眠, 因为资源被占用的时候, 会一直循环检测锁是否被释放(CPU不能做其他的事情)

节省了唤醒睡眠线程的内核消耗(在加锁时间短暂的情况下会大大提高效率)

在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等

自旋锁和互斥锁的区别

例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和 Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞 (blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而自旋锁则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在 Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。

  • 互斥锁:当一个线程试图获取一个已经被其他线程持有的锁时,这个线程会被阻塞,直到锁被释放。这通常涉及线程的上下文切换,线程被挂起并让出处理器时间给其他线程或进程。
  • 自旋锁:当一个线程试图获取一个已经被其他线程持有的锁时,这个线程不会被阻塞,而是会持续检查(“自旋”)锁的状态,直到锁被释放。这避免了上下文切换的开销,但在锁持有时间较长时可能导致浪费CPU资源。

互斥锁:线程会从sleep(加锁)——>running(解锁),过程中有上下文的切换,cpu的抢占,信号的发送等开销。

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

**总结:**自旋锁在等待锁时会持续检查锁状态而不放弃CPU时间,适合短期等待;互斥锁在等待时会让出CPU,进入阻塞状态,直至锁可用,适用于长期等待或需要避免CPU空转的场景。

读写锁

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

写的时候:读写都等待;

读的时候:写等待,读无需等待

条件锁

通常与互斥锁一起使用,条件锁允许一个或多个线程等待某个特定条件满足时才继续执行,而互斥锁则用于保护临界区,防止多个线程同时访问共享资源。

工作原理

  1. 等待条件:一个线程调用条件锁的 wait() 方法时,它会释放互斥锁并阻塞自己,直到满足某个条件。在 wait() 被调用期间,其他线程可以获取互斥锁并访问临界区。
  2. 唤醒线程:当条件满足时,通常是由另一个线程调用条件锁的 notify_one()notify_all() 方法来唤醒一个或所有等待的线程。被唤醒的线程将重新尝试获取互斥锁。
  3. 检查条件:被唤醒的线程重新获取互斥锁后,需要再次检查条件是否仍然满足,因为可能有其他线程改变了条件状态。如果条件不满足,线程可能需要再次调用 wait()

递归锁

跟互斥类似, 但是允许同一个线程在未释放锁前,多次加锁, 不会引发死锁

九种锁

在iOS中有九种锁:

  • OSSpinLock
  • os_unfair_lock
  • Dispatch_semaphore
  • pthread_mutex
  • NSLock
  • NSConditon
  • NSRecursiveLock
  • NSConditionLock
  • @synchronize

img

下面是9种锁的耗时排行:

img

OSSpinLock 自旋锁

OSSpinLock是iOS旧版本中提供的一种自旋锁(Spin Lock)实现。它通过忙等待的方式来获取锁,并且不会导致线程的阻塞和切换。自旋锁在iOS 10之后已被标记为废弃,因为它存在优先级反转和性能问题,可以使用os_unfair_lock替代

os_unfair_lock

是一种轻量级的互斥锁,特别适合于那些锁持有时间较短,且锁竞争不激烈的情况。相比于传统的互斥锁(如 pthread_mutex),os_unfair_lock 提供了更低的上下文切换开销

当一个线程试图获取一个已经被占用的锁时,它不会排队等待,而是立即返回,这意味着它可能在其他等待的线程之前再次尝试获取锁。这种机制在低竞争环境下可以减少锁的获取时间,但在高竞争环境下可能导致某些线程长期无法获取锁。

注意⚠️:尽管它在某些方面表现出类似自旋锁的行为(例如,在锁未被其他线程持有时,尝试获取锁的线程不会立即被阻塞),但它本质上仍然是一种互斥锁,因为它确保了同一时刻只有一个线程可以拥有锁。

dispatch_semaphore

  • semaphore叫做”信号量”
  • 信号量的初始值,可以用来控制线程并发访问的最大数量
  • 信号量的初始值为1,代表同时只允许1条线程访问资源,保证线程同步
//表示最多开启5个线程
dispatch_semaphore_create(5);
// 如果信号量的值 > 0,就让信号量的值减1,然后继续往下执行代码
// 如果信号量的值 <= 0,就会休眠等待,直到信号量的值变成>0,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);

运行下面代码:

@interface dispatch_semaphoreDemo()
@property (strong, nonatomic) dispatch_semaphore_t semaphore;
@end
@implementation dispatch_semaphoreDemo
- (instancetype)init
{
if (self = [super init]) {
self.semaphore = dispatch_semaphore_create(1);
}
return self;
}
- (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,就让信号量的值减1,然后继续往下执行代码
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);sleep(2);
NSLog(@"test - %@", [NSThread currentThread]);// 让信号量的值+1
dispatch_semaphore_signal(self.semaphore);
}
@end

在这里插入图片描述

每隔一秒出现一次打印。虽然我们同时开启20个线程,但是一次只能访问一条线程的资源

pthread_mutex

mutex叫做”互斥锁”,等待锁的线程会处于休眠状态

  • 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_mutex_destroy(&_mutex);

可以不初始化属性,在传属性的时候直接传NULL,表示使用默认属性PTHREAD_MUTEX_NORMALpthread_mutex_init(mutex, NULL);

确保在调用 pthread_mutex_lock() 后总是调用相应的 pthread_mutex_unlock(),并且在解锁前不要释放调用线程。否则,可能会导致死锁

NSLock

NSLock是对mutex普通锁的封装

NSLock 遵循 NSLocking 协议。Lock 方法是加锁,unlock 是解锁,tryLock 是尝试加锁,如果失败的话返回 NO,lockBeforeDate: 是在指定Date之前尝试加锁,如果在指定时间之前都不能加锁,则返回NO

@protocol NSLocking
- (void)lock;
- (void)unlock;
@end@interface NSLock : NSObject <NSLocking> {
@private
void *_priv;
}- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;
@property (nullable, copy) NSString *name
@end

使用方法:

@interface NSLockDemo()
@property (nonatomic,strong) NSLock *lock;
@end- (void)test{
[self.lock lock];
[self doSomeThing];
[self.lock unlock];
}

NSRecursiveLock

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

@interface RecursiveLockDemo()
@property (nonatomic,strong) NSRecursiveLock *lock;
@end
- (void)test{
[self.lock lock];
[self doSomeThing];
[self.lock unlock];
}

NSCondition

NSCondition是对mutexcond的封装,更加面向对象

@interface NSCondition : NSObject <NSLocking> {
- (void)wait;//用于使当前线程等待,直到接收到信号。 
- (BOOL)waitUntilDate:(NSDate *)limit;//使当前线程等待,直到接收到信号或者指定的日期超时。
- (void)signal;//唤醒一个等待中的线程。
- (void)broadcast;//唤醒所有等待中的线程。
@property (nullable, copy) NSString *name //用于标识 NSCondition 对象的名称
@end

NSConditionwaitsignalbroadcast 方法会自动获取锁,所以你应该在调用这些方法之前先释放锁。这是因为,当你调用 wait 方法时,线程会释放锁并进入等待状态;当你调用 signalbroadcast 方法时,被唤醒的线程会在继续执行前自动获取锁。

#import <Foundation/Foundation.h>NSCondition *queueCondition = [[NSCondition alloc] init];
NSMutableArray *queue = [NSMutableArray array];void producer() {for (int i = 0; i < 10; i++) {[NSThread sleepForTimeInterval:1]; // 模拟耗时操作[queueCondition lock];[queue addObject:[NSNumber numberWithInt:i]];NSLog(@"Producer added item: %d", i);[queueCondition signal]; // 唤醒一个等待的消费者[queueCondition unlock];}
}void consumer() {while (TRUE) {[queueCondition lock];while ([queue count] == 0) {[queueCondition wait]; // 等待生产者添加项目}NSNumber *item = [queue objectAtIndex:0];[queue removeObjectAtIndex:0];NSLog(@"Consumer got item: %@", item);[queueCondition unlock];}
}int main(int argc, const char * argv[]) {@autoreleasepool {NSThread *producerThread = [[NSThread alloc] initWithTarget:self selector:@selector(producer) object:nil];NSThread *consumerThread = [[NSThread alloc] initWithTarget:self selector:@selector(consumer) object:nil];[producerThread start];[consumerThread start];[producerThread join];[consumerThread join];}return 0;
}
  • 一个生产者线程(producer)生成项目并将其添加到队列中,然后调用 signal 方法唤醒一个等待的消费者线程。
  • 一个消费者线程(consumer)等待队列中有项目可用,一旦有项目可用,它就会从队列中取出项目并处理。

NSCondition 本身并不提供锁的功能,所以需要结合使用 NSLockNSRecursiveLock 来保护临界区

NSConditionLock

NSConditionLock是对NSCondition的进一步封装,可以设置具体的条件值。它结合了互斥锁和条件变量的功能。NSConditionLock 提供了一种高级的同步机制,允许线程在等待特定条件满足时阻塞自己,而同时保持对共享资源的锁定。

@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
  • initWithCondition:初始化Condition,并且设置状态值
  • -[NSConditionLock lock]:获取锁,阻止其他线程访问受保护的资源。
  • -[NSConditionLock unlock]:释放锁,允许其他线程访问受保护的资源。
  • -[NSConditionLock lockWhenCondition:]:如果条件不满足,则释放锁并使当前线程等待,直到条件变为真(true),然后再次获取锁并继续执行。
  • -[NSConditionLock lockWhenCondition:withTimeout:]:与 lockWhenCondition: 相同,但增加了一个超时值,如果超时时间内条件未满足,线程将恢复执行而不等待。
#import <Foundation/Foundation.h>@interface ConditionDemo : NSObject
@property (nonatomic, strong) NSConditionLock *lock;
@property (nonatomic, assign) int counter;
@end@implementation ConditionDemo- (instancetype)init {self = [super init];if (self) {_lock = [[NSConditionLock alloc] initWithCondition:1];_counter = 0;}return self;
}- (void)incrementCounter {[self.lock lock];if (_counter >= 10) {[self.lock lockWhenCondition:1]; // 等待条件满足}_counter++;NSLog(@"Counter incremented to: %d", _counter);[self.lock unlock];
}- (void)waitForCounterToReachTen {[self.lock lock];while (_counter < 10) {[self.lock lockWhenCondition:0]; // 等待条件满足}NSLog(@"Counter reached ten.");[self.lock unlock];
}@endint main(int argc, const char * argv[]) {@autoreleasepool {ConditionDemo *demo = [[ConditionDemo alloc] init];dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);for (int i = 0; i < 20; i++) {dispatch_async(queue, ^{[demo incrementCounter];});}[demo waitForCounterToReachTen];}return 0;
}

注意

使用 NSConditionLock 的关键点在于正确管理条件的真伪值。通常,你可以使用条件锁的构造函数初始化一个条件值,然后在代码中根据需要更新这个值。当一个线程调用 lockWhenCondition:lockWhenCondition:withTimeout: 方法时,它会检查条件值,如果条件为假(false),则线程会释放锁并等待,直到另一个线程改变了条件值使其为真(true)。

@synchronized

@synchronized是一个编译器指令,对mutex递归锁的封装, @synchronized(obj)内部会生成obj对应的递归锁,然后进行加锁、解锁操作

@synchronized([obj]) {// 临界区代码// 这里的代码将受到保护,一次只允许一个线程执行
}

这里的 [object] 是任何 Objective-C 对象,@synchronized 将使用该对象的地址作为锁的标识。这意味着每次调用 @synchronized 并传入相同的对象时,都会使用同一个锁。如果传入不同的对象,则会使用不同的锁。

工作原理

当一个线程进入由 @synchronized 保护的代码块时,它会尝试获取与传入对象关联的锁。如果锁当前未被其他线程持有,那么该线程将获得锁并执行临界区的代码。如果锁已被其他线程持有,那么当前线程将等待,直到锁被释放。当线程完成临界区的代码执行时,它会自动释放锁。

@synchronized的底层实现就是在开始和结束的时候调用了objc_sync_enter&objc_sync_exit方法。

int objc_sync_enter(id obj)
{
int result = OBJC_SYNC_SUCCESS;if (obj) {
SyncData* data = id2data(obj, ACQUIRE);
assert(data);
data->mutex.lock();
} 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();
}return result;
}

就是根据id2data方法找到一个data对象,然后在对data对象进行mutex.lock()加锁操作。我们点击进入id2data方法继续查找

#define LIST_FOR_OBJ(obj) sDataLists[obj].data
static StripedMap<SyncList> sDataLists;

发现获取data对象的方法其实就是根据sDataLists[obj].data这个方法来实现的,也就是一个哈希表。

总之就是@synchronized在底层维护了一个哈希链表进行data的存储,使用recursive_mutex_t进行加锁

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机网络必会面经

1.键入网址到网页显示&#xff0c;期间发生了什么 2.在TCP/IP网络模型中。TCP将数据进行分段后&#xff0c;为什么还需要IP层继续分片 3.详细说明tcp三次握手&#xff0c;为什么是三次&#xff0c;若每次握手丢了&#xff0c;解决办法是什么 4.详细说明tcp四次挥手&#xff…

【Python】python基础

本篇文章将讲解以下知识点&#xff1a; &#xff08;1&#xff09;循环语句 &#xff08;2&#xff09;字符串格式化 &#xff08;3&#xff09;运算符 一&#xff1a;循环语句 循环语句有两种&#xff1a;while for 本篇文章只讲解while循环 格式&#xff1a; whil…

Unity材质球自动遍历所需贴图

Unity材质球自动遍历所需贴图 文章目录 Unity材质球自动遍历所需贴图一、原理二、用法1.代码&#xff1a;2.使用方法 一、原理 例如一个材质球名为&#xff1a;Decal_Text_Cranes_01_Mat &#xff0c; 然后从全局遍历出&#xff1a;Decal_Text_Cranes_01_Albedo赋值给材质球的…

【网络基础】初识网络 {计算机网络背景;网络协议初识;网络传输基本流程;网络中的地址管理;网络设备简单介绍}

一、计算机网络背景 1.1 网络发展 计算机网络的发展可以追溯到20世纪60年代&#xff0c;那时候最初的计算机网络只是为了让科学家们能够共享计算机资源和数据。但是在20世纪80年代&#xff0c;互联网的出现彻底改变了计算机网络的面貌&#xff0c;使得人们可以随时随地通过互…

AI剪辑短视频以及账号管理矩阵工具系统搭建开发

目录 前言 一、系统有哪些功能&#xff1f; 二、怎么开发 前言 通过AI剪辑短视频以及生成短视频&#xff0c;以及对自媒体账号的管理功能的功能进行开发。这款系统能够批量混合剪辑视频然后一键发布到绑定好的自媒体账号里面。 一、系统有哪些功能&#xff1f; 1.AI智能文…

【深度学习实战(49)】目标检测损失函数:IoU、GIoU、DIoU、CIoU、EIoU、alpha IoU、SIoU、WIoU原理及Pytorch实现

前言 损失函数是用来评价模型的预测值和真实值一致程度&#xff0c;损失函数越小&#xff0c;通常模型的性能越好。不同的模型用的损失函数一般也不一样。损失函数主要是用在模型的训练阶段&#xff0c;如果我们想让预测值无限接近于真实值&#xff0c;就需要将损失值降到最低…

深入探讨RCE漏洞及其防御策略

1. RCE漏洞 1.1. 漏洞原理 远程代码执行&#xff08;RCE&#xff09;漏洞允许攻击者远程注入并执行操作系统命令或代码&#xff0c;从而控制后台系统。 1.2. 漏洞产生条件 调用第三方组件存在代码执行漏洞。用户输入内容作为系统命令参数拼接到命令中。对用户输入的过滤不严…

汽车雷达系统集成

汽车雷达系统集成是实现高级驾驶辅助系统&#xff08;ADAS&#xff09;和自动驾驶功能的重要环节&#xff0c;它涉及多种硬件和软件的协同工作。以下将详细讲解汽车雷达系统集成的各个方面&#xff1a; 雷达传感器选择 毫米波雷达&#xff1a;毫米波雷达主要使用24GHz和77GHz频…

【SQL Server点滴积累】SQL Server 2016数据库邮件(Database Mail)功能故障的解决方法

今天和大家分享SQL Server 2016数据库邮件(Database Mail)功能故障的解决方法 故障现象&#xff1a; 在SQL Server 2016中配置完成数据库邮件(Database Mail)功能后&#xff0c;当你尝试发送测试邮件后&#xff0c;既收不到测试邮件&#xff0c;也不显示错误消息 KB3186435 -…

Windows + Ubuntu双系统!小白轻松安装

前言 这几天有小伙伴想着装WindowsUbuntu双系统&#xff0c;但苦于找不到办法&#xff0c;就在某篇文章后台留言&#xff1a; 这不&#xff0c;今天就更新了嘛&#xff01;虽然做不到有求必应&#xff0c;但教程帖还是可以写写的&#xff0c;能帮一个是一个&#xff01; 今天要…

FFmpeg:多媒体处理的瑞士军刀

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

docker安装phpMyAdmin

直接安装phpMyAdmin需要有php环境&#xff0c;比较麻烦&#xff0c;总结了使用docker安装方法&#xff0c;并提供docker镜像。 1.docker镜像 见我上传的docker镜像&#xff1a;https://download.csdn.net/download/taotao_guiwang/89595177 2.安装 1).加载镜像 docker load …

Portainer-CE(可视化工具)一键部署+中文版

目录 Portainer 的主要功能 安装官方版2.19.5 安装中文版 2.19.5 安装中文版2.11.0 Portainer 是一个开源的图形化管理工具&#xff0c;用于管理 Docker 容器、Swarm 集群和 Kubernetes 集群。它提供了一个用户友好的 Web 界面&#xff0c;可以帮助用户更轻松地管理和监控…

[数据结构] AVL树 模拟实现AVL树

标题&#xff1a;[数据结构] AVL树 && 模拟实现AVL树 水墨不写bug 正文开始&#xff1a; 目录 &#xff08;一&#xff09;普通二叉搜索树的痛点 &#xff08;二&#xff09;AVL树简介 &#xff08;1&#xff09;AVL树的概念 &#xff08;三&#xff09;AVL树的…

《程序猿入职必会(5) · CURD 页面细节规范 》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

为 Laravel 提供生产模式下的容器化环境:打造现代开发环境的终极指南

为 Laravel 提供生产模式下的容器化环境&#xff1a;打造现代开发环境的终极指南 在现代开发中&#xff0c;容器化已经成为一种趋势。使用 Docker 可以让我们轻松地管理和部署应用程序。本文将带你一步步构建一个高效的 Laravel 容器化环境&#xff0c;确保你的应用程序在开发…

一些Kafka面试题

Kafka是如何保证消息不丢失&#xff1f; 1.生产者发送消息到Broker丢失&#xff1a; 设置异步发送&#xff1a;发送失败则使用回调进行记录或者重发 消息重试&#xff1a;参数配置&#xff0c;可以设置重试次数 2.消息在broker中存储丢失 发送确认机制acks acks0&#xf…

谷粒商城实战笔记-MySQL踩坑记录

文章目录 1&#xff0c; Public Key Retrieval is not allowed问题描述解决办法 2&#xff0c;1044 -Access denied for user root% to database解决方案 1&#xff0c; Public Key Retrieval is not allowed 问题描述 打开DBeaver连接MySQL提示“Public Key Retrieval is no…

4款免费且安全:常用的PDF转Word在线转换工具推荐

现在办公越来越离不开电脑了&#xff0c;PDF文件和Word文档来回转换的需求也越来越大。作为一个天天跟文件打交道的上班族&#xff0c;我特别明白找个好用、靠谱的PDF转Word在线转换工具有多重要。今儿个&#xff0c;给大家说说五个免费的转换工具&#xff0c;都是我试过觉得挺…

多微信管理不再难:聚合聊天神器助你轻松应对!

在当今社交媒体高度发达的时代&#xff0c;很多人都在使用多个微信账号来管理个人与工作联系。面对如此众多的信息沟通&#xff0c;如何高效管理成了一个难题。 幸运的是&#xff0c;聚合聊天神器的出现&#xff0c;彻底改变了这一局面&#xff0c;让我们轻松应对多微信账号的…