【iOS开发】—— 初识锁

【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;@endint 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 endint 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.rhkb.cn/news/325191.html

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

相关文章

双向冒泡法,可以只求最大最小值

int BiBubbleSort(int Arr[],int n,int maxnum){int left0,rightn-1;int i;bool notDone true;int temp;if(n<2)return -1;while(left<right&&notDone){ notDone false; //设置未发生交换标志 for(ileft;i<right;i){if(Arr[i]>Arr[i1]){//swap(Arr[…

Python-VBA函数之旅-staticmethod函数

目录 一、staticmethod函数的常见应用场景 二、staticmethod函数使用注意事项 三、如何用好staticmethod函数&#xff1f; 1、staticmethod函数&#xff1a; 1-1、Python&#xff1a; 1-2、VBA&#xff1a; 2、推荐阅读&#xff1a; 个人主页&#xff1a; https://blog…

HackMyVM-VivifyTech

目录 信息收集 arp nmap nikto whatweb WEB web信息收集 wpscan feroxbuster hydra 提权 系统信息收集 横向渗透 git提权 get root 信息收集 arp ┌──(root㉿0x00)-[~/HackMyVM] └─# arp-scan -l Interface: eth0, type: EN10MB, MAC: 08:00:27:9d:6d:7b, …

【密评】 | 商用密码应用安全性评估从业人员考核题库(9/58)

Hill密码是重要古典密码之一&#xff0c;其加密的核心思想的是&#xff08;&#xff09;。 A.线性变换 B.非线性变换 C.循环移位 D.移位 著名的Kerckhoff原则是指&#xff08;&#xff09;。 A.系统的保密性不但依赖于对加密体制或算法的保密&#xff0c;而且依赖于密钥 B.系统…

即插即用篇 | YOLOv8 引入 Strip Pooling | 重新思考场景解析的空间池化

本改进已集成到 YOLOv8-Magic 框架。 空间池化已被证明在捕获像素级预测任务的长距离上下文信息方面非常有效,如场景解析。在本文中,我们超越了通常具有N N规则形状的常规空间池化,重新思考空间池化的构成,引入了一种新的池化策略,称为条带池化,它考虑了一个长而窄的核,…

C++ 中的 lambda 表达式

1.概念 lambda表达式实际上是一个匿名类的成员函数&#xff0c;该类由编译器为lambda创建&#xff0c;该函数被隐式地定义为内联。因此&#xff0c;调用lambda表达式相当于直接调用匿名类的operator()函数&#xff0c;这个函数可以被编译器内联优化&#xff08;建议&#xff0…

C++的数据结构(二)

一、链表的基本概念 链表&#xff08;Linked List&#xff09;是一种物理存储单元上非连续的、非顺序的线性数据结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点&#xff08;链表中每一个元素称为节点&#xff09;组成&#xff0c;节点…

py黑帽子学习笔记_网络编程工具

tcp客户端 socket.AF_INET表示使用标准IPV4地址和主机名 SOCK_STREAM表示这是一个TCP客户端 udp客户端 udp无需连接&#xff0c;因此不需要client.connect这种代码 socket.SOCK_DGRAM是udp的 tcp服务端 server.listen(5)表示设置最大连接数为5 发现kill server后端口仍占用…

牛客NC404 最接近的K个元素【中等 二分查找+双指针 Java/Go/PHP】

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/b4d7edc45759453e9bc8ab71f0888e0f 知识点 二分查找&#xff1b;找到第一个大于等于x的数的位置idx;然后从idx开始往两边扩展Java代码 import java.util.*;public class Solution {/*** 代码中的类名、方法名、…

Shell编程规范与变量

Shell 什么是Shell&#xff1f; 就是与内核沟通的界面、应用程序等等。比如你要播放音乐&#xff0c;你的计算机通过你在Shell输入的打开音乐的命令&#xff0c;Shell在告诉操作系统的内核用户希望打开音乐&#xff0c;内核在通过cpu调度、内存管理、磁盘输入输出等工作&#…

JAVA课程设计

一&#xff1a;Java连接mysql数据库 1.1点击进入mysql jar包下载官网 MySQL :: MySQL Community Downloads 将下载好的压缩包进行解压 解压之后下图就是连接数据库所用到的jar包&#xff1a; 将jar包复制到IDEA所用的项目下&#xff0c;放置jar包的目录为lib&#xff0c;需要…

NSS刷题

[SWPUCTF 2021 新生赛]jicao 类型&#xff1a;PHP、代码审计、RCE 主要知识点&#xff1a;json_decode()函数 json_decode()&#xff1a;对JSON字符串解码&#xff0c;转换为php变量 用法&#xff1a; <?php $json {"ctf":"web","question"…

安装Nox夜神模拟器关闭了HyperV后Docker运行不了怎么办?

1.背景 为了模拟真机&#xff0c;尝试安装了Nox夜神模拟器&#xff0c; 安装过程要求关闭Hyper-V。当时只是在程序安装卸载中关闭了系统服务。以为到时勾选上就好了。操作路径&#xff1a;控制面板\所有控制面板项\程序和功能\启用或关闭Windows功能\Hyper-V。 后来卸载掉了夜神…

FreeRTOS的列表和列表项 list.c文件详解

列表、列表项的定义以及初始化 列表相当于链表&#xff0c;列表项相当于节点&#xff0c;FreeRTOS中的列表相当于一个双向环形链表。 列表使用指针指向列表项。一个列表&#xff08;list&#xff09;下面可能有很多个列表项&#xff08;list item&#xff09;&#xff0c;每个…

vivado仿真readmemb函数相对路径

目前常用的vivado工程的结构如下所示 prj-name|-xxx|-prj.sim|-sim_1|-behav|-modelsim|-tb_prj.do|-xsim|-prj.srcs|-sim_1|-new|-tb_prj.v|-tb_prj_mem.txt一般来说我们创建的testbench文件和新建的txt文件都会放在srcs->sim_1->new这个路径下面&#xff0c;但是我们在…

【PHP【实战版】系统性学习】——登录注册页面的教程,让编写PHP注册变成一个简单的事情

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;开发者-曼亿点 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 曼亿点 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a…

最美博客POETIZE个人博客系统源码

源码说明&#xff1a; POETIZE个人博客系统源码 | 最美博客 这是一个基于SpringBoot、Vue2和Vue3的开源项目&#xff0c;支持移动端自适应&#xff0c;并具备完善的前台和后台管理功能。 网站分为两个模块&#xff1a; 1. 博客系统&#xff1a;包括文章、表白墙、图片墙、收…

SpringBoot实现图片验证码

引入依赖 <dependency><groupId>com.github.whvcse</groupId><artifactId>easy-captcha</artifactId><version>1.6.2</version> </dependency>代码实现 package com.qiangesoft.captcha.controller;import com.wf.captcha.*…

【Linux】基础命令:进程、网络

systemctl命令 控制内置服务 systemctl start | stop | status | enable | disable 服务名 start | stop开启关闭&#xff0c;status状态&#xff0c;enable | disable开启关闭开机自启 date命令 查看系统时间 date [-d] [格式化字符串] date -d “1 day” %Y-%m-%d 修改时区…

Stable Diffusion:AI绘画的新纪元

摘要&#xff1a; Stable Diffusion&#xff08;SD&#xff09;作为AI绘画领域的新星&#xff0c;以其开源免费、强大的生成能力和高度的自定义性&#xff0c;正在引领一场艺术与技术的革命。本文旨在为读者提供Stable Diffusion的全面介绍&#xff0c;包括其原理、核心组件、安…