开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

基于Redission高级应用6-RSemaphoreRPe

发表于 2024-04-27

RSemaphore 实现原理

RSemaphore 是 Redisson 提供的一个分布式信号量(Semaphore),它是基于 Redis 的 SETNX、LPUSH、LPOP 等原子操作实现的。信号量用于控制对有限资源的访问,确保同一时间只有有限数量的进程或线程可以访问。

工作原理如下:

  1. 在 Redis 中,使用一个键来表示信号量,该键关联的值表示剩余的许可数。
  2. 当一个进程或线程尝试获取许可时,Redisson 会通过 Redis 的原子减操作(如 DECRBY)尝试减少一个许可。
  3. 如果减操作后的许可数不小于 0,获取许可成功;如果小于 0,获取许可失败,并且进程或线程可能会被阻塞,直到其他进程或线程释放许可。
  4. 当释放许可时,Redisson 会通过 Redis 的原子加操作(如 INCRBY)来增加许可数。

RPermitExpirableSemaphore 实现原理

RPermitExpirableSemaphore 是 Redisson 提供的一个分布式可过期许可信号量。与 RSemaphore 类似,但是每个许可都有一个过期时间,在这个时间后许可会自动释放。

工作原理如下:

  1. 每个许可都是通过一个唯一的 ID 标识的,这个 ID 与一个 Redis 键关联,该键的生存时间(TTL)就是许可的过期时间。
  2. 当一个进程或线程尝试获取许可时,如果信号量的计数器大于 0,Redisson 会创建一个新的 Redis 键,并设置其 TTL。
  3. 许可的 ID 被返回给调用者,用来标识持有的许可。
  4. 如果持有许可的进程或线程完成了任务,它可以通过提供许可的 ID 来释放许可,这将删除对应的 Redis 键。
  5. 如果许可未被显式释放,它将在到达 TTL 后自动释放。

优点

  1. 分布式协调:可以在多个进程或服务之间协调对共享资源的访问。
  2. 高可用性:由于基于 Redis,这些信号量具有 Redis 的高可用性和持久性特性。
  3. 可过期许可:RPermitExpirableSemaphore 支持许可的自动释放,避免了死锁的问题。
  4. 公平性:Redisson 可以配置信号量来确保公平的获取许可,按请求的顺序授予许可。

缺点

  1. 性能开销:与本地信号量相比,分布式信号量因为网络通信和 Redis 操作会有更高的性能开销。
  2. 网络依赖:信号量的操作依赖于网络和 Redis 服务器的稳定性,网络问题可能导致许可获取失败或延迟。
  3. 时钟同步:在使用可过期许可时,需要确保系统时钟同步,否则可能会导致许可提前过期或过期时间不一致。
  4. 资源浪费:如果许可频繁地被获取和释放,可能会导致 Redis 的资源浪费和性能下降。

在使用 RSemaphore 或 RPermitExpirableSemaphore 时,需要考虑到实际的业务场景和系统架构,以及它们的优缺点,来决定是否适合在您的应用中使用这些分布式信号量。

实战示例:

Redisson 提供了 RSemaphore 和 RPermitExpirableSemaphore,这两种信号量可以帮助你在分布式系统中实现限流和资源控制。以下是这些信号量在实战应用中的一些例子:

RSemaphore 实战应用示例

分布式限流

假设你正在构建一个微服务架构的系统,你需要确保一个特定的服务在任何给定时间内不会被过多的并发请求所压垮。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Autowired
private RedissonClient redissonClient;

public void initSemaphore(int permits) {
RSemaphore semaphore = redissonClient.getSemaphore("serviceLimitSemaphore");
semaphore.trySetPermits(permits);
}

public boolean acquire() {
RSemaphore semaphore = redissonClient.getSemaphore("serviceLimitSemaphore");
return semaphore.tryAcquire();
}

public void release() {
RSemaphore semaphore = redissonClient.getSemaphore("serviceLimitSemaphore");
semaphore.release();
}

在这个例子中,你首先设置了信号量的许可数,这个数目代表了你的服务可以同时处理的最大请求数。然后,每当有新的请求进来时,你会尝试获取一个许可。如果成功,服务将处理请求,处理完成后释放许可。如果无法获取许可,说明服务已达到最大负载,请求可以被拒绝或排队。

RPermitExpirableSemaphore 实战应用示例

分布式定时任务锁

在分布式系统中,可能需要确保定时任务在同一时间只能在一个节点上执行。RPermitExpirableSemaphore 可以发放一个可过期的许可,确保任务不会在多个节点上同时执行。

1
2
3
4
5
6
7
8
9
10
java复制代码@Autowired
private RedissonClient redissonClient;

public String acquireJobPermit(long leaseTime, TimeUnit timeUnit) {
RPermitExpirableSemaphore semaphore = redissonClient.getPermitExpirableSemaphore("jobLockSemaphormitId);
}
} else {
// 获取许可失败,另一个节点可能正在执行任务
}
}

在这个例子中,每次尝试执行定时任务之前,我们尝试获取一个带有租约时间的许可。如果获取成功,当前节点将执行任务,并在完成后释放许可。如果许可无法获取,表示有其他节点正在执行任务,当前节点可以跳过任务执行。租约时间确保即使在节点失败的情况下,许可也会在一定时间后自动释放,防止死锁。

注意事项

  • RSemaphore 和 RPermitExpirableSemaphore 在分布式环境中非常有用,但需要注意网络延迟和时钟同步问题。
  • 在使用可过期许可时,确保租约时间足够长,以覆盖任务的执行时间,防止在任务执行过程中许可过期。
  • 释放许可时,确保正确处理异常,并在必要时释放许可,避免资源泄露。
  • 在高并发场景下,信号量的操作可能会成为瓶颈,需要合理设置许可数量并监控系统性能。

这些实战应用示例展示了如何使用 Redisson 的信号量来控制分布式系统中的资源访问和任务执行,从而实现限流和同步。在实际应用中,开发者需要根据具体的业务场景和系统架构来选择合适的信号量类型和策略。

