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 调用),适合个人开发者或小规模项目。 付费版: 超出免费额度后,可能需要按…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...
Python爬虫实战:研究feedparser库相关技术
1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...
STM32F4基本定时器使用和原理详解
STM32F4基本定时器使用和原理详解 前言如何确定定时器挂载在哪条时钟线上配置及使用方法参数配置PrescalerCounter ModeCounter Periodauto-reload preloadTrigger Event Selection 中断配置生成的代码及使用方法初始化代码基本定时器触发DCA或者ADC的代码讲解中断代码定时启动…...
【单片机期末】单片机系统设计
主要内容:系统状态机,系统时基,系统需求分析,系统构建,系统状态流图 一、题目要求 二、绘制系统状态流图 题目:根据上述描述绘制系统状态流图,注明状态转移条件及方向。 三、利用定时器产生时…...
工业自动化时代的精准装配革新:迁移科技3D视觉系统如何重塑机器人定位装配
AI3D视觉的工业赋能者 迁移科技成立于2017年,作为行业领先的3D工业相机及视觉系统供应商,累计完成数亿元融资。其核心技术覆盖硬件设计、算法优化及软件集成,通过稳定、易用、高回报的AI3D视觉系统,为汽车、新能源、金属制造等行…...
根据万维钢·精英日课6的内容,使用AI(2025)可以参考以下方法:
根据万维钢精英日课6的内容,使用AI(2025)可以参考以下方法: 四个洞见 模型已经比人聪明:以ChatGPT o3为代表的AI非常强大,能运用高级理论解释道理、引用最新学术论文,生成对顶尖科学家都有用的…...
Redis数据倾斜问题解决
Redis 数据倾斜问题解析与解决方案 什么是 Redis 数据倾斜 Redis 数据倾斜指的是在 Redis 集群中,部分节点存储的数据量或访问量远高于其他节点,导致这些节点负载过高,影响整体性能。 数据倾斜的主要表现 部分节点内存使用率远高于其他节…...
Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信
文章目录 Linux C语言网络编程详细入门教程:如何一步步实现TCP服务端与客户端通信前言一、网络通信基础概念二、服务端与客户端的完整流程图解三、每一步的详细讲解和代码示例1. 创建Socket(服务端和客户端都要)2. 绑定本地地址和端口&#x…...
AI+无人机如何守护濒危物种?YOLOv8实现95%精准识别
【导读】 野生动物监测在理解和保护生态系统中发挥着至关重要的作用。然而,传统的野生动物观察方法往往耗时耗力、成本高昂且范围有限。无人机的出现为野生动物监测提供了有前景的替代方案,能够实现大范围覆盖并远程采集数据。尽管具备这些优势…...
[大语言模型]在个人电脑上部署ollama 并进行管理,最后配置AI程序开发助手.
ollama官网: 下载 https://ollama.com/ 安装 查看可以使用的模型 https://ollama.com/search 例如 https://ollama.com/library/deepseek-r1/tags # deepseek-r1:7bollama pull deepseek-r1:7b改token数量为409622 16384 ollama命令说明 ollama serve #:…...
