【Code】《代码整洁之道》笔记-Chapter13-并发编程
第13章 并发编程
“对象是过程的抽象。线程是调度的抽象。”
编写整洁的并发程序很难——非常难,而编写在单线程中执行的代码却简单得多。编写表面上看似不错、深入进去却支离破碎的多线程代码也简单,但是系统一旦遭受压力,这种代码就扛不住了。
本章将讨论并发编程的需求及其困难之处,并给出一些应对这些难点、编写整洁的并发代码的建议。最后,我们将讨论与测试并发代码有关的问题。
整洁的并发编程是个复杂话题,值得用一整本书来讨论。本书只做概览,并在附录A中提供更详细的指引。如果你只是对并发好奇,阅读本章就足够了。如果你需要更深入地理解并发,就应读完整个附录A。
13.1 为什么要并发
并发是一种解耦策略。它帮助我们把做什么(目的)和何时做(时机)分解开。在单线程应用中,目的与时机紧密耦合,很多时候只要查看栈追踪即可断定应用程序的状态。调试这种系统的程序员可以设定断点或者断点序列,通过查看到达哪个断点来了解系统状态。
解耦目的与时机能明显地改进应用程序的吞吐量和结构。从结构的角度来看,应用程序看起来更像是许多台协同工作的计算机,而不是一个大循环。系统因此会更被易于理解,给出了许多切分关注面的有力手段。
例如,Web应用的Servlet标准模式。这类系统运行于Web或EJB容器的保护伞之下,Web或EJB为你部分地处理并发问题。当有Web请求时,servlet就会异步执行。servlet程序员无须管理所有的请求。原则上,每次servlet是在自己的小世界中执行,与其他servlet的执行是分离的。
当然,如果只是那么简单,也就没必要写这一章了。实际上,Web容器提供的解耦手段离完美还差得远。servlet程序员得非常警惕、非常小心地保证并发程序不出错。同样,servlet模式的结构性好处还是很明显的。
但结构并非采用并发的唯一动机。有些系统对响应时间和吞吐量有要求,需要手工编写并发解决方案。例如,考虑一个单线程信息聚合程序,它从许多Web站点获取信息,再合并写入日志中。因为该系统是单线程的,它会逐个访问Web站点,在开始下一个访问之前等待当前站点访问完毕。每天的执行时间必须少于24小时。然而,随着要访问的站点越来越多,采集所有数据花费的时间也越来越多,最终超过了24小时的限制。单线程程序许多时间花在等待Web套接字I/O结束上面。通过采用同时访问多个站点的多线程算法,就能改进性能。
或者,考虑某个每次花费1秒处理一个用户请求的系统。该系统在用户量较少的时候响应及时,但随着用户数增加,系统的响应时间也增加了。没人想排在150个人后面!通过并发处理多个用户请求,就能改善系统响应时间。
再或者,考虑某个解释大量数据集,但只在处理完全部数据后给出一个完整解决方案的系统。或许可以在独立的计算机上处理每个数据集,那样的话许多数据集就能并行地得到处理。
迷思与误解
看来有足够的理由采用并发方案。然而,如前文所述,并发编程很难。如果你不那么细心,就会搞出不堪入目的东西来。看看以下常见的迷思和误解:
(1)并发总能改进性能。并发有时能改进性能,但只在多个线程或处理器之间能分享大量等待时间的时候管用。事情没那么简单。
(2)编写并发程序无须修改设计。事实上,并发算法的设计有可能与单线程系统的设计极不相同。目的与时机的解耦往往会对系统结构产生巨大影响。
(3)在采用Web或EJB容器的时候,理解并发问题并不重要。实际上,你最好了解容器在做什么,了解如何应对本章后面将提到的并发更新、死锁等问题。
下面是一些有关编写并发软件的中肯说法:
-
并发会在性能和编写额外代码上增加一些开销;
-
正确的并发是复杂的,即便对于简单的问题也是如此;
-
并发缺陷并非总能重现,所以常被看作偶发事件而忽略,未被当作真的缺陷看待;
-
并发常常需要对设计策略做根本性修改。
13.2 挑战
并发编程为何如此之难?来看看下面这个小型类:
public class X {private int lastIdUsed;public int getNextId() {return ++lastIdUsed;}
}
例如,创建X
的一个实体,将lastIdUsed
设置为42,在两个线程中共享这个实体。假设这两个线程都调用getNextId()
方法,结果可能有以下3种输出:
-
线程一得到值43,线程二得到值44,lastIdUsed为44;
-
线程一得到值44,线程二得到值43,lastIdUsed为44;
-
线程一得到值43,线程二得到值43,lastIdUsed为43。
第三种结果令人惊异,当两个线程相互影响时就会出现这种情况。这是因为线程在执行那行Java代码时有许多可能的路径,有些路径会产生错误的结果。有多少种不同路径呢?要真正回答这个问题,需要理解Just-In-Time编译器如何对待生成的字节码,还要理解Java内存模型认为什么东西具有原子性。
简单回答一下,就生成的字节码而言,对于在getNextId
方法中执行的那两个线程,有12870种不同的可能执行路径。如果lastIdUsed
的类型从int
变为long
,则可能路径的数量将增至2704156种。当然,多数路径都能得到正确结果,问题是其中一些不能得到正确结果。
13.3 并发防御原则
下面给出一系列防御并发代码问题的原则和技巧。
13.3.1 单一权责原则
单一权责原则(SRP)认为,方法/类/组件应当只有一个修改的理由。并发设计足够复杂,以至于这种复杂足以成为其需要修改的理由,所以确实应该从其他代码中分离出来。不幸的是,并发实现细节常常直接嵌入其他生产代码中。下面是要考虑的一些问题:
-
并发相关代码有自己的开发、修改和调优生命周期;
-
并发相关代码有自己要应对的挑战,它和非并发相关代码不同,而且往往更为困难;
-
即便没有增加周边应用程序的负担,写得不好的并发代码可能的出错方式数量也已经足具挑战性。
建议:分离并发相关代码与其他代码。
13.3.2 推论:限制数据作用域
如我们所见,两个线程修改共享对象的同一字段时,可能会互相干扰,导致未预期的行为。解决方案之一是采用关键字synchronized
在代码中保护一块使用共享对象的临界区(critical section)。限制临界区的数量很重要。更新共享数据的地方越多,就越可能:
-
你会忘记保护一个或多个临界区——破坏了修改共享数据的代码;
-
得多花力气保证一切都受到有效防护(违反了DRY原则);
-
很难找到错误源,也很难判断错误源。
建议:谨记数据封装;严格限制对可能被共享的数据的访问。
13.3.3 推论:使用数据副本
避免共享数据的好方法之一是,从开始就避免共享数据。在某些情形下,有可能复制对象并以只读方式对待。在另外的情况下,有可能复制对象,从多个线程收集所有副本的结果,并在单个线程中合并这些结果。
如果有避免共享数据的简易手段,结果代码就会大大减小导致错误的可能。你可能会关心创建额外对象的成本,值得试验一下看看那是否真是个问题。然而,假使使用对象副本能避免代码同步执行,则因避免了锁定而省下的价值有可能补偿得上额外的创建成本和垃圾收集开销。
13.3.4 推论:线程应尽可能地独立
让每个线程在自己的世界中存在,而不与其他线程共享数据。每个线程处理一个客户端请求,从不共享的源头接纳所有请求数据,存储为本地变量。这样一来,每个线程都像是世界中的唯一线程,没有同步需要。
例如,HttpServlet
的子类接收所有以参数形式传递给doGet
方法和doPost
方法的信息。每个Servlet
都像拥有独立虚拟机一般运行。只要Servlet
中的代码只使用本地变量,Servlet
就不会导致同步问题。当然,多数使用Servlet
的应用程序最终都还是会用到类似数据库连接之类的共享资源。
建议:尝试将数据分解为可被独立线程(可能在不同处理器上)操作的独立子集。
13.4 了解Java库
与之前的版本相比,Java 5提供了许多并发开发方面的改进。在用Java 5编写线程代码时,要注意以下几点:
- 使用类库提供的线程安全群集;
- 使用executor框架执行无关任务;
- 尽可能使用非锁定解决方案;
- 有几个类并不是线程安全的。
线程安全群集
当Java还年轻时,Doug Lea编写了《Java并发编程》(Concurrent Programming in Java)教程,同时开发了几个线程安全群集,这些代码后来成为JDK中java.util.concurrent
包的一部分。该代码包中的群集对于多线程解决方案是安全的,执行良好。实际上,在几乎所有情况下,ConcurrentHashMap
实现都比HashMap
表现得好,它还支持同步并发读写,也拥有支持非线程安全的合成操作的方法。如果部署环境是Java 5,可以采用ConcurrentHashMap
。
还有几个支持高级并发设计的类。以下是其中的一小部分,如表13-1所示。
表13-1 支持高级并发设计的类(部分)
类 | 作 用 |
---|---|
ReentrantLock | 可在一个方法中获取、在另一个方法中释放的锁 |
Semaphore | 经典的“信号”的一种实现,有计数器的锁 |
CountDownLatch | 在释放所有等待的线程之前,等待指定数量事件发生的锁。这样,所有线程都平等地几乎同时启动 |
建议:检读可用的类。对于Java,需要掌握java.util.concurrent
、java.util. concurrent.atomic
和java.util.concurrent.locks
。
13.5 了解执行模型
有几种在并发应用中切分行为的途径。要讨论这些途径,我们需要理解一些基础定义,如表13-2所示。
表13-2 基础定义
定 义 | 描 述 |
---|---|
限定资源 | 并发环境中有着固定尺寸或数量的资源。例如,数据库连接和固定尺寸读/写缓存等 |
互斥 | 每一时刻仅有一个线程能访问共享数据或共享资源 |
线程饥饿 | 一个或一组线程在很长时间内或永久被禁止。例如,总是让执行得快的线程先运行,假如执行得快的线程没完没了,则执行时间长的线程就会“挨饿” |
死锁 | 两个或多个线程互相等待执行结束。每个线程都拥有其他线程需要的资源,如果得不到其他线程拥有的资源,就无法终止 |
活锁 | 执行次序一致的线程,每个都想要起步,但发现其他线程已经“在路上”。由于竞步的原因,线程会持续尝试起步,但在很长时间内却无法如愿,甚至永远无法启动 |
有了这些定义,我们就能讨论在并发编程中用到的几种执行模型了。
13.5.1 生产者-消费者模型
一个或多个生产者线程创建某些工作,并置于缓存或队列中。一个或多个消费者线程从队列中获取并完成这些工作。生产者和消费者之间的队列是一种限定资源。
13.5.2 读者-作者模型
当存在一个主要为读者线程提供信息源,但只偶尔被作者线程更新的共享资源,吞吐量就会是个问题。增加吞吐量,会导致线程饥饿和过时信息的累积。更新会影响吞吐量。协调读者线程,不去读作者线程正在更新的信息(反之亦然),这是一种辛苦的平衡工作。作者线程倾向于长期锁定许多读者线程,从而导致吞吐量问题。
挑战之处在于平衡读者线程和作者线程的需求,实现正确操作,提供合理的吞吐量,避免线程饥饿。
13.5.3 宴席哲学家
想象一下,一群哲学家环坐在圆桌旁。每个哲学家的左手边放了一把叉子。桌面中央摆着一大碗意大利面。哲学家们思索良久,直至肚子饿了。每个人都要拿起叉子吃饭。但除非手上有两把叉子,否则就没法进食。如果左边或右边的哲学家已经取用一把叉子,中间这位就得等到别人吃完、放回叉子。每位哲学家吃完后,就将两把叉子放回桌面,直到肚子再饿。
用线程代替哲学家,用资源代替叉子,就变成了许多企业级应用中进程竞争资源的情形。如果没有用心设计,这种竞争式系统就会遭遇死锁、活锁、吞吐量和效率降低等问题。
你可能遇到的并发问题,大多数都是以上3种模型的变种。请研究并使用这些算法,这样,遇到并发问题时你就能有解决问题的准备了。
建议:学习这些基础算法,理解其解决方案。
13.6 警惕同步方法之间的依赖
同步方法之间的依赖会导致并发代码中的狡猾缺陷。Java语言有synchronized
概念,可以用来保护单个方法。然而,如果在同一共享类中有多个同步方法,系统就可能写得不太正确了。
建议:避免使用一个共享对象的多个方法。
有时必须使用一个共享对象的多个方法。在这种情况发生时,有以下3种写对代码的手段:
-
基于客户端的锁定——客户端代码在调用第一个方法前锁定服务端,确保锁的范围覆盖了调用最后一个方法的代码;
-
基于服务端的锁定——在服务端内创建锁定服务端的方法,调用所有方法,然后解锁。让客户端代码调用新方法;
-
适配服务端——创建执行锁定的中间层。这是一种基于服务端的锁定的例子,但不修改原始服务端代码。
13.7 保持同步区域微小
关键字synchronized
制造了锁。同一个锁维护的所有代码区域在任一时刻保证只有一个线程执行。锁是昂贵的,因为它们带来了延迟和额外开销。所以我们不愿将代码扔给synchronized
语句了事。另外,临界区应该被保护起来。所以,应该尽可能少地设计临界区。
有些天真的程序员想通过扩大临界区面积达到这个目的。然而,如果将同步延展到最小临界区范围之外,会加剧资源争用,降低执行效率。
建议:尽可能减小同步区域。
13.8 很难编写正确的关闭代码
编写永远运行的系统,与编写运行一段时间后平静地关闭的系统是两码事。
平静关闭很难做到。常见问题与死锁有关,线程一直等待永远不会到来的信号。
例如,想象一个系统中有一个父线程分裂出数个子线程,父线程等待所有子线程结束后才会释放资源并关闭。如果其中一个子线程发生死锁会怎样?父线程将一直等待下去,而系统就永远不能关闭。
或者,考虑一个被指示关闭的类似系统。父线程告知全体子线程放弃任务并结束。如果其中两个子线程正以生产者-消费者模型操作会怎样呢?假设生产者线程从父线程处接收到信号,并迅速关闭,而消费者线程可能还在等待生产者线程发来消息,于是就被锁定在无法接收到关闭信号的状态中,它会死等生产者线程,永不结束,从而导致父线程也无法结束。
这类情形并非不常见。如果你要编写涉及平静关闭的并发代码,就多预留一些时间搞对关闭过程。
建议:尽早考虑关闭问题,尽早令其工作正常。这会花费比你预期的更多的时间。检视既有算法,因为这可能会比想象中难得多。
13.9 测试线程代码
证明代码的正确性不切实际。测试并不能确保正确性,然而,好的测试却能尽量降低风险。这对于所有单线程解决方案都是对的。当有两个或多个线程使用同一代码段和共享数据,事情就变得非常复杂了。
建议:编写有潜力曝露问题的测试,在不同的编程配置、系统配置和负载条件下频繁运行。如果测试失败,就跟踪错误,别因为后来测试通过了后来的运行就忽略失败。
有一大堆问题要考虑。下面是一些精练的建议:
-
将伪失败看作可能的线程问题;
-
先使非线程代码可工作;
-
编写可插拔的线程代码;
-
编写可调整的线程代码;
-
运行多于处理器数量的线程;
-
在不同平台上运行;
-
调整代码并强迫错误发生。
13.9.1 将伪失败看作可能的线程问题
线程代码导致“不可能失败的”失败。多数开发者缺乏有关线程如何与其他代码(可能由其他作者编写)互动的直觉。线程代码中的缺陷可能在一千或一百万次执行中才会显现一次。重复执行想要复现问题令人沮丧。所以开发者常常会将失败归咎于宇宙射线、硬件错误或其他“偶发事件”。最好假设这种偶发事件根本不存在。“偶发事件”被忽略得越久,代码就越有可能搭建于不完善的基础之上。
建议:不要将系统错误归咎于偶发事件。
13.9.2 先使非线程代码可工作
这看起来太浅显,但强调一下不无益处。确保线程之外的代码可工作,通常,这意味着创建由线程调用的POJO。POJO与线程无涉,所以可在线程环境之外测试。能放进POJO中的代码越多越好。
建议:不要同时追踪非线程缺陷和线程缺陷。确保代码在线程之外可工作。
13.9.3 编写可插拔的线程代码
编写可在数个配置环境下运行的线程代码:
-
单线程与多个线程在执行时不同的情况;
-
线程代码与实物或测试替身互动;
-
用运行快速、缓慢和有变动的测试替身执行;
-
将测试配置为能运行一定数量的迭代。
建议:编写可插拔的线程代码,这样就能在不同的配置环境下运行。
13.9.4 编写可调整的线程代码
要获得良好的线程平衡,常常需要反复试验。一开始,在不同的配置环境下监测系统性能。要允许线程数量可调整。在系统运行时允许线程发生变动。允许线程依据吞吐量和系统使用率自我调整。
13.9.5 运行多于处理器数量的线程
系统在切换任务时会发生一些事。为了促使任务交换的发生,运行多于处理器或处理器核心数量的线程。任务交换越频繁,越有可能找到错过临界区或导致死锁的代码。
13.9.6 在不同平台上运行
2007年,我们开发了一套关于并发编程的课程。该课程主要在OS X下开发,在运行于虚拟机的Windows XP上展示。对于用于演示的测试失败条件,在OS X上要比在XP上失败得更频繁。
被测试的代码已知是不正确的。这正强调了不同操作系统有着不同线程策略的事实,不同的线程策略影响了代码的执行。在不同环境中,多线程代码的行为也不一样。应该在所有可能部署的环境中运行测试。
建议:尽早并经常地在所有目标平台上运行线程代码。
13.9.7 装置试错代码
并发代码中藏有缺陷,这并不罕见。简单的测试往往无法曝露这些缺陷。实际上,缺陷经常隐藏于一般处理过程中,可能好几个小时、好几天甚至好几个星期才会跳出来一次!
线程中的缺陷之所以如此不频繁、偶发、难以重现,是因为在几千个穿过脆弱区域的可能路径当中,只有少数路径会真的导致失败。经过会导致失败的路径的可能性惊人地低。所以,监测与调试也非常难。
怎么才能增加捕捉住如此罕见之物的机会?可以装置代码,增加对Object.wait()
、Object.sleep()
、Object.yield()
和Object.priority()
等方法的调用,改变代码执行顺序。
这些方法都会影响执行顺序,从而增加监测到缺陷的可能性。有问题的代码最好尽早测试,尽可能多地使其通不过测试。
有两种装置代码的方法:
-
硬编码;
-
自动化。
13.9.8 硬编码
你可以手工向代码中插入wait()
、sleep()
、yield()
和priority()
的调用。在测试某段棘手的代码时,正当如此操作。
下面是一个例子:
public synchronized String nextUrlOrNull() {if(hasNext()) {String url = urlGenerator.next();Thread.yield(); // inserted for testing.updateHasNext();return url;} return null;
}
插入对yield()
的调用,将改变代码的执行路径,由此可能导致代码在以前未失败过的地方失败。如果代码的确出错,那并非是因为你插入了yield()
方法调用。代码出错了,才是失败的原因。
这种方法有许多毛病:
-
你得手工找到合适的地方来插入方法调用;
-
你怎么知道在哪里插入调用、插入什么调用?
-
不必要地在产品环境中留下这类代码,将拖慢代码执行速度;
-
这是一种无的放矢的手段。你可能找不到缺陷。实际上,这不在你的把握之中。
我们所需要的,是一种在测试中但不在生产中实现的手段。我们还需要为多次运行方便地调整配置,从而增加总的发现错误的机会。
无疑,如果将系统分解为对线程及控制线程的类一无所知的POJO,就能更容易地找到装置代码的位置,而且,还能创建许多个以不同方式调用sleep
、yield
等方法的POJO测试。
13.9.9 自动化
可以使用Aspect-Oriented Framework、CGLIB或ASM之类的工具,通过编程来装置代码。例如,可以使用有单个方法的类:
public class ThreadJigglePoint {public static void jiggle() {}
}
可以在代码的不同位置调用这个方法:
public synchronized String nextUrlOrNull() {if(hasNext()) {ThreadJigglePoint.jiggle();String url = urlGenerator.next();ThreadJigglePoint.jiggle();updateHasNext();ThreadJigglePoint.jiggle();return url;} return null;
}
如此,你就得到了一个随机选择无所作为、睡眠或让步的方面。
或者,想象ThreadJigglePoint
类有两种实现。第一种实现jiggle
什么都不做,在生产环境中使用。第二种实现生成一个随机数,在睡眠、让步或径直执行间做选择。如果上千次地做这种随机测试,大概就能找到一些缺陷的根源。假如测试都通过了,至少你可以说自己已谨慎对待。这种方法看似过于简单,却是替代复杂工具的一种可选方案。
有一种叫作ConTest的工具,由IBM公司开发,能做类似的事情,但做法稍微复杂些。
要点是让代码发生“异动”,从而使线程以不同次序执行。编写良好的测试与“异动”组合,能有效地增加发现错误的机会。
建议:使用异动策略搜出错误。
13.10 小结
并发代码很难写正确。加入多线程和共享数据后,简单的代码也会变成噩梦。要编写并发代码,就得严格地编写整洁的代码,否则将面临细微和不频繁发生的失败。
第一要诀是遵循单一权责原则。将系统切分为分离了线程相关代码和线程无关代码的POJO。确保在测试线程相关代码时只是在测试,而没有做其他事情。线程相关代码应该保持短小和目的集中。
了解并发问题的可能原因:对共享数据的多线程操作,或使用了公共资源池。类似平静关闭或停止循环之类的边界情况尤其棘手。
学习类库,了解基本算法。理解类库提供的与基础算法类似的解决问题的特性。
学习如何找到必须锁定的代码区域并锁定之。不要锁定不必锁定的代码。避免从锁定区域中调用其他锁定区域。这需要深刻理解某物是否已共享。尽可能减少共享对象和共享范围。修改对象的设计,向客户代码提供共享数据,而不是迫使客户代码管理共享状态。
问题会跳出来。那种在早期没跳出来的问题往往是偶发的。这种所谓的偶发问题,通常仅在高负载下出现或者偶然出现。所以,你需要在不同平台上,以不同配置持续重复运行线程代码。跟随TDD三要则而来的可测试性意味着某种程度的可插拔性,从而提供了在大量不同配置下运行代码的必要支持。
如果花点时间装置代码,就能极大地提升发现错误代码的机会。可以手工做,也可以使用某种自动化技术。尽早这么做。在将线程代码投入生产环境前,就要尽可能多地运行它。
只要采用了整洁的做法,做对的可能性就会有翻天覆地的提高。
相关文章:
【Code】《代码整洁之道》笔记-Chapter13-并发编程
第13章 并发编程 “对象是过程的抽象。线程是调度的抽象。” 编写整洁的并发程序很难——非常难,而编写在单线程中执行的代码却简单得多。编写表面上看似不错、深入进去却支离破碎的多线程代码也简单,但是系统一旦遭受压力,这种代码就扛不…...
TDengine 可靠性保障:数据持久化与容灾备份(一)
一、引言 在数字化浪潮席卷全球的当下,数据已成为企业和组织最为关键的资产之一。无论是互联网企业记录用户的行为数据,还是金融机构存储交易信息,又或是工业领域监测设备的运行状态,数据的可靠性直接关乎到业务的正常运转、决策…...
AWTK-MVVM 如何让多个View复用一个Model记录+关于app_conf的踩坑
前言 有这么一个业务,主界面点击应用窗口进入声纳显示界面,声纳显示界面再通过按钮进入菜单界面,菜单界面有很多关于该声纳显示界面的设置项,比如量程,增益,时间显示,亮度,对比度等…...
第四节:React Hooks进阶篇-useEffect依赖项为空数组[]与不写的区别
陷阱题:闭包问题、Stale Closure举例 一、依赖项为空数组[]与不写的核心区别 行为空数组[]不写依赖项执行时机仅在组件挂载时执行一次(类似componentDidMount)组件每次渲染后都执行(类似componentDidUpdate)更新触发…...
25级总分413数学一142专业124东南大学820考研经验电子信息通信工程,真题,大纲,参考书。
我是南京理工大学的本科生,25 考研一战东大,政治 69,英一 78,数一 142,专业课(820)124,总分 413。我从 3 月正式开始备考,专业课跟着无线电论坛jenny 老师进行学习&#…...
Docker Desktop磁盘镜像位置用途解析
在设置里面的资源中有个磁盘镜像位置的配置,这个目录默认位置是:C:\Users\haitao.luo\AppData\Local\Docker\wsl\disk,这里面对应的是一个docker_data.vhdx,虚拟磁盘文件,因为是在c盘上,所以可能后面下载镜…...
android display 笔记(十)surfaceflinger与HWC的关系
在 Android 图形系统中,SurfaceFlinger 和 Hardware Composer (HWC) 是紧密协作的两个核心组件,共同负责屏幕内容的合成与显示。它们的关系可以用 “决策者与执行者” 来概括: 首先HWC中 DEVICE:指“显示设备硬件”(…...
#MES系统运维问题分析思路
一套适用于90% MES运维现场问题的排查分析思维模型,叫做: 🔍 MES系统问题分析七步法(现场实战适用) ✅ 第一步:明确问题现象(What) 问题要说清楚,“不能操作”这种模糊描…...
基于FPGA的六层电梯智能控制系统 矩阵键盘-数码管 上板仿真均验证通过
基于FPGA的六层电梯智能控制系统 前言一、整体方案二、软件设计总结 前言 本设计基于FPGA实现了一个完整的六层电梯智能控制系统,旨在解决传统电梯控制系统在别墅环境中存在的个性化控制不足、响应速度慢等问题。系统采用Verilog HDL语言编程,基于Cyclo…...
FPGA上实现SD卡连续多块读的命令
在FPGA上实现SD卡连续多块读的命令 CMD17命令一次只能读取1个块 CMD18命令一次可以连续读取多个块,直到停止命令CMD12 CMD18命令读的块数程序可任意设置 目录 前言 一、SD卡多块读命令CMD18 二、停止读命令CMD12 三、SD卡初始化SD卡连续块读操作的verilog代码 …...
AI 大语言模型 (LLM) 平台的整体概览与未来发展
📋 分析报告:AI 大语言模型 (LLM) 平台的整体概览与未来发展 自动生成的结构化分析报告 💻 整体概述:AI LLM 平台的市场现状与发展动力 随着人工智能技术的飞速发展,大语言模型(Large Language Models, L…...
【技术派部署篇】Windows本地部署技术派
一、技术派简介 技术派是一个采用 Spring Boot、MyBatis-Plus、MySQL、Redis、ElasticSearch、MongoDB、Docker、RabbitMQ 等技术栈的社区系统,其 1.0 版已正式上线。该项目的技术栈按阶段集成引入,开发者可根据自身需求选择不同版本进行学习。 二、环…...
asm汇编语言源代码之-获取环境变量
提供1个子程序: 1. 读取环境变量 GETENVSTR 具体功能及参数描述如下 GETENVSTR PROC FAR ;IN: DSPSP SEG. ; ES:BX -> ENV VAR NAME ;OUT: DS:DX -> ENV VAR VALUE; IF DX0FFFFH, NOT FOUND ; more source code at http://www.ahjoe.com/source/srcdown.aspPU…...
消失的它:揭开 CoreData 托管对象神秘的消失之谜(上)
概述 使用 CoreData 作为 App 持久存储“定海神针”的小伙伴们想必都知道,我们需要将耗时的数据库查询操作乖巧的放到后台线程中,以便让主线程负责的 UI 获得风驰电掣般地享受。 不过,如何将后台线程中查询获得的托管对象稳妥的传送至主线程…...
Python中如何用正则表达式精准匹配IP地址?
在网络编程和数据处理时,我们经常需要从文本中提取或验证IP地址。Python的正则表达式(re模块)是完成这个任务的利器。但你知道怎么写才能准确匹配各种合法的IP地址吗?今天我们就来详细探讨这个问题。 为什么需要IP正则表达式? 假设你正在分…...
初识华为防火墙
防火墙配置与应用 一、防火墙的基本概念 1.防火墙的网络区域(一般认为三个区域:trust、DMZ、untrust) (1)本地区域(安全级别 100,local)(防火墙内部区域,一般不说明此…...
十二、C++速通秘籍—静态库,动态库
上一章节: 十一、C速通秘籍—多线程-CSDN博客https://blog.csdn.net/weixin_36323170/article/details/147055932?spm1001.2014.3001.5502 本章节代码: cpp2/library CuiQingCheng/cppstudy - 码云 - 开源中国https://gitee.com/cuiqingcheng/cppst…...
我爱学算法之——滑动窗口攻克子数组和子串难题(下)
这几道题可以说是有一点难度的,但是掌握方法以后可以说非常简单了; 一、找到字符串中所有字母异位词 题目解析 题目给定了两个字符串s和p,让我们在s中找到p的异位词的字串,并且返回这些字串的索引 **异位词:**简单来说…...
leaflet 之 获取中国某个行政区的经纬度边界(latLngBounds)
思路 在json文件中获取下面的四个点 组成东北,西南两组 { “southwest”: { “lat”: 35.950, “lng”: 120.000 },//西南方 “northeast”: { “lat”: 36.200, “lng”: 120.300 }//东北方 } 最西点经度(minLng) 最东点经度(maxLng&#x…...
鸢尾花分类的6种机器学习方法综合分析与实现
鸢尾花分类的6种机器学习方法综合分析与实现 首先我们来看一下对应的实验结果。 数据准备与环境配置 在开始机器学习项目前,首先需要准备编程环境和加载数据。以下代码导入必要的库并加载鸢尾花数据集: import numpy as np import pandas as pd impo…...
基于李永乐线性代数基础的行列式的起源于理解
起源于解方程组的过程 对于解一个二元方程组,很自然的会通过加减消元,变成下面这样 对于三元方程组,也是一样: 这一大长串,是A*x1b1这个形式时,A的值 人们为了方便记忆x未知数前这一大坨相乘后相加减的数…...
MacOs java环境配置+maven环境配置踩坑实录
oracl官网下载jdk 1.8的安装包 注意可能需要注册!!! 下载链接:下载地址点击 注意晚上就不要下载了 报错400 !!! 1.点击安装嘛 2.配置环境变量 export JAVA_HOME/Library/Java/Java…...
LeetCode 3272.统计好整数的数目:枚举+排列组合+哈希表
【LetMeFly】3272.统计好整数的数目:枚举排列组合哈希表 力扣题目链接:https://leetcode.cn/problems/find-the-count-of-good-integers/ 给你两个 正 整数 n 和 k 。 如果一个整数 x 满足以下条件,那么它被称为 k 回文 整数 。 x 是一个…...
蓝桥杯嵌入式历年省赛客观题
一.第十五届客观题 第十四届省赛 十三届 十二届...
RFID 在制造业的深度应用与未来趋势
一、引言 制造业作为国民经济的核心支柱,正面临着全球供应链重构、个性化需求激增、成本压力加剧等多重挑战。RFID(射频识别)技术以其非接触式自动识别、数据实时传输、环境适应性强等特性,成为推动制造业数字化转型的关键引擎。…...
spring--声明式事务
声明式事务 1、回顾事务 要么都成功,要么都失败! 事务在项目开发中,十分重要,涉及数据的一致性问题 确保完整性和一致性 事务ACID: 原子性:事务是原子性操作,由一系列动作组成,…...
java爬虫案例
以下是一个简单的Java爬虫案例,使用了 Jsoup 和 Apache HttpClient 两个常用的库来实现网页内容的爬取和解析。这个案例会演示如何获取网页的HTML内容、解析HTML并提取所需数据。 示例:使用Jsoup爬取网页内容 1. 添加依赖 在项目中添加以下依赖ÿ…...
博途 TIA Portal之1200做主站与有意思的板子做MODBUS_RTU通讯
做为博途的硬件,1200和1500本体都不具有串口通讯功能,只能使用扩展板或是通讯模块完成。 其中1200使用CB1241或CM1241进行串口通讯,本文将使用CM1241进行演示。 1、硬件介绍 1200的PLC一台,有意思的板子(以下简单4D板)一台。 其中1200带扩展模块CM1241 RS232;4D板使…...
01_核心系统下的技术原理解析
15年前,基本上国内的核心系统被C垄断,基本上是IBM的那套东西,场景也是比价复杂,这里不再赘述,TPS太过于庞大,技术上确实比较复杂。为此我这里抛砖引玉,说下对应的支付系统: &#x…...
【力扣hot100题】(092)最长回文串
有点难度,一开始想到的两种方法都不对,花了不少时间。 先说之前的方法: ① 遍历每个点,每个点向外扩张,如果左等于右就一直扩展直到不等。 这个方法可是可以,但我没有考虑到两个相同字母也是回文串的情况…...
第一期:[特殊字符] 深入理解MyBatis[特殊字符]从JDBC到MyBatis——持久层开发的转折点[特殊字符]
前言 🌟 在软件开发的过程中,持久层(或数据访问层)是与数据库进行交互的关键部分。早期,开发者通常使用 JDBC(Java Database Connectivity)来实现与数据库的连接与操作。虽然 JDBC 在一定程度上…...
指针的进阶2
六、函数指针数组 字符指针数组 - 存放字符指针的数组 char* arr[10] 整型指针数组 - 存放整型指针的数组 int* arr[10] 函数指针数组 - 存放函数指针的数组 void my_strlen() {} int main() {//指针数组char* ch[5];int arr[10] {0};//pa是是数组指针int (*pa)[10] &…...
Java学习——day28(Java并发工具类与线程池)
文章目录 1. 并发工具类简介1.1 ExecutorService1.2 Callable 和 Future1.3 Executors 工具类1.4 线程池优势 2. 实践:线程池执行任务并收集结果示例2.1 示例代码 3. 代码详解3.1 线程池的创建3.2 定义任务列表3.3 提交任务并收集 Future3.4 获取任务执行结果3.5 关…...
2021第十二届蓝桥杯大赛软件赛省赛C/C++ 大学 B 组
记录刷题的过程、感悟、题解。 希望能帮到,那些与我一同前行的,来自远方的朋友😉 大纲: 1、空间-(题解)-字节单位转换 2、卡片-(题解)-可以不用当组合来写,思维题 3、直…...
【数据结构】之二叉树
二叉树是我们在数据结构中学到的第一个非线性结构,是后续学习更为复杂的树、图结构的基础。本文整理了二叉树的概念定义、基本操作、遍历算法、伪代码与代码实现以及实例说明,方便大家随时查找对应。 一、定义与基本术语 二叉树是一种树形结构…...
电感、互感器、变压器和磁珠综合对比——《器件手册--电感/线圈/变压器/磁珠篇》
三、电感/线圈/变压器/磁珠 名称 定义 特点...
CLIP中的Zero-Shot Learning原理
CLIP(Contrastive Language-Image Pretraining)是一种由OpenAI提出的多模态模型,它通过对比学习的方式同时学习图像和文本的表示,并且能在多种任务中进行零样本学习(Zero-Shot Learning)。CLIP模型的核心创…...
基于 Redis 实现一套动态配置中心 DCC 服务与反射基础知识讲解
目录 动态配置中心核心价值 轻量级 Redis 方案与 ZooKeeper 的对比分析 为什么选择自定义 Redis 方案? 1. 技术决策背景 一、活动降级拦截 1. 定义与作用 2. 实现原理 二、活动切量拦截 1. 定义与作用 2. 实现原理 三、两者的核心区别 四、实际应用案例 1. 电商大促…...
vue 前端遇到问题 样式不展示
vue 前端遇到问题 样式不展示 先看接口返回有数据没 如果有数据看下 是不是 输入赋值给其他 字段 没有赋值上导致报错 所以页面没展示数据...
基于 Spring Boot + Vue 的 [业务场景] 管理系统设计与实现
技术范围:SpringBoot、Vue、SSM、HLMT、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、小程序、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:免费功能设计、开题报告、任务书、中期检查PPT、系统功能实现、代码编写、论文编写和辅导、论文…...
2025蓝桥杯JavaB组
说明 博主自己水平有限,而且答案也不一定对,下面代码和思路仅作分享。我只把我考场上做了的写出来了,有什么问题欢迎评论区交流。 A:逃离高塔 思路: 由于有了去年的经验,所以一上来我就是找规律…...
HDF5文件格式:数据类型与读写功能详解
HDF5文件格式:数据类型与读写功能详解 HDF5简介 HDF5(Hierarchical Data Format version 5)是一种用于存储和管理大量科学数据的文件格式和库。它由美国国家高级计算应用中心(NCSA)开发,具有以下特点&…...
探索 Python 的 functools 模块:缓存、属性缓存与 LRU 缓存
李升伟 编译 Python 的 functools 模块是函数式编程爱好者的宝库,提供了许多工具来提升代码的效率和优雅性。本文将深入探讨三个强大的函数——cache、cached_property 和 lru_cache,它们通过存储昂贵计算的结果来优化性能。无论是加速递归算法还是简化…...
缓存与数据库一致性:从问题到解决方案全解析
一、⼀致性问题的由来:为什么会不一致? 我们先从现实例子出发,来看为什么会出现一致性问题: 📦 场景举例:电商下单业务 用户提交订单 → 服务写入数据库订单表;同时更新缓存(比如用…...
【android bluetooth 框架分析 02】【Module详解 2】【gd_shim_module 模块介绍】
1. 背景 上一章节 我们介绍了 module_t 的 大体框架 ,本节内容我们就选择 我们的 gd_shim_module 模块为例子,具体剖析一下,它里面的逻辑。 static const char GD_SHIM_MODULE[] "gd_shim_module";// system/main/shim/shim.cc …...
dbt:新一代数据转换工具
dbt(Data Build Tool)一款专为数据分析和工程师设计的开源工具,专注于 ETL/ELT 流程的数据转换(Transform)环节,帮助用户以高效、可维护的方式将原始数据转换为适合分析的数据模型。 用户只需要编写查询&am…...
Linux-内核驱动-makemenu,make modules,make uImage,杂项
动态生成设备节点设备号...
linux 内存踩踏导致的空指针问题分析纪要
1,查看日志信息打印 我们看到日志发现发包的skb模块有NULL pointer情况,我们看代码分析skb指针不可能出现是空指针,这个时候我们怀疑可能是出现了踩内存导致的空指针情况,所以我们首先需要找到系统PANIC的条件,也就是…...
【C++】 —— 笔试刷题day_14
一、乒乓球筐 题目解析 题目输入两个字符串A和B,分别代表A和B中的乒乓球,不同的大写字母就表示不同的乒乓球; 如果判断B中的所有乒乓球在A中都有,且A中每种乒乓球的数量大于等于B中的。(简单来说就是B是A的子集&#…...
在WPS中通过JavaScript宏(JSA)调用DeepSeek官方API自动识别标题级别和目录
我们希望通过AI,能够自动识别像“一”、“(一)”、“1”、“(1)” 这类常见标题序号。做一个规则,如果存在“一”时,则“一”、“(一)”、“1”分别识别为H1、H2、H3&…...