RSemaphore 和 RPermitExpirableSemaphore 是 Redisson 提供的两种分布式信号量实现,它们允许在分布式环境中限制对共享资源的访问。以下是这两种信号量的高级用法和相应的实战示例。

RSemaphore 高级用法

公平信号量

RSemaphore 可以配置为公平模式,这意味着 Redisson 会按照请求许可的顺序来分配许可,确保先到先得。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Autowired
private RedissonClient redissonClient;

public RSemaphore initFairSemaphore(String semaphoreName, int permits) {
RSemaphore semaphore = redissonClient.getFairSemaphore(semaphoreName);
semaphore.trySetPermits(permits);
return semaphore;
}

public void performActionWithFairSemaphore(String semaphoreName) {
RSemaphore semaphore = redissonClient.getFairSemaphore(semaphoreName);
try {
semaphore.acquire();
// 执行受限制的操作
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release();
}
}

在这个示例中,初始化信号量时,我们使用 getFairSemaphore 方法获取一个公平的信号量。这适用于那些需要保证处理顺序的场景,例如打印任务队列。

RPermitExpirableSemaphore 高级用法

可过期许可

RPermitExpirableSemaphore 允许发放带有过期时间的许可。如果持有许可的服务因任何原因未能释放许可,许可将在指定时间后自动释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Autowired
private RedissonClient redissonClient;

public String acquireExpirablePermit(String semaphoreName, long leaseTime, TimeUnit timeUnit) {
RPermitExpirableSemaphore semaphore = redissonClient.getPermitExpirableSemaphore(semaphoreName);
try {
return semaphore.acquire(leaseTime, timeUnit);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}

public void performActionWithExpirablePermit(String semaphoreName, long leaseTime, TimeUnit timeUnit) {
String permitId = acquireExpirablePermit(semaphoreName, leaseTime, timeUnit);
if (permitId != null) {
try {
// 执行受限制的操作
} finally {
semaphore.release(permitId);
}
}
}

在这个示例中,我们获取了一个带有过期时间的许可,这适用于那些需要在特定时间内完成操作,或者需要在操作未能正常完成时自动放弃许可的场景。

扩展实战示例

分布式任务调度

在分布式系统中,我们可能需要确保定时任务或后台作业在任何时刻只能在一个节点上运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
java复制代码@Autowired
private RedissonClient redissonClient;

public void scheduleJob(String jobName) {
RPermitExpirableSemaphore semaphore = redissonClient.getPermitExpirableSemaphore("jobSemaphore:" + jobName);
String permitId = null;
try {
// 尝试获取许可,设置合适的过期时间
permitId = semaphore.acquire(5, TimeUnit.MINUTES);
if (permitId != null) {
// 执行任务
executeJob(jobName);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (permitId != null) {
// 释放许可
semaphore.release(permitId);
}
}
}

private void executeJob(String jobName) {
// 实际的作业逻辑
}

在这个实战示例中,我们使用 RPermitExpirableSemaphore 来确保定时任务不会在多个节点上同时执行。每个任务都尝试获取一个许可,并在执行完成后释放许可。如果许可无法获取,任务将被跳过,以防止在另一个节点上重复执行。

注意事项

  • 在使用这些信号量时,确保所有节点的系统时间同步,避免因时间偏差导致的问题。
  • 使用 RPermitExpirableSemaphore 时,应该设置合理的许可过期时间,以便在节点故障时能够自动释放许可。
  • 需要处理好中断异常,确保在获取许可失败时,线程的中断状态得到正确处理。
  • 释放许可时,确保传递正确的许可 ID,避免错误地释放其他节点的许可。

这些高级用法和实战示例展示了如何在分布式环境中使用 Redisson 的信号量来控制对共享资源的并发访问,以及如何实现任务调度的同步。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何在Swift中使用AsyncStream创建类似回调的行

发表于 2024-04-27

如何在Swift中使用AsyncStream创建类似回调的行为

hudson 译 原文

毫无疑问,Swift并发彻底改变了我们在Swift中处理异步代码的方式。它的一个强大组件是AsyncStream,这是一种特殊的AsyncSequence形式,非常适合使用async/await 语法实现回调或类似委托的行为。

在Swift 并发之前,开发人员必须依靠闭包来触发回调,并在异步操作期间通知调用者某些事件。然而,随着AsyncStream的引入,这种基于闭包的方法现在可以被更直观、更直接的async/await语法所取代。

在本文中,让我们探索一个简单而说明性的示例,说明如何利用AsyncStream来跟踪下载操作的进度。阅读完成后,您将很好地了解AsyncStream的工作原理,并开始在自己的项目中使用它。

所以,不用多说,让我们开始吧。

示例应用程序

为了展示AsyncStream的力量,让我们创建一个示例应用程序,该应用程序将模拟下载操作,并使用进度条显示下载进度。

在这里插入图片描述

为了模拟通常与文件下载相关的等待期,我创建了一个带有 performDownload() 方法的File结构,该方法将随机睡眠一段时间。

1
2
3
4
5
6
7
8
9
10
11
swift复制代码struct File {

let name: String

func performDownload() async {

// Sleep for a random amount of time to emulate the wait required to download a file
let downloadTime = Double.random(in: 0.03...0.5)
try? await Task.sleep(for: .seconds(downloadTime))
}
}

在现实生活中,这种performDownload() 方法很可能由连接到服务器并等待其响应的代码组成。

有了这个解释,让我们深入研究有趣的部分。

创建异步流

首先,让我们创建一个下载器类(FileDownloader),该类接受File文件对象数组并逐个下载,每次成功下载后,它将通过提供下载文件的文件名来通知调用者。

为了实现这种行为,基于闭包的方法很可能看起来像这样:

1
2
3
4
5
6
swift复制代码static func download(_ files: [File], completion: (String) -> Void) {

// Download each file and trigger completion handler
// ...
// ...
}

然而,如果我们选择async/await语法,我们将需要用AsyncStream替换完成处理程序。

1
2
3
4
5
6
7
8
9
10
11
12
swift复制代码static func download(_ files: [File]) -> AsyncStream<String> {

// Init AsyncStream with element type = `String`
let stream = AsyncStream(String.self) { continuation in

// Perform download operation and yield the downloaded file’s filename
// ...
// ...
}

return stream
}

如上述代码所示,我们可以通过给它一个元素类型和自定义闭包来初始化AsyncStream,该闭包将元素交给AsyncStream。在我们的案例中,我们将元素类型设置为String,因为每次下载成功时,我们的闭包将产生下载文件的文件名。

有了这些,我们可以像这样实现执行下载操作的自定义闭包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
swift复制代码// Init AsyncStream with element type = `String`
let stream = AsyncStream(String.self) { continuation in

Task {
for file in files {

// Download the file
await file.performDownload()

// Yield the element (filename) when download is completed
continuation.yield(file.name)
}

// All files are downloaded
// Call the continuation’s finish() method when there are no further elements to produce
continuation.finish()
}
}

使用AsyncStream时要记住的一个要点是在完成所有操作后调用延续的finish() 方法。这一步骤至关重要,因为如果不这样做,将导致在调用点无限期等待,导致我们应用程序中的意外和非预期的行为。

消费AsyncStream

有了FileDownloader,是时候将其与用户界面集成以显示下载进度了。首先,我们将创建50个File对象并触发下载过程。

1
2
3
4
5
6
7
swift复制代码let totalFile = 50

// Generate file objects
let files = (1...totalFile).map { File(name: “Image_\($0).jpg”) }

// Start download
let downloaderStream = FileDownloader.download(files)

现在,为了在UI上显示下载进度,我们将利用给定的AsyncStream实例(downloaderStream),并利用for-wait-in语法处理每个文件名,文件名是在调用延续的yield()方法时由 AsyncStream生成的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
swift复制代码Task {
var downloadedFile = 0
for await filename in downloaderStream {

downloadedFile += 1

// Update progress bar
progressBar.progress = Float(downloadedFile) / Float(totalFile)

// Update status label
statusLabel.text = “Downloaded \(filename)”
}

statusLabel.text = “Download completed”
}

如前所述,调用延续的 finish() 方法是一个必不可少的步骤。如果没有此步骤,for循环将无限期等待,状态消息将不会更改为“下载完成”。

如果您想亲自尝试一下,您可以在GitHub上找到完整的示例代码。

感谢您的阅读。👨🏻‍💻

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

可解决传统保险丝缺陷的电子保险丝efuse

发表于 2024-04-27

近年来,电子保险丝/熔断器获得了越来越多的关注,业界对此类解决方案的需求也在不断增加。传统的玻璃管保险丝、片式保险丝和聚合物保险丝很容易受到环境温度和其他使用条件的影响,而且熔断电流的精确度较低。此外,响应速度也很慢。近年来,由于电子系统的小型化和系统性能提高的需要,这些问题的影响显得越来越严重。还有一个需要考虑的重要因素,就是传统的保险丝在熔断之后必须进行维修或更换。此外,现在还要求新一代的保险丝具有更先进的功能,以符合IEC62368-1标准(与ICT和AV设备相关的新安全标准)。

电子保险丝/熔断器是集成电路(IC)。MOSFET用于电压-电流检测电路、控制电路和电流通路的通断。因为它们是集成电路,所以在紧凑的封装内集成了许多高精度和快速响应的功能模块,包括过流、过电压、短路、过热保护等。传统的保险丝基本上只是切断电流通路,而电子保险丝却能提供许多保护和控制功能,从而能更容易地获得IEC62368-1认证。电子保险丝能够克服传统保险丝的相关缺陷。

)

eFuse IC的三大优点

eFuse IC是什么?

eFuse IC的应用有哪些?

eFuse IC的电路示例

)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

