Netty源码解析之线程池的实现(二):创建线程与执行任务
前言
先看下面的代码:
public class MyTest {public static void main(String[] args) {//创建NioEventLoopGroupNioEventLoopGroup loopGroup = new NioEventLoopGroup(3);System.out.println(Thread.currentThread()+"准备执行任务");//执行任务for (int i = 0 ;i < 3 ; i++){loopGroup.execute(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread()+"执行任务");}});}//优雅的关闭loopGroup.shutdownGracefully();}
}
执行结果为:
由此我们可以看出,我们调用EventLoopGroup的execute(Runnable command)方法后,EventLoopGroup会分别创建不同的新线程来执行我们的任务。
那么问题来了,EventLoopGroup在什么时候创建的线程?如何创建的新线程?这个是本文需要论述的内容。
NioEventLoopGroup的实例化过程
NioEventLoopGroup的实例化过程做了大量的事情,现在以NioEventLoopGroup的无参构造为例。
首先我们如果执行了下面的代码,创建NioEventLoopGroup对象。
NioEventLoopGroup group = new NioEventLoopGroup();
1、NioEventLoopGroup中的构造函数调用
进入NioEventLoopGroup的源码中,看构造方法的调用链:
//第一步,继续调用其他构造方法,并且传入线程数量为0public NioEventLoopGroup() {this(0);}//第二步:继续调用其他构造方法,增加参数Executor为nullpublic NioEventLoopGroup(int nThreads) {this(nThreads, (Executor) null);}//第三步,继续调用其他构造方法,增加参数与操作系统相关的SelectorProvider//这个SelectorProvider用来以后创建NIO的Selectorpublic NioEventLoopGroup(int nThreads, Executor executor) {this(nThreads, executor, SelectorProvider.provider());}//第三步:继续调用其他构造方法,增加参数SelectStrategyFactorypublic NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider) {this(nThreads, executor, selectorProvider, DefaultSelectStrategyFactory.INSTANCE);}//第四步:调用父类的构造方法,增加参数RejectedExecutionHandlerspublic NioEventLoopGroup(int nThreads, Executor executor, final SelectorProvider selectorProvider,final SelectStrategyFactory selectStrategyFactory) {super(nThreads, executor, selectorProvider, selectStrategyFactory, RejectedExecutionHandlers.reject());}
通过以上可以得知,如果使用NioEventLoopGroup的无参构造创建对象,最终会调用父类的构造方法,并且传入的参数中线程数量nThreads=0,执行器executor=null。
2、MultithreadEventLoopGroup的构造函数
下面,进入NioEventLoopGroup的父类MultithreadEventLoopGroup的构造方法中。
protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);}
发现其做了两件事情:
(1)判断传入的线程数量是否为0,如果为0的话,使用默认线程数DEFAULT_EVENT_LOOP_THREADS。
这个默认DEFAULT_EVENT_LOOP_THREADS由一个静态代码块来初始化。
static {DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));}}
从上面的代码可以看出,如果没有指定io.netty.eventLoopThreads的值,则执行“NettyRuntime.availableProcessors() * 2”代码。
public static int availableProcessors() {return holder.availableProcessors();}synchronized int availableProcessors() {if (this.availableProcessors == 0) {final int availableProcessors =SystemPropertyUtil.getInt("io.netty.availableProcessors",//获取CPU核心数Runtime.getRuntime().availableProcessors());setAvailableProcessors(availableProcessors);}return this.availableProcessors;}
最终,如果也没有指定io.netty.availableProcessors的值,则会调用Runtime.getRuntime().availableProcessors()来获取CPU核心数。
综上,Netty的默认线程数量是CPU核心数的2倍。
(2)调用父类的MultithreadEventExecutorGroup的构造方法,传入默认线程数和Executor等参数。
3、MultithreadEventExecutorGroup中的构造函数调用
在MultithreadEventExecutorGroup中,首先调用了下面的构造方法。这时候,传入的nThreads是CPU核心数的2倍,executor为null。
protected MultithreadEventExecutorGroup(int nThreads, Executor executor, Object... args) {this(nThreads, executor, DefaultEventExecutorChooserFactory.INSTANCE, args);}
上面的构造方法做了两件事:
(1)使用 DefaultEventExecutorChooserFactory.INSTANCE创建执行器选择器工厂,该工厂将在后面用于创建一个执行器选择器。这个执行器选择器就是调用NioEventLoopGroup的next()方法时,来选择一个执行器。
(2)继续调用另一个构造方法。
下面,进入这个构造方法,这也是最重要的构造方法,仍然位于MultithreadEventExecutorGroup之中。
protected MultithreadEventExecutorGroup(int nThreads, Executor executor,EventExecutorChooserFactory chooserFactory, Object... args) {//检查传入的线程数量nThreads不能小于或等于0checkPositive(nThreads, "nThreads");//1、实例化executor,这里默认创建的是ThreadPerTaskExecutorif (executor == null) {executor = new ThreadPerTaskExecutor(newDefaultThreadFactory());}//2、创建数组,用于存储执行器,数组数量需要等于线程数量。//因为每个执行器只有一个线程,即单线程的执行器。//在构造函数执行结束后,此数组最终存储的会是NioEventLoop实例。children = new EventExecutor[nThreads];//3、循环创建执行器for (int i = 0; i < nThreads; i ++) {//用来标记当前执行器是否创建成功boolean success = false;try {//4、使用newChild方法创建执行器实例children[i] = newChild(executor, args);success = true;} catch (Exception e) {// TODO: Think about if this is a good exception typethrow new IllegalStateException("failed to create a child event loop", e);} finally {//任何一个执行器创建失败,已经创建好的执行器都需要进行优雅关闭if (!success) {for (int j = 0; j < i; j ++) {children[j].shutdownGracefully();}for (int j = 0; j < i; j ++) {EventExecutor e = children[j];try {//无限期等待,直到当前执行器关闭为止,或者执当前线程被中断while (!e.isTerminated()) {e.awaitTermination(Integer.MAX_VALUE, TimeUnit.SECONDS);}} catch (InterruptedException interrupted) {// Let the caller handle the interruption.Thread.currentThread().interrupt();break;}}}}}//5、根据EventExecutor数组的长度(即NioEventLoop的数量)来创建一个执行器选择器//根据上文,这里的chooserFactory是DefaultEventExecutorChooserFactory实例chooser = chooserFactory.newChooser(children);//6、创建监听器,以便在每个EventExecutor结束时,收到通知final FutureListener<Object> terminationListener = new FutureListener<Object>() {@Overridepublic void operationComplete(Future<Object> future) throws Exception {//如果所有的EventExecutor(一般即NioEventLoop)都进行了结束的通知if (terminatedChildren.incrementAndGet() == children.length) {//EventLoopGroup进行结束通知terminationFuture.setSuccess(null);}}};/** 每个EventExecutor(一般即NioEventLoop)在任务执行结束(即run()方法运行结束)* 后,最终会执行terminationFuture.setSuccess(null);代码进行监听器的通知。* 代码位于SingleThreadEventExecutor类的doStartThread()方法* */for (EventExecutor e: children) {e.terminationFuture().addListener(terminationListener);}//7、将所有的执行器EventExecutor存入只读的副本,以便于能通过迭代器获取Set<EventExecutor> childrenSet = new LinkedHashSet<EventExecutor>(children.length);Collections.addAll(childrenSet, children);readonlyChildren = Collections.unmodifiableSet(childrenSet);}
重点关注这两点:
(1)NioEventLoopGroup的父类MultithreadEventExecutorGroup中包含一个属性children,是一个EventExecutor型的数组,用来存储NioEventLoopGroup包含的所有执行器。
(2)执行器组NioEventLoopGroup包含的执行器在父类MultithreadEventExecutorGroup的构造方法中使用newChild方法来依次创建的。
(3)newChild方法创建执行器时传入了一个非常重要的参数executor,executor是使用“new ThreadPerTaskExecutor(newDefaultThreadFactory())”代码创建的ThreadPerTaskExecutor实例。
这里先剧透一下,newChild方法创建的执行器就是NioEventLoop实例,也就是说children数组中存储的是NioEventLoop实例。需要知道其原因,则要看newChild方法的具体实现。
4、NioEventLoopGroup中newChild方法的实现
在MultithreadEventExecutorGroup中,newChild是一个抽象方法,由子类NioEventLoopGroup实现。
protected EventLoop newChild(Executor executor, Object... args) throws Exception {//1、对参数进行整理SelectorProvider selectorProvider = (SelectorProvider) args[0];/*省略与本文内容无关的代码*///2、使用NioEventLoop的构造方法创建NioEventLoopreturn new NioEventLoop(this, executor, selectorProvider,selectStrategyFactory.newSelectStrategy(),rejectedExecutionHandler, taskQueueFactory, tailTaskQueueFactory);}
上面的newChild方法中,第1步可以先不看,直接看第2步,使用NioEventLoop的构造方法创建NioEventLoop。
这里需要注意的是,此时传递的executor参数是上文中使用“executor = new ThreadPerTaskExecutor(newDefaultThreadFactory())”代码创建的ThreadPerTaskExecutor实例。
5、创建NioEventLoop实例
接上文,NioEventLoopGroup中newChild最后new了NioEventLoop实例。下面,我们来看NioEventLoop实例的创建过程。
//第1步:调用NioEventLoop的构造方法NioEventLoop(NioEventLoopGroup parent, Executor executor, SelectorProvider selectorProvider,SelectStrategy strategy, RejectedExecutionHandler rejectedExecutionHandler,EventLoopTaskQueueFactory taskQueueFactory, EventLoopTaskQueueFactory tailTaskQueueFactory) {//调用父类的构造方法super(parent, executor, false, newTaskQueue(taskQueueFactory), newTaskQueue(tailTaskQueueFactory),rejectedExecutionHandler);//对属性进行赋值this.provider = ObjectUtil.checkNotNull(selectorProvider, "selectorProvider");this.selectStrategy = ObjectUtil.checkNotNull(strategy, "selectStrategy");final SelectorTuple selectorTuple = openSelector();this.selector = selectorTuple.selector;this.unwrappedSelector = selectorTuple.unwrappedSelector;}//第2步:调用父类SingleThreadEventLoop的构造方法protected SingleThreadEventLoop(EventLoopGroup parent, Executor executor,boolean addTaskWakesUp, Queue<Runnable> taskQueue, Queue<Runnable> tailTaskQueue,RejectedExecutionHandler rejectedExecutionHandler) {//调用父类的构造方法super(parent, executor, addTaskWakesUp, taskQueue, rejectedExecutionHandler);//tailTasks 赋值,这个tailTasks的作用暂时不用管tailTasks = ObjectUtil.checkNotNull(tailTaskQueue, "tailTaskQueue");}//第3步:调用父类SingleThreadEventExecutor的构造方法protected SingleThreadEventExecutor(EventExecutorGroup parent, Executor executor,boolean addTaskWakesUp, Queue<Runnable> taskQueue,RejectedExecutionHandler rejectedHandler) {//调用父类的构造方法super(parent);//一些属性的赋值//是否添加唤醒任务this.addTaskWakesUp = addTaskWakesUp;//新任务被拒绝之前的最大待处理任务数,即为任务队列的容量this.maxPendingTasks = DEFAULT_MAX_PENDING_EXECUTOR_TASKS;//执行器executor属性this.executor = ThreadExecutorMap.apply(executor, this);//任务队列this.taskQueue = ObjectUtil.checkNotNull(taskQueue, "taskQueue");//拒绝策略this.rejectedExecutionHandler = ObjectUtil.checkNotNull(rejectedHandler, "rejectedHandler");}
这里,我们重点看第3步,即NioEventLoop的父类SingleThreadEventExecutor的构造方法。这里面有下面的这行代码:
this.executor = ThreadExecutorMap.apply(executor, this);
在这一行代码中,executor是上文中使用“new ThreadPerTaskExecutor(newDefaultThreadFactory())”代码创建的ThreadPerTaskExecutor实例,this就是NioEventLoop自身。
因此,可以看出,NioEventLoop对象里面含有executor属性,从其父类SingleThreadEventExecutor中继承而来,这个属性是ThreadPerTaskExecutor实例,这是NioEventLoop中线程的创建与运行的重点。
EventLoopGroup和EventLoop中的execute(Runnable command)方法
1、EventLoopGroup
在本文开头,我们使用NioEventLoopGroup的execute(Runnable command)方法执行任务。在Executor接口中,execute是抽象方法,需要由子类实现。在NioEventLoopGroup体系中,由NioEventLoopGroup的父类AbstractEventExecutorGroup对execute进行了重写实现。
public void execute(Runnable command) {//先使用next()方法,选出一个执行器(即一个NioEventLoop),然后调用//执行器的execute(Runnable command)next().execute(command);}
其中的next()方法作用是在EventLoopGroup管理的执行器中选择一个,即选择一个NioEventLoop实例。
选择好NioEventLoop实例后,调用NioEventLoop的execute来执行任务。
2、EventLoop
添加任务到队列
EventLoop的execute(Runnable command)方法是在其父类SingleThreadEventExecutor中进行的实现:
实现代码如下:
public void execute(Runnable task) {execute0(task);}private void execute0(@Schedule Runnable task) {//task 任务不能为 nullObjectUtil.checkNotNull(task, "task");execute(task, !(task instanceof LazyRunnable) && wakesUpForTask(task));}
最终调用下面的方法:
/** 执行任务的方法,这里并不直接运行Runnable任务的run()方法,* 而是将任务添加到队列中,然后启动线程去执行* */private void execute(Runnable task, boolean immediate) {// 当前线程是不是执行器线程boolean inEventLoop = inEventLoop();// 重点1、将任务添加到待普通执行任务队列taskQueue中// 注意这里是可以被不同线程调用的,所以有并发冲突问题。// 因此任务队列taskQueue 必须是一个线程安全的队列,就是可以处理并发问题。addTask(task);//如果当前线程不是执行器的线程if (!inEventLoop) {//重点2、要调用 startThread方法开启执行器线程,//这个方法做了判断,只有当执行器状态是 ST_NOT_STARTED 才会开启执行器线程startThread();// 如果执行器状态已经 Shutdown 之后,就要拒绝任务。// 注意这里的状态是已经 Shutdown 之后,所以不包括开始 Shutdown 的状态。if (isShutdown()) {boolean reject = false;try {// 移除任务if (removeTask(task)) {reject = true;}} catch (UnsupportedOperationException e) {// The task queue does not support removal so the best thing we can do is to just move on and// hope we will be able to pick-up the task before its completely terminated.// In worst case we will log on termination.}if (reject) {reject();}}}// 是否唤醒可能阻塞的执行器线程//addTaskWakesUp属性为调用addTask(Runnable)添加任务时是否能唤醒线程//immediate为任务是否立即执行if (!addTaskWakesUp && immediate) {wakeup(inEventLoop);}}
可以看出,EventLoop执行execute(Runnable task)方法的第一步就是
执行addTask(task)将任务添加到队列中。
其最终会将任务添加到taskQueue队列之中。
//添加任务protected void addTask(Runnable task) {ObjectUtil.checkNotNull(task, "task");if (!offerTask(task)) {reject(task);}}//将任务加入普通队列final boolean offerTask(Runnable task) {if (isShutdown()) {reject();}return taskQueue.offer(task);}
判断是否需要开启线程—startThread()方法
在将任务添加到队列之后,判断当前线程是不是本执行器EventLoop的执行线程,如果不是的话,调用startThread()来创建执行器的线程。
当我们第一次调用EventLoopGroup或EventLoop的execute(Runnable task)方法时,执行器处于未运行状态,此时一定会进入到doStartThread()方法中。
//开启执行器的线程private void startThread() {// 只有执行器处于未运行状态,才需要开启运行if (state == ST_NOT_STARTED) {// 通过CAS方式,将执行器变成运行状态if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {boolean success = false;try {// 执行器开启运行doStartThread();success = true;} finally {//出错的话,执行器状态还原if (!success) {STATE_UPDATER.compareAndSet(this, ST_STARTED, ST_NOT_STARTED);}}}}}
在上面的startThread()方法中,再次检查执行器状态为未运行,然后通过CAS方式,将执行器变成运行状态。准备就绪之后,就进入了正式创建线程的doStartThread()方法。
开启线程(一)—doStartThread()方法
下面,我们来重点看下doStartThread()方法是如何开启一个新线程的。
private void doStartThread() {//执行本方法之前,因为没有创建线程,所以执行器的线程一定是nullassert thread == null;//1、新建一个Runnable任务//2、使用调用executor属性的execute方法来执行这个Runnable任务executor.execute(new Runnable() {@Overridepublic void run() {// 获取当前线程,赋值给 thread,就是执行器线程//注意:此时新线程肯定已经创建完成,否则不会赋值给threadthread = Thread.currentThread();// 若线程需要中断则中断线程if (interrupted) {thread.interrupt();}boolean success = false;// 更新最近一次执行任务时间updateLastExecutionTime();try {// 这个方法由子类实现,// 一般情况下,这个方法里面利用死循环,// 来获取待执行任务队列 taskQueue 中的任务并运行SingleThreadEventExecutor.this.run();// 标记启动成功success = true;} catch (Throwable t) {logger.warn("Unexpected exception from an event executor: ", t);} finally {/*此处省略了大量与本文内容无关的代码*/}}
在doStartThread()方法中,其实只做了两个动作,
1、新建一个Runnable任务。
2、使用调用executor属性的execute方法来执行这个Runnable任务。
并且,在Runnable任务的第一步,就获取当前线程,赋值给执行器EventLoop的thread属性。因此,executor在执行到Runnable任务的代码块时,肯定已经创建了新线程。
开启线程(二)—EventLoop中executor属性的execute方法
首先,EventLoop的executor属性到底是什么?
这个在上文已经交代过,就是“new ThreadPerTaskExecutor(newDefaultThreadFactory())”代码创建的ThreadPerTaskExecutor实例。
因此,doStartThread()方法中的executor.execute(new Runnable() {……})这行代码实际上就是调用ThreadPerTaskExecutor的execute方法。
接下来,我们来看看ThreadPerTaskExecutor的execute方法到底做了什么。
1、ThreadPerTaskExecutor的execute方法作用是创建线程
进入ThreadPerTaskExecutor的源码,其源码非常简单。只有一个threadFactory属性,和一个execute(Runnable command)方法。
public final class ThreadPerTaskExecutor implements Executor {//线程工厂private final ThreadFactory threadFactory;//构造方法public ThreadPerTaskExecutor(ThreadFactory threadFactory) {this.threadFactory = ObjectUtil.checkNotNull(threadFactory, "threadFactory");}@Overridepublic void execute(Runnable command) {threadFactory.newThread(command).start();}
}
可以发现,其execute(Runnable command)方法就是使用其threadFactory属性的newThread方法来创建一个新线程,然后使用start()方法启动线程。
2、线程工厂threadFactory
ThreadPerTaskExecutor的threadFactory在其构造方法中赋值,由于之前使用的是“new ThreadPerTaskExecutor(newDefaultThreadFactory())”代码创建的ThreadPerTaskExecutor实例,我们继续进入newDefaultThreadFactory()方法之中。
protected ThreadFactory newDefaultThreadFactory() {return new DefaultThreadFactory(getClass());}
newDefaultThreadFactory()方法作用是new了一个DefaultThreadFactory对象,传入的参数是当前实例的Class对象。由于我们现在创建的是NioEventLoopGroup实例,因此getClass()方法获取到的就是NioEventLoopGroup的Class对象。
这里传入Class对象,主要是为了获取类名,用来拼接线程的名称。这也是为什么在本文的开头,执行任务的线程的名称会是nioEventLoopGroup-2-1、nioEventLoopGroup-2-2这种类型(见本文“前言”部分的代码运行结果)。至于线程名称的具体拼接方式,可以在DefaultThreadFactory的源码中看,本文不再详述。
3、newThread方法的具体内容
在DefaultThreadFactory源码在,其newThread方法就是创建线程,但是创建的线程是FastThreadLocalThread类型。
这个FastThreadLocalThread是Thread的子类,即也是线程类。其具体的作用后续再写文章描述,和Netty对jdk中的ThreadLocal进行的改造和优化有关。
public Thread newThread(Runnable r) {//1、将Runnable任务封装成FastThreadLocalRunnable//2、拼接线程名称//3、调用同名的newThread方法Thread t = newThread(FastThreadLocalRunnable.wrap(r), prefix + nextId.incrementAndGet());/* 此处省略代码 */return t;}protected Thread newThread(Runnable r, String name) {return new FastThreadLocalThread(threadGroup, r, name);}
到这里,可以看出来ThreadPerTaskExecutor的execute方法实际上就是创建了一个新的线程(FastThreadLocalThread实例),并且把新线程Runnable任务交给新线程去执行。这也就是为什么在doStartThread()方法中,executor.execute(new Runnable() {……})这行代码执行到Runnable对象的内部时,已经是一个新线程去执行了。
开启线程(三)—NioEventLoop的executor属性执行的Runnable
在知道了NioEventLoop在执行execute(Runnable command)方法时,是调用ThreadPerTaskExecutor实例(在NioEventLoop中是executor属性)的execute方法,先创建一个新线程(FastThreadLocalThread实例),然后由新线程去运行Runnable任务。
在doStartThread()方法中,我们可以看到Runnable任务的内容:
executor.execute(new Runnable() {@Overridepublic void run() {// 获取当前线程,赋值给 thread,就是执行器线程//注意:此时新线程肯定已经创建完成,否则不会赋值给threadthread = Thread.currentThread();/*省略与本文无关的代码*/boolean success = false;// 更新最近一次执行任务时间updateLastExecutionTime();try {// 这个方法由子类实现,// 一般情况下,这个方法里面利用死循环,// 来获取待执行任务队列 taskQueue 中的任务并运行SingleThreadEventExecutor.this.run();// 标记启动成功success = true;} catch (Throwable t) {logger.warn("Unexpected exception from an event executor: ", t);} finally {/*此处省略了大量与本文内容无关的代码*/}
在Runnable内部的run()方法中,并没有直接去执行NioEventLoop的execute(Runnable command)方法所提交的任务,因为上文说过,这个任务已经存入到NioEventLoop中的任务队列taskQueue之中。这里面最重要的就是下面的这行代码:
SingleThreadEventExecutor.this.run();
ThreadPerTaskExecutor创建的新线程执行了一个Runnable任务,在Runnable任务里面又运行了SingleThreadEventExecutor的run()方法。但是SingleThreadEventExecutor的run()方法是抽象方法,需要由子类去实现。在Netty中,一般情况下这个run()方法的功能之一就是循环从任务队列中获取任务并运行。
如在NioEventLoop中,run()方法要做的事情之一就是队列中任务的处理。
总结
1、调用NioEventLoopGroup的构造方法时,会创建一个ThreadPerTaskExecutor实例,然后将其赋值给每个NioEventLoop的executor属性。
1、调用NioEventLoopGroup的execute(Runnable command)方法时,会首先使用next()方法选择一个NioEventLoop,然后调用NioEventLoop的execute方法执行任务。
2、调用NioEventLoop的execute方法时,会先将任务加入到队列中,然后使用NioEventLoop中的executor(ThreadPerTaskExecutor的实例)来创建线程。
3、创建的新线程会执行NioEventLoop的run()方法,在NioEventLoop的run()方法中会执行任务队列中的任务。
虽然Netty源码比较复杂,但是一切还是有规可循的。
相关文章:

Netty源码解析之线程池的实现(二):创建线程与执行任务
前言 先看下面的代码: public class MyTest {public static void main(String[] args) {//创建NioEventLoopGroupNioEventLoopGroup loopGroup new NioEventLoopGroup(3);System.out.println(Thread.currentThread()"准备执行任务");//执行任务for (in…...

IDEA - 一个启动类多次启动方法
More Run/Debug -> Modify Run Configuration -> modify options -> Allow mutiple instances...

U3D支持webgpu阅读
https://docs.unity3d.com/6000.1/Documentation/Manual/WebGPU-features.html 这里看到已经该有的差不多都有了 WOW VFX更是好东西 https://unity.com/cn/features/visual-effect-graph 这玩意儿化简了纯手搓一个特效的流程 如果按原理说就是compute shader刷position&#…...

C++广度优先搜索
简介 老规矩,先来介绍一下什么是广度优先搜索(至于这么长时间没更新是为什么,我放在文章结尾了,感兴趣可以看看,以后也是如此) 广度优先搜索,从名字就能听出来,他和深度优先搜索关…...

SVN 提交与原有文件类型不一样的文件时的操作
SVN 提交与原有文件类型不一样的文件时的操作 背景 SVN 服务器上原本的文件是软链接类型的,但是我将它改成普通文件再上传。出现了以下提示: 解决过程 本来想着通过 svn rm 和 svn add 来解决,但是行不通。 最终解决方案 svn rm --keep-…...

活动预告 | Power Hour: Copilot 引领商业应用的未来
课程介绍 智能化时代,商业应用如何实现突破?微软全球副总裁 Charles Lamanna 将为您深度解析,剖析其中关键因素。 在本次线上研讨会中,Charles Lamanna 将分享他在增强商业运营方面的独到见解与实战策略,深度解读商业…...

WPF 进度条(ProgressBar)示例一
本文讲述:WPF 进度条(ProgressBar)简单的样式修改和使用。 进度显示界面:使用UserControl把ProgressBar和进度值以及要显示的内容全部组装在UserControl界面中,方便其他界面直接进行使用。 <UserControl x:Class"DefProcessBarDemo…...

【C#】任务调度的实现原理与组件应用Quartz.Net
Quartz 是一个流行的开源作业调度库,最初由 Terracotta 开发,现在由 Terracotta 的一部分 Oracle 所有。它主要用于在 Java 应用程序中调度作业的执行。Quartz 使用了一种复杂的底层算法来管理任务调度,其中包括任务触发、执行、持久化以及集…...

UV - Python 包管理
文章目录 创建 uv 项目已有项目已有uv项目 创建 uv 项目 # 创建项目 uv init m3 # 创建环境 cd m3 uv venv --python 3.11 # 激活环境 source .venv/bin/activate # 添加库 uv add flask 如果创建项目后,给库取别的名字,add 的时候,会…...

pytorch torch.linalg模块介绍
torch.linalg 是 PyTorch 的 线性代数 (Linear Algebra) 子模块,它提供了许多 高效的矩阵操作和分解方法,类似于 NumPy 的 numpy.linalg 或 SciPy 的 scipy.linalg,但针对 GPU 加速和自动微分 进行了优化。 1. 矩阵基本运算 矩阵乘法 torc…...

光伏-报告显示,假期内,硅料端签单顺序发货相对稳定。若3月份下游存提产,则不排除硅料价格有上调预期。
据TrendForce集邦咨询报告显示,假期内,硅料端按照前期签单顺序发货,相对稳定。若3月份下游存提产,则不排除硅料价格有上调预期。 002306中科云网 旅游 | 公司为提供复合菜系特色餐饮的连锁企业,形成了以粤菜ÿ…...

【web自动化】指定chromedriver以及chrome路径
selenium自动化,指定chromedriver,以及chrome路径 对应这篇文章,可以点击查看,详情 from selenium import webdriverdef get_driver():# 获取配置对象option webdriver.ChromeOptions()option.add_experimental_option("de…...

顺丰数据分析(数据挖掘)面试题及参考答案
你觉得数据分析人员必备的技能有哪些? 数据分析人员需具备多方面技能,以应对复杂的数据处理与解读工作。 数据处理能力:这是基础且关键的技能。数据常以杂乱、不完整的形式存在,需通过清洗,去除重复、错误及缺失值数据,确保数据质量。例如,在电商销售数据中,可能存在价…...

Android studio:顶部导航栏Toolbar
主流APP在顶部都配有导航栏,在 Android 中,ActionBar 是默认启用的,它是位于屏幕顶部的一个工具栏,用来放置应用的标题、导航和操作菜单。 如果你想使用自定义的 Toolbar 来替代 ActionBar,应该先关闭它。可以通过设置…...

mmap 文件映射
🌈 个人主页:Zfox_ 🔥 系列专栏:Linux 目录 一:🔥 mmap介绍🦋 基本说明🦋 参数介绍🦋 返回值 二:🔥 demo代码🦋 写入映射🦋…...

基于微信小程序的医院预约挂号系统的设计与实现
hello hello~ ,这里是 code袁~💖💖 ,欢迎大家点赞🥳🥳关注💥💥收藏🌹🌹🌹 🦁作者简介:一名喜欢分享和记录学习的在校大学生…...

【Linux】Socket编程—UDP
🔥 个人主页:大耳朵土土垚 🔥 所属专栏:Linux系统编程 这里将会不定期更新有关Linux的内容,欢迎大家点赞,收藏,评论🥳🥳🎉🎉🎉 文章目…...

2025年物联网相关专业毕业论文选题参考,文末联系,选题相关资料提供
一、智能穿戴解决方案研究方向 序号解决方案论文选题论文研究方向1智能腰带健康监测基于SpringBoot和Vue的智能腰带健康监测数据可视化平台开发研究如何利用SpringBoot和Vue技术栈开发一个数据可视化平台,用于展示智能腰带健康监测采集的数据,如心率、血…...

如何在WPS和Word/Excel中直接使用DeepSeek功能
以下是将DeepSeek功能集成到WPS中的详细步骤,无需本地部署模型,直接通过官网连接使用:1. 下载并安装OfficeAI插件 (1)访问OfficeAI插件下载地址:OfficeAI助手 - 免费办公智能AI助手, AI写作,下载…...

DeepSeek之Api的使用(将DeepSeek的api集成到程序中)
一、DeepSeek API 的收费模式 前言:使用DeepSeek的api是收费的 免费版: 可能提供有限的免费额度(如每月一定次数的 API 调用),适合个人开发者或小规模项目。 付费版: 超出免费额度后,可能需要按…...

使用DeepSeek实现AI自动编码
最近deepseek很火,低成本训练大模型把OpenAI、英伟达等股票搞得一塌糊涂。那它是什么呢,对于咱们程序员编码能有什么用呢?DeepSeek 是一款先进的人工智能语言模型,在自然语言处理和代码生成方面表现出色。它经过大量代码数据训练&…...

30~32.ppt
目录 30.导游小姚-介绍首都北京❗ 题目 解析 31.小张-旅游产品推广文章 题目 解析 32.小李-水的知识❗ 题目 解析 30.导游小姚-介绍首都北京❗ 题目 解析 新建幻灯片-从大纲-重置-检查设计→主题对话框→浏览主题:考生文件夹(注意&#x…...

Java的匿名内部类转为lamada表达式
在Java中,匿名内部类通常用于创建没有命名类的实例。例如,你可能需要创建一个实现了某个接口的匿名类,或者在需要重写某个方法时使用它。在Java 8及更高版本中,你可以使用Lambda表达式来替代传统的匿名内部类,使得代码…...

redis高级数据结构Stream
文章目录 背景stream概述消息 ID消息内容常见操作独立消费创建消费组消费 Stream弊端Stream 消息太多怎么办?消息如果忘记 ACK 会怎样?PEL 如何避免消息丢失?分区 Partition Stream 的高可用总结 背景 为了解决list作为消息队列是无法支持消息多播问题,Redis5.0…...

LeetCode781 森林中的兔子
问题描述 在一片神秘的森林里,住着许多兔子,但是我们并不知道兔子的具体数量。现在,我们对其中若干只兔子进行提问,问题是 “还有多少只兔子与你(指被提问的兔子)颜色相同?” 我们将每只兔子的…...

单硬盘槽笔记本更换硬盘
背景 本人的笔记本电脑只有一个硬盘槽,而且没有M.2的硬盘盒,只有一个移动硬盘 旧硬盘:512G 新硬盘:1T 移动硬盘:512G 参考链接:https://www.bilibili.com/video/BV1iP41187SW/?spm_id_from333.1007.t…...

EB生成配置的过程
EB Tresos Studio,简称EB,通过图形化的模式进行配置生成,并根据选项配置生成配置代码,即 MCAL 层各个模块的配置参数。 在 MCAL 代码中,分为静态代码和配置代码。静态代码,就是 AUTOSAR 规范内容,包含对硬件的封装以及标准化接口的封装;配置代码一般用于配置初始化结构…...

量化交易数据获取:xtquant库的高效应用
量化交易数据获取:xtquant库的高效应用 在量化交易领域,历史行情数据的重要性不言而喻。它不仅为策略回测提供基础,也是实时交易决策的重要参考。本文将介绍如何使用xtquant库来高效获取和处理历史行情数据。 技术背景与应用场景 对于量化…...

哨兵模式与 Redis Cluster:高可用 Redis 的深度剖析
深入探讨 Redis 高可用性解决方案:哨兵模式与 Redis Cluster 一、哨兵模式(Redis Sentinel)深入解析 (一)工作原理详解 哨兵模式通过一个或多个哨兵实例监控 Redis 主从复制集群,确保在主节点发生故障时…...

C++20新特性
作者:billy 版权声明:著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处 前言 C20 是 C 标准中的一个重要版本,引入了许多新特性和改进,包括模块(Modules)、协程…...