大厂面试题:两道来自京东的关于MyBatis执行器的面试题

发表于 2024-04-27

大家好,我是王有志。
今天给大家带来两道来自于京东关于的 MyBatis 面试题:

  • MyBatis 提供了哪些执行器(Executor)?它们有什么区别?
  • Mybatis 中如何指定 Executor 的类型?

MyBatis 提供了哪些执行器(Executor)?它们有什么区别?

MyBatis 提供的 Executor

严格意义上 MyBatis 中提供了 4 种 Executor:

image

其中具有执行 SQL 语句能力的是继承自 BaseExecutor 的 3 种 Executor:

  • SimpleExecutor,最基础的 Executor,每次执行 SQL 语句时都会创建 Statement 实例对象,完成 SQL 语句的执行后关闭 Statement 实例对象,无任何性能上的优化;
  • ReuseExecutor,提供复用 Statement 能力的 Executor,ReuseExecutor 会将 Statement 缓存到 Map<String, Statement> 实例对象中,其中 key 是 String 类型的 SQL 语句,而 value 是 SQL 语句的 Statement 对象,这样避免了频繁创建和销毁 Statement 带来的性能损耗;
  • BatchExecutor,提供了批量处理 Statement 的能力,在执行 update 语句时,将所有的 Statement 对象添加到 BatchExecutor 的 statementList 对象中,等到执行 SqlSession#commit时 统一提交,避免了频繁与数据库交互带来的性能损耗。

以上 3 种 Executor 除了自身的特点外,它们还具备抽象类 BaseExecutor 提供的一级缓存的能力。

CachingExecutor 本身不具备执行 SQL 语句的能力,它提供了对 MyBatis 二级缓存的支持。CachingExecutor 会持有一个继承自 BaseExecutor 的实例对象,CachingExecutor 在执行 SQL 语句时会调用自身持有的 Executor 实例对象来完成 SQL 语句的执行。

MyBatis 默认的 Executor

如果我们刨除不能够独立执行 SQL 语句的 CachingExecutor 的话,MyBatis 默认的 Executor 是 SimpleExecutor。如果把 CachingExecutor 也算在内的话,由于 MyBatis 是默认开启二级缓存的,因此默认的 Executor 就是 CachingExecutor。

这点可以在 MyBatis 的源码中得到印证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Java复制代码public class Configuration {

protected boolean cacheEnabled = true;

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
executor = new SimpleExecutor(this, transaction);
}
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
return (Executor) interceptorChain.pluginAll(executor);
}
}

源码的第 514 行中,是根据配置的 ExecutorType 创建继承自 BaseExecutor 的 Executor 实例对象,第 1517 行中,判断了 cacheEnabled 的配置情况,并决定是否要使用 CachingExecutor。

Mybatis 中如何指定 Executor 的类型?

MyBatis 配置 Executor 的方式

MyBatis 中有两种方式指定 Executor 的类型:

  • 通过 MyBatis 的核心配置指定 MyBatis 的 Executor 类型;
  • 创建 SqlSession 时可以指定指定 Executor 的类型。

MyBatis 核心配置文件中配置 Executor

可以在核心配置文件 mybatis-config.xml 中通过 settings 元素来配置 Executor 的类型,例如:

1
2
3
4
5
6
7
8
9
XML复制代码<configuration>
<!-- 省略 -->

<settings>
<setting name="defaultExecutorType" value="REUSE"/>
</settings>

<!-- 省略 -->
</configuration>

这样我们通过 mybatis-config.xml 创建的 SqlSession 中都会使用 ReuseExecutor。

创建 SqlSession 时指定 Executor

除了在核心配置文件 mybatis-config.xml 中配置 Executor 的类型外,还以在获取 MyBatis 的 SqlSession 实例对象时指定 Executor 的类型,如下:

1
2
3
4
5
Java复制代码    Reader mysqlReader = Resources.getResourceAsReader("mybatis-config.xml");
sqlSessionFactory = new SqlSessionFactoryBuilder().build(mysqlReader);

// 指定 Executor的类型
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE);

如果同时在核心配置文件 mybatis-coonfig.xml 中配置了 Executor 的类型,且在创建 SqlSession 的实例对象时也指定了 Executor 的类型,此时以创建 SqlSession 的实例对象时指定的 Executor 类型为准。


好了,今天的内容就到这里了,如果本文对你有帮助的话,希望多多点赞支持,如果文章中出现任何错误,还请批评指正。最后欢迎大家关注分享硬核 Java 技术的金融摸鱼侠王有志,我们下次再见!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【TVM 教程】在树莓派上部署预训练模型

发表于 2024-04-27

此教程介绍如何用 Relay 编译 ResNet 模型,并将其部署到树莓派。

1
2
3
4
5
6
javascript复制代码import tvm
from tvm import te
import tvm.relay as relay
from tvm import rpc
from tvm.contrib import utils, graph_executor as runtime
from tvm.contrib.download import download_testdata

在设备上构建 TVM Runtime

首先在远程设备上构建 TVM runtime。

本节和下一节中的所有指令都应在目标设备(例如树莓派)及 Linux 上执行。

由于我们是在本地机器上进行编译,远程设备仅用于运行生成的代码,因此只需在远程设备上构建 TVM runtime。

1
2
3
4
5
6
7
bash复制代码git clone --recursive https://github.com/apache/tvm tvm
cd tvm
mkdir build
cp cmake/config.cmake build
cd build
cmake ..
make runtime -j4

runtime 构建完成后,在 ~/.bashrc 文件中设置环境变量——用 vi ~/.bashrc 命令来编辑 ~/.bashrc,添加下面这行代码(假设 TVM 目录在 ~/tvm 中):

1
ruby复制代码export PYTHONPATH=$PYTHONPATH:~/tvm/python

执行 source ~/.bashrc 来更新环境变量。

在设备上设置 RPC 服务器

在远程设备(该示例中为树莓派)上运行以下命令,启动 RPC 服务器:

1
css复制代码python -m tvm.exec.rpc_server --host 0.0.0.0 --port=9090

看到如下结果,则表示 RPC 服务器启动成功:

1
ruby复制代码INFO:root:RPCServer: bind to 0.0.0.0:9090

准备预训练模型

注意:确保主机已经(用 LLVM)安装了完整的 TVM。

使用 MXNet Gluon 模型集合 中的预训练模型。更多有关这部分的信息详见 编译 MXNet 模型 教程。

1
2
3
4
5
6
python复制代码from mxnet.gluon.model_zoo.vision import get_model
from PIL import Image
import numpy as np

# 用一行代码来获取模型
block = get_model("resnet18_v1", pretrained=True)

为了测试模型,下载一张猫的图片,并转换其格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码img_url = "https://github.com/dmlc/mxnet.js/blob/main/data/cat.png?raw=true"
img_name = "cat.png"
img_path = download_testdata(img_url, img_name, module="data")
image = Image.open(img_path).resize((224, 224))

def transform_image(image):
image = np.array(image) - np.array([123.0, 117.0, 104.0])
image /= np.array([58.395, 57.12, 57.375])
image = image.transpose((2, 0, 1))
image = image[np.newaxis, :]
return image

x = transform_image(image)

synset 用于将 ImageNet 类的标签,转换为人类更容易理解的单词。

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码synset_url = "".join(
[
"https://gist.githubusercontent.com/zhreshold/",
"4d0b62f3d01426887599d4f7ede23ee5/raw/",
"596b27d23537e5a1b5751d2b0481ef172f58b539/",
"imagenet1000_clsid_to_human.txt",
]
)
synset_name = "imagenet1000_clsid_to_human.txt"
synset_path = download_testdata(synset_url, synset_name, module="data")
with open(synset_path) as f:
synset = eval(f.read())

以下代码可将 Gluon 模型移植到可移植的计算图上:

1
2
3
4
5
6
ini复制代码# 在 mxnet.gluon 中支持 MXNet 静态计算图(符号)和 HybridBlock
shape_dict = {"data": x.shape}
mod, params = relay.frontend.from_mxnet(block, shape_dict)
# 添加 softmax 算子提高概率
func = mod["main"]
func = relay.Function(func.params, relay.nn.softmax(func.body), None, func.type_params, func.attrs)

以下是一些基本的数据工作负载配置:

1
2
3
4
ini复制代码batch_size = 1
num_classes = 1000
image_shape = (3, 224, 224)
data_shape = (batch_size,) + image_shape

编译计算图

用计算图的配置和参数调用 relay.build() 函数,从而编译计算图。但是,不能在具有 ARM 指令集的设备上部署 x86 程序。除了用来指定深度学习工作负载的参数 net 和 params,Relay 还需要知道目标设备的编译选项。不同选项会导致性能不同。

如果在 x86 服务器上运行示例,可将其设置为 llvm。如果在树莓派上运行,需要指定它的指令集。若要在真实设备上运行,需将 local_demo 设置为 False。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码local_demo = True

if local_demo:
target = tvm.target.Target("llvm")
else:
target = tvm.target.arm_cpu("rasp3b")
# 上面一行代码是下面代码的简单形式:
# target = tvm.target.Target('llvm -device=arm_cpu -model=bcm2837 -mtriple=armv7l-linux-gnueabihf -mattr=+neon')

with tvm.transform.PassContext(opt_level=3):
lib = relay.build(func, target, params=params)

# 在 `relay.build` 之后,会得到三个返回值:计算图,库和新参数,因为我们做了一些优化,它们会改变参数,但模型的结果不变。

# 将库保存在本地临时目录中。
tmp = utils.tempdir()
lib_fname = tmp.relpath("net.tar")
lib.export_library(lib_fname)

输出结果:

1
2
3
4
vbnet复制代码/workspace/python/tvm/relay/build_module.py:411: DeprecationWarning: Please use input parameter mod (tvm.IRModule) instead of deprecated parameter mod (tvm.relay.function.Function)
DeprecationWarning,
/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead.
"target_host parameter is going to be deprecated. "

通过 RPC 远程部署模型

利用 RPC 可将模型从主机部署到远程设备。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ini复制代码# 从远程设备获取 RPC session。
if local_demo:
remote = rpc.LocalSession()
else:
# 下面是教程环境,把这个改成你目标设备的 IP 地址
host = "10.77.1.162"
port = 9090
remote = rpc.connect(host, port)

# 将库上传到远程设备,并加载它
remote.upload(lib_fname)
rlib = remote.load_module("net.tar")

# 创建远程 runtime 模块
dev = remote.cpu(0)
module = runtime.GraphModule(rlib["default"](dev))
# 设置输入数据
module.set_input("data", tvm.nd.array(x.astype("float32")))
# 运行
module.run()
# 得到结果
out = module.get_output(0)
# 得到排第一的结果
top1 = np.argmax(out.numpy())
print("TVM prediction top-1: {}".format(synset[top1]))

输出结果:

1
bash复制代码TVM prediction top-1: tiger cat

下载 Python 源代码:deploy_model_on_rasp.py

下载 Jupyter Notebook:deploy_model_on_rasp.ipynb

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

拥有你自己的Copilot!基于Llama3和CodeGPT

发表于 2024-04-27

当谈到代码自动补全和辅助编程工具时,GitHub Copilot是一个备受推崇的选择。然而,有时我们可能希望在本地环境中构建一个类似的解决方案,以便更好地控制数据和隐私,更重要的是Llama3是免费,而Github Copilot是收费的。本文将分享如何基于Llama3和CodeGPT这两个强大的开源项目,搭建自己的本地Copilot。

部署Llama3模型

在超越GPT-3.5!Llama3个人电脑本地部署教程中我已经分享过如何使用Ollama在本地部署Llama3模型,本文不再赘述。

安装CodeGPT扩展

打开Visual Studio Code,转到扩展标签页。搜索“CodeGPT”并安装这个扩展。CodeGPT是一个可以使用多种大语言模型辅助代码编程的插件。

注意:要认证发布者是CodeGPT,不要安装CSDN发布的!

image-20240425204658304

设置Llama3为CodeGPT默认模型

安装完CodeGPT之后,VSCode左侧会出现CodeGPT的产品图标,按照下图设置Llama3为CodeGPT使用的模型。

image-20240425205154839

测试

下面是笔者的测试截图,可以看到模型工作正常,提供了一个Echo Server例子:

image-20240425210425337

在日常开发中,我们可以在任意源码处点击右键让CodeGPT对代码进行解释或者优化。

image-20240425210538668

与收费的GitHub Copilot不同,Llama3提供了免费的解决方案,并且我们可以更好地控制数据和隐私。通过安装Llama3模型和CodeGPT扩展,我们能够在本地环境中享受到强大的代码辅助功能。无论是解释代码还是进行优化,CodeGPT都能为我们提供准确而实用的建议。

希望本文对你构建自己的本地Copilot有所帮助,让你在编程过程中更高效、更愉快!

在搭建本地Copilot的过程中有任何疑问都可以关注公众号加群进行交流。

WX20240420-091554@2x.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Conda创建与激活虚拟环境(指定虚拟环境创建位置) 1C

发表于 2024-04-27

1.Conda优势

Conda是一个开源的软件包管理系统和环境管理系统,主要用于在不同的计算环境中安装和管理软件包和其依赖项。它最初是为Python而设计的,但现在也可以用于管理其他语言的软件包。

Conda提供了对虚拟环境的支持,这使得用户可以在同一台计算机上同时管理多个相互独立的Python环境这对于开发和测试不同的项目或在项目之间切换时非常有用。

总结:Conda所创建的每一个虚拟环境都可以对应你的每一个Python项目,你的每一个Python项目所需的软件包等等东西可能不同,为了不使项目之间产生冲突,你可以为他们每一个配置一个虚拟环境,虚拟环境之间互不干扰。

2.Anaconda的下载和安装

太简单了,这里跳过,简单说就是进入官网,下载,按照提示安装即可,B站视频很多(这里的Anaconda就是Conda)

3.Conda创建虚拟环境以及相关注意问题

方法一:

  1. 打开Anaconda Prompt(找不到的直接在开始里面搜索,或者去你安装Anaconda的文件夹里面找),打开后窗口页面如下

![{KYZDBSSA_QC_3FH`0QT@E7.png](https://gitee.com/songjianzaina/juejin_p4/raw/master/img/968a864e40e07bb0819d0a7de61bb40dbf8c10353e24efb0315fa93a3713317c)
2. 创建和激活虚拟环境

  • 法一:

创建:

在prompt中输入以下代码:

conda create --name myenv python=3.6

myenv为你想要创建的虚拟环境的名称,python=3.6为该环境配置的python版本,如果不想添加python的话删掉即可

激活:注意,你通过这种形式创建的虚拟环境默认存在C盘当中,且其可以在Anaconda中找到(也就是下图的环境列表中找到),因此激活该虚拟环境的办法一是直接在下面的conda环境列表里面点击切换,二是在Anaconda prompt中输入代码conda activate 你设定的环境变量名称

64~M50%YDJ3%}Q6L9Z23SY.png

  • 法二(在其他盘创建虚拟环境):

创建:如果你想要将虚拟环境创建在D盘,可以使用--prefix参数来指定路径。以下是一个示例命令,用于将虚拟环境创建在D盘中:

conda create --prefix D:\path\to\myenv python=3.6

通过上述命令,你可以将虚拟环境创建在D盘的指定路径下。然后,当你激活这个虚拟环境时,可以使用conda activate D:\path\to\myenv来激活它,但无法使用环境名称来激活它。[注意:该方法创建的虚拟环境不会在Anaconda环境列表中显示]

方法二:

直接在Anaconda环境列表面板创建(此处原理等同于上文法一):

![~G6P(GK1N8BVFG3JNP)`V@Q.png](https://gitee.com/songjianzaina/juejin_p4/raw/master/img/b9e6c5b780b3094e8a4533cf9736b142dd4ac4ba7fa74eefa384f71b77f9e0c2)
选择你需要的python版本

UXC6WIP_}2FFUG5QP}CC1VF.png
最后,大家可以通过在prompt中输入conda env list来查看自己的全部环境,顺带说一句,如果是用上文方法二中的法二来创建的虚拟环境在列表中会显示其位置但不会显示其名称,同时在Anaconda的环境列表里面也是不会显示的,请大家注意!

那么如果我既想把环境创建在其他盘来减少C盘内存的占用又想可以直接通过他的名字直接激活该虚拟环境,这该怎么办呢?

详解请看下文 更改Conda虚拟环境创建位置并用其名称激活的方法

欢迎大家点赞收藏和交流!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

更改Conda虚拟环境创建位置并用其名称激活的方法

发表于 2024-04-27

很多同学说,用了网上或者我前文给的创建虚拟环境的方法创建虚拟环境后,要么就是虚拟环境无法使用其名称进行激活,必须使用其路径,特别麻烦,要么就是还是创建在C盘,占据大量内存,那到底有什么方法能够更换Conda虚拟环境创建位置并用其名称激活呢?

笔者给出下解:

要实现将虚拟环境创建在D盘,并且在激活环境时直接使用环境名称的方法,可以通过设置环境变量来实现。你可以在Conda的配置文件中指定环境的路径,这样在激活环境时就可以直接使用环境名称。

下面是具体的步骤:

  1. 打开Anaconda Prompt或者命令行窗口。
  2. 使用以下命令打开Conda的配置文件(通常是.condarc文件):
1
erlang复制代码notepad %USERPROFILE%.condarc
  1. 在打开的配置文件中添加以下内容:
1
2
vbnet复制代码envs_dirs:
- D:\path\to\envs

确保将路径D:\path\to\envs替换为你希望存储虚拟环境的实际路径。

  1. 保存并关闭配置文件。
  2. 现在,当你使用conda create --name myenv python=3.6创建虚拟环境时,它会被保存在你指定的D盘路径下。然后,你可以使用conda activate myenv来激活这个虚拟环境,而无需输入完整的路径。

通过这种方法,你可以轻松地在D盘创建虚拟环境,并且在激活时直接使用环境名称。

该方法的原理是修改了Anaconda配置安装虚拟环境位置的源文件。

希望该方法对你有帮助,欢迎点赞收藏和交流!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

大厂前端面试题 var x = 100;consolelo

发表于 2024-04-27

前言

大家面试的时候,会不会碰到这么一道题目。

1
2
3
4
5
php复制代码var global = 100;
function fn(){
console.log(global);
}
fn();

面试官问,这段代码打印的是什么?心中暗喜,这不简简单单打印的就是100嘛,这面试官问的也特简单了吧,于是脱口而出100,然后面试官继续问,为什么是100?然后你仔细一思索,于是这么回答到:因为在全局域中定义了一个变量global,函数作用域能访问到全局作用域的变量,所以打印的是100。于是面试官说,你还是回家等消息吧。然后你百思不得其解,我回答的没错呀,为什么面试官叫我回家呢?你回答的确实没错,但是还是那句话,你回答的还不够优秀,没有答到面试官的心窝窝里面去。

接下来就由蘑菇头来解答一下这道简单但是其实并不简单的面试题。

首先上述回答一点问题都没有,但是我们要知道,大厂考这么简单的一道题,那么他想考察的知识点肯定不会这么简单,你以为他想考察的是作用域,其实,面试官真正想要考察的是编译原理。他想知道你对这么一段代码的理解程度。我们从编译原理的角度来分析这里为什么打印的是100。

预编译

我们先清楚一个概念,什么是预编译。代码在执行前需要进行编译操作,用于确定变量的作用域,提高运行效率。预编译的过程包括词法分析、语法分析、语义分析、代码生成等步骤,用于确定各种代码之间的关联。

所以上面这段代码会执行一次预编译的过程。在此之前,我们先了解一下v8是怎么理解一个函数的,比如下面这个函数

1
2
3
scss复制代码function foo(){//函数的声明
}
foo();//函数的调用

foo函数,在JavaScript中它是一个对象,按道理来说对象身上就会有一些属性和方法,所以foo函数身上也会有一些属性和方法如:

1
2
3
4
lua复制代码foo.name //"foo";
foo.length //0; 表示函数参数的长度
foo.prototype //undefined; 表示函数的原型
foo.[[scope]] //函数的作用域属性 我们无法访问 v8引擎内部属性 -隐式属性

这个作用域属性也就是我们现在要聊的。每个函数都会有一个作用域属性,在 JavaScript 中,函数的作用域属性是指在函数创建时,除了开辟内存和赋值外,系统还会为该函数设置一个作用域。当前函数的作用域等于当前函数创建时所在的上下文。这个作用域也叫函数AO对象,AO 对象(Activation Object)是函数执行前的一瞬间生成的一个对象。它主要用于存储函数的上下文信息,包括函数的参数、局部变量、内部函数等,当函数调用结束后,AO对象会被销毁,当再次调用时,AO对象会再次创建。里面会用键值对的形式存储有效标识符。在全局作用域下会创建GO对象。如果您还不了解作用域的话可以移步这篇文章:作用域你真的了解吗? - 掘金 (juejin.cn)

代码是一行一行执行的,当v8扫描到函数的声明时,并不会进行编译,当碰到函数的调用时,v8才会进行编译,他会先找到这个函数的声明,并且创建AO对象。

OK,当我们了解这些之后,我们可以完整的分析一下上述代码v8是如何理解的。

1
2
3
4
5
php复制代码var global = 100;
function fn(){
console.log(global);
}
fn();

首先,v8会先创建GO对象,然后在全局作用域下找所有的变量声明,将变量名当做key,值为undefined存入GO对象中,所以现在GO对象中有一个global属性,然后在全局中找函数声明,将函数名作为key,值为函数体存入,现在GO对象中有global和fn。执行语句,将global的值更新为100,然后开始执行函数体fn(),在执行fn()函数体之前,会进行编译,v8会创建AO对象,然后在函数作用域下找所有的形参和变量名,将形参和变量名作为key,值为undefined存入AO对象中,然后就是形参和实参统一,将AO对象形参key的值更新为实参的值,然后在函数体内找函数声明,将函数名作为AO的属性名值为该函数体,并且该AO对象指向会指向GO对象。这个函数编译完成,执行fn,打印global,会先在当前作用域AO中查找,如果没有找到,则会在上一级作用域中查找,直到全局作用域中查找。如果全局作用域中也没有找到,则会报错,。在全局中找到一个global值为100,所以打印100。如果该函数体fn内还有函数执行,会再次执行上述过程,在执行语句调用前编译并且创建新的AO对象并且指向上一个AO对象也就是fn的作用域。

1
2
3
4
5
6
7
8
javascript复制代码//GO AO 对象更新过程
GO:{
global:undefined --> 100,
fn:function(){}
}
AO:{
//找不到global变量,去上一级GO作用域找
}

当我们知道V8的编译原理之后,如果碰到以下问题,那我们就能神挡杀神,佛挡杀佛。

1
2
3
4
5
6
7
8
9
10
11
12
13
javascript复制代码function fn(a){
console.log(a);//function a(){}
var a = 123;
console.log(a);//123
function a(){}
console.log(a);//123
var b = function(){}
console.log(b);//function(){}
function c(){}
var c = a;
console.log(c);//123
}
fn(1);

问打印的是什么?把自己想像成v8,看看他是怎么理解的。

1
2
3
4
5
6
7
8
9
javascript复制代码//赋值过程
GO{
fn:fn(){}
}
AO{
a:undefined --> 1 --> function a(){} -->123
b:undefined --> function(){}
c:undefined -->function c(){} -->123
}

总结

今天我们学习了v8的编译过程,主要可以分成两种:

1、发生在全局

a.创建GO对象

b.找变量声明,将变量名key和值undefined存入GO对象中

c.在全局找函数声明,将函数名作为GO的属性名,值为该函数体

d.执行函数体,函数体中使用变量时,会先在当前作用域中查找,如果没有找到,则会在上一级作用域中查找,直到全局作用域中查找。如果全局作用域中也没有找到,则会报错。

全局代码先编译,然后执行,在执行过程中可能会碰到其他函数调用,函数调用时,会先编译该函数,然后执行。

2、发生在函数体内

a.创建函数作用域AO对象,找形参和变量名,将变量名key和值undefined存入AO对象中

b.形参和实参统一

c.在函数体内找函数声明,将函数名作为AO的属性名,值为该函数体

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Tapable的神秘之处-源码解析(2) Tapable的神

发表于 2024-04-27

Tapable的神秘之处-源码解析(2)

前言:研究webpack过程中,发现tapable在其中占据了很重要的成分,所以就看看它的奥妙之处

Tapable是一个在webpack中被广泛使用的核心模块,它提供了一组灵活的钩子函数,可以在不同的生命周期中插入自定义的逻辑。通过使用Tapable,我们可以轻松地实现各种功能,从简单的插件拓展到复杂的编译过程优化。在这篇文章中,我们将深入探索Tapable的神秘之处,了解它的底层实现。

仓库地址:github.com/webpack/tap…

源码目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
js复制代码tapable
├─ .babelrc
├─ .editorconfig
├─ .eslintrc
├─ .gitattributes
├─ .gitignore
├─ .prettierrc.js
├─ .travis.yml
├─ README.md
├─ lib
│ ├─ AsyncParallelBailHook.js
│ ├─ AsyncParallelHook.js
│ ├─ AsyncSeriesBailHook.js
│ ├─ AsyncSeriesHook.js
│ ├─ AsyncSeriesLoopHook.js
│ ├─ AsyncSeriesWaterfallHook.js
│ ├─ Hook.js
│ ├─ HookCodeFactory.js
│ ├─ HookMap.js
│ ├─ index.js
│ ├─ MultiHook.js
│ ├─ SyncBailHook.js
│ ├─ SyncHook.js
│ ├─ SyncLoopHook.js
│ ├─ SyncWaterfallHook.js
│ ├─ util-browser.js
│ └─ __tests__
│     ├─ AsyncParallelHooks.js
│     ├─ AsyncSeriesHooks.js
│     ├─ Hook.js
│     ├─ HookCodeFactory.js
│     ├─ HookStackOverflow.js
│     ├─ HookTester.js
│     ├─ MultiHook.js
│     ├─ SyncBailHook.js
│     ├─ SyncHook.js
│     ├─ SyncHooks.js
│     ├─ SyncWaterfallHook.js
│     └─ __snapshots__
│       ├─ AsyncParallelHooks.js.snap
│       ├─ AsyncSeriesHooks.js.snap
│       ├─ HookCodeFactory.js.snap
│       └─ SyncHooks.js.snap
├─ LICENSE
├─ package.json
├─ tapable.d.ts
└─ yarn.lock

书接上文的Tapable的神秘之处-源码解析(1),今天我们继续来看看lib目录中的其他文件

工具方法lib/HookMap.js

代码里定义个一个HookMap类的构造函数 接受两个可选参数:factory(工厂函数)和name(HookMap的实例名称)。在构造函数中,初始化了一些实例属性,包括_map、name、_factory、_interceptors等,并设置了默认的调用方法(call、callAsync、promise)

1
2
3
4
5
6
7
8
9
10
11
js复制代码初始化配置部分
constructor(factory, name = undefined) {
   // 内部映射用于存储和管理钩子信息
   this._map = new Map();
   // hookMap实例名称标识,用于后续引用。
   this.name = name;
   // 传入的工厂函数 factory 赋值给 this._factory,这样以后就可以用来创建新的钩子。
   this._factory = factory;
   // 用于存储拦截器,拦截器可以在钩子创建过程中修改它们或者在钩子触发事件之前做一些操作
   this._interceptors = [];
}
1
2
3
4
5
6
7
8
9
10
11
js复制代码HookMap.prototype.tap = util.deprecate(function(key, options, fn) {
return this.for(key).tap(options, fn);
}, "HookMap#tap(key,…) is deprecated. Use HookMap#for(key).tap(…) instead.");

HookMap.prototype.tapAsync = util.deprecate(function(key, options, fn) {
return this.for(key).tapAsync(options, fn);
}, "HookMap#tapAsync(key,…) is deprecated. Use HookMap#for(key).tapAsync(…) instead.");

HookMap.prototype.tapPromise = util.deprecate(function(key, options, fn) {
return this.for(key).tapPromise(options, fn);
}, "HookMap#tapPromise(key,…) is deprecated. Use HookMap#for(key).tapPromise(…) instead.");

get(key)方法和for(key)方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
js复制代码get(key) {
   return this._map.get(key);
}

for(key) {
   // 寻找this._map里面是否存在对应key的钩子,如果寻找的到,直接就返回现有的钩子
   const hook = this.get(key);
   if (hook !== undefined) {
       return hook;
  }
// 调用_factory函数创建一个新的钩子(按需创建)
   let newHook = this._factory(key);
// 遍历所有的拦截器,并且新的钩子注入,方便拦截器可以修改钩子的状态信息
   const interceptors = this._interceptors;
   for (let i = 0; i < interceptors.length; i++) {
       newHook = interceptors[i].factory(key, newHook);
  }
// 存储到内部映射中
   this._map.set(key, newHook);
   return newHook;
}
  • get(key) 方法: 这个方法接受一个键(key)作为参数,并尝试从HookMap实例的内部映射(_map)中检索与这个键相关联的钩子。如果找到了这个钩子,它就会被返回;如果没有找到,方法会返回undefined。
  • for(key) 方法: 这个方法也接受一个键(key)作为参数,目的是要确保与这个键相关联的钩子存在。如果内部映射中已经有了对应的钩子,它就会直接返回这个现有的钩子。

如果找不到,方法将执行以下步骤:

1. 调用`_factory`函数创建一个新的钩子。这个工厂函数是在`HookMap`构造时传入的,用于按需创建钩子。
2. 遍历`_interceptors`数组,这个数组包含了所有的拦截器。拦截器是在钩子创建过程中可以介入的对象,每个拦截器都有可能修改新创建的钩子。
3. 对每个拦截器调用`factory`方法,传入当前的键和新创建的钩子。这允许拦截器根据键和钩子的当前状态来修改钩子。
4. 将新创建(并可能被拦截器修改过的)钩子与键关联并存储到内部映射中。
5. 返回新创建的钩子。

这样,for方法确保无论何时请求特定的键,都会得到一个钩子,如果这个钩子不存在,就现场创建一个。这允许HookMap按需动态地管理钩子,而无需预先定义所有可能的钩子。

intercept(interceptor)方法

1
2
3
4
5
6
7
8
9
10
js复制代码intercept(interceptor) {
   this._interceptors.push(
       Object.assign(
          {
               factory: defaultFactory
          },
           interceptor
      )
  );
}

该方法的作用是向 HookMap 实例的内部拦截器数组 _interceptors 添加一个新的拦截器对象

结尾

这个 HookMap 类为 tapable 提供了以下功能和特性:

  • 管理多个钩子实例:HookMap管理多个不同类型的钩子实例。它提供了一种集中管理和访问钩子的机制,可以方便地对多个钩子进行操作和触发。
  • 提供钩子的注册和访问接口:HookMap提供了钩子的注册和访问接口。通过注册接口,可以将钩子实例添加到HookMap中,并指定一个标识符来区分不同的钩子。通过访问接口,可以根据标识符获取对应的钩子实例,方便进行后续的操作。
  • 支持批量操作:HookMap支持批量操作多个钩子实例。可以一次性触发或执行多个钩子,避免了对每个钩子实例进行独立操作的麻烦。
  • 提供钩子的生命周期管理:HookMap可以跟踪和管理钩子的生命周期。它可以在钩子注册或注销时执行相应的生命周期钩子函数,用于进行一些额外的处理或清理操作。

综上所述,HookMap在tapable中的作用和意义是提供了一个集中管理和操作多个钩子实例的机制,方便进行批量操作和生命周期管理。它是实现插件系统和事件机制的重要组成部分。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…456…399

开发者博客

3990 日志
1304 标签
RSS
© 2024 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%