很久前(2020-10-23),就有想法学习线程池并输出博客,但是写着写着感觉看不懂了,就不了了之了。现在重拾起,重新写一下(学习一下)。

线程池的优点也是老生常谈的东西了

  1. 减少线程创建的开销(任务数大于线程数时)
  2. 统一管理一系列的线程(资源)

在讲ThreadPoolExecutor前,我们先看看它的父类都有些啥。

Executor,执行提交的Runnable任务的对象,将任务提交与何时执行分离开。

execute方法是Executor接口的唯一方法。

    // 任务会在未来某时执行,可能执行在一个新线程中、线程池或调用该任务的线程中。
void execute(Runnable command);

ExecutorService是一个Executor,提供了管理终止的方法和返回Future来跟踪异步任务的方法(sumbit)。

终止的两个方法

  • shutdown(), 正在执行的任务继续执行,不接受新任务
  • shutdownNow(), 正在执行的任务也要被终止

AbstractExecutorService,实现了ExecutorServicesumbitinvokeAny,invokeAll

介绍

线程池主要元素

底层变量

ctl

我们讲讲先ctl(The main pool control state), 其包含两个信息

  1. 线程池的状态(最高三位)
  2. 线程池的workerCount,有效的线程数
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1; // runState is stored in the high-order bits
private static final int RUNNING = -1 << COUNT_BITS;
private static final int SHUTDOWN = 0 << COUNT_BITS;
private static final int STOP = 1 << COUNT_BITS;
private static final int TIDYING = 2 << COUNT_BITS;
private static final int TERMINATED = 3 << COUNT_BITS;

‍线程池的状态转化图

看一下每个状态的含义

  1. RUNNING, 接受新的任务并且处理阻塞队列的任务
  2. SHUTDOWN, 拒绝新任务,但是处理阻塞队列的任务
  3. STOP, 拒绝新任务,并且抛弃阻塞队列中的任务,还要中断正在运行的任务
  4. TIDYING,所有任务执行完(包括阻塞队列中的任务)后, 当前线程池活动线程为0, 将要调用terminated方法
  5. TERMINATED, 终止状态。调用terminated方法后的状态

workers

工作线程都添加到这个集合中。可以想象成一个集中管理的平台,可以通过workers获取活跃的线程数,中断所有线程等操作。

    private final HashSet<Worker> workers = new HashSet<Worker>();

可修改变量

构造器中的参数

    public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime, // 最大等待任务的时间,超过则终止超过corePoolSize的线程
TimeUnit unit,
BlockingQueue<Runnable> workQueue, // 阻塞队列
ThreadFactory threadFactory, // executor使用threadFactory创建一个线程
RejectedExecutionHandler handler) // 拒绝策略

corePoolSize、maximumPoolSize,workQueue三者的关系:

  • 当线程数小于corePoolSize,任务进入,即使有其他线程空闲,也会创建一个新的线程
  • 大于corePoolSize且小于maximumPoolSizeworkQueue未满,将任务加入到workQueue中;只有workQueue满了,才会新建一个线程
  • workQueue已满,且任务大于maximumPoolSize,将会采取拒绝策略(handler)

拒绝策略:

  1. AbortPolicy, 直接抛出RejectedExecutionException
  2. CallerRunsPolicy, 使用调用者所在线程来执行任务
  3. DiscardPolicy, 默默丢弃
  4. DiscardOldestPolicy, 丢弃头部的一个任务,重试

allowCoreThreadTimeOut

控制空闲时,core threads是否被清除。

探索源码

最重要的方法就是execute

提交的任务将在未来某个时候执行

    public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* Proceed in 3 steps:
*
* 1. If fewer than corePoolSize threads are running, try to
* start a new thread with the given command as its first
* task. The call to addWorker atomically checks runState and
* workerCount, and so prevents false alarms that would add
* threads when it shouldn't, by returning false.
*
* 2. If a task can be successfully queued, then we still need
* to double-check whether we should have added a thread
* (because existing ones died since last checking) or that
* the pool shut down since entry into this method. So we
* recheck state and if necessary roll back the enqueuing if
* stopped, or start a new thread if there are none.
*
* 3. If we cannot queue task, then we try to add a new
* thread. If it fails, we know we are shut down or saturated
* and so reject the task.
*/ // 获取workCount与runState
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}

根据上面的注释,我们将execute分为三个部分来讲解

  • 当正在运行的线程数小于corePoolSize
  • 当大于corePoolSize时,需要入队
  • 队列已满

当正在运行的线程数小于corePoolSize

        // execute第一部分代码
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}

addWorker, 创建工作线程。

当然它不会直接就添加一个新的工作线程,会检测runStateworkCount,来避免不必要的新增。检查没问题的话,新建线程,将其加入到wokers,并将线程启动。

   // firstTask,当线程启动时,第一个任务
// core,为true就是corePoolSize作为边界,反之就是maximumPoolSize
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c); // Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false; for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
} boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get()); if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}

上面的代码很长,我们将它分为两部分

// addWorker()第一部分代码
// 这部分主要是通过CAS增加workerCount
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c); // Check if queue empty only if necessary.
// 下面的条件我们将它转化成
// if (rs >= SHUTDOWN &&
// (rs != SHUTDOWN ||
// firstTask != null ||
// workQueue.isEmpty()))
// 结合线程池状态分析! // 情况1. 当前的线程池状态为STOP、TIDYING,TERMINATED
// 情况2. 当前线程池的状态为SHUTDOWN且firstTask不为空,只有RUNNING状态才可以接受新任务
// 情况3. 当前线程池的状态为SHUTDOWN且firstTask为空且队列为空。
// 这几种情况,没有必要新建worker(线程)。
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false; for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// CAS增加workerCount成功,继续第二部分操作
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
// 这里的线程池状态被改变了,继续外部循环,再次检查线程池状态
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}

经过上面的代码,我们成功通过CAS使workerCount + 1,下面我们就会新建worker并添加到workers中,并启动通过threadFactory创建的线程。

// addWorker()第二部分代码

        boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get()); // 第一种情况,rs为RUNNING
// 第二种情况是rs为SHUTDOWN,firstTask为null, 但是workQueue(阻塞队列)不为null,创建线程进行处理
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
// 这里是该线程已经被启动了,我觉得的原因是threadFactory创建了两个相同的thread,不知道还有其他原因没。
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
// 上面的线程创建可能失败,或者线程工厂返回null
// 或者线程启动时,抛出OutOfMemoryError
if (! workerStarted)
// 回滚状态
addWorkerFailed(w);
}
return workerStarted;

看完了addWorker的步骤,代码中有个Worker类,看似是线程但又不完全是线程,我们去看看它的结构。

Worker

这个类的主要作用是,维护线程运行任务的中断控制状态记录每个线程完成的任务数

整体结构

    private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable{ /** Thread this worker is running in. Null if factory fails. */
final Thread thread;
/** Initial task to run. Possibly null. */
Runnable firstTask;
/** 每个线程的任务完成数 */
volatile long completedTasks; Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
// 在创建线程时,将任务传入到threadFactory中
this.thread = getThreadFactory().newThread(this);
} public void run() {
// 将运行委托给外部方法runWorker,下面会详见。 // 这里是运行任务的核心代码
runWorker(this);
} // 实现AQS的独占模式的方法,该锁不能重入。
// Lock methods
//
// The value 0 represents the unlocked state.
// The value 1 represents the locked state. protected boolean isHeldExclusively() {
return getState() != 0;
} protected boolean tryAcquire(int unused) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
} protected boolean tryRelease(int unused) {
setExclusiveOwnerThread(null);
setState(0);
return true;
} public void lock() { acquire(1); }
public boolean tryLock() { return tryAcquire(1); }
public void unlock() { release(1); }
public boolean isLocked() { return isHeldExclusively(); }
// 线程运行之后才可以被中断
void interruptIfStarted() {
Thread t;
if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
try {
t.interrupt();
} catch (SecurityException ignore) {}
}
}
}

我们来看看runWorker的实现。这个类主要的工作就是,不停地阻塞队列中获取任务并执行,若firstTask不为空,就直接执行它。

    final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// getTask控制阻塞等待任务或者是否超时就清除空闲的线程
// getTask非常之重要,后面会讲到
while (task != null || (task = getTask()) != null) {
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
// 第二种情况,重新检测线程池状态,因为此时可能其他线程会调用shutdownNow
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
// 执行前
beforeExecute(wt, task);
Throwable thrown = null;
try {
// 执行firstTask的run方法
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
// 执行后
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
// 处理worker退出
processWorkerExit(w, completedAbruptly);
}
}

‍ 启动一个线程,大致执行的方法流程

getTask,我们来看看它是怎样阻塞或定时等待任务的

Performs blocking or timed wait for a task, depending on current configuration settings, or returns null if this worker must exit because of any of:

  1. There are more than maximumPoolSize workers (due to a call to setMaximumPoolSize).
  2. The pool is stopped.
  3. The pool is shutdown and the queue is empty.
  4. This worker timed out waiting for a task, and timed-out workers are subject to termination (that is, allowCoreThreadTimeOut || workerCount > corePoolSize) both before and after the timed wait, and if the queue is non-empty, this worker is not the last thread in the pool. ‍(超时等待任务的worker,在定时等待前后都会被终止(情况有,allowCoreThreadTimeOut || wc > corePoolSize)

Returns:

task, or null if the worker must exit, in which case workerCount is decremented(worker退出时,workerCount会减一)

  private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out? for (;;) {
int c = ctl.get();
int rs = runStateOf(c); // 检测是否有必要返回新任务,注意每个状态的含义就明白了
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
} int wc = workerCountOf(c); // 检测worker是否需要被淘汰
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; // 下面的代码结合上面的timed变量,超时后,当大于corePoolSize时,返回null
// 或者当allowCoreThreadTimeOut = true时,超时后,返回null
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
} try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
// 这里r为null的话,只能是timed = true的情况;take(),一直会阻塞直到有任务返回
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}

我们来看看当getTask返回null时,线程池是如何处理worker退出的

根据runWorker的代码,getTask为null,循环体正常退出,此时completedAbruptly = false;

processWorkerExit

  private void processWorkerExit(Worker w, boolean completedAbruptly) {
// 1. 有异常退出的话, workerCount将会减一
// 2. 正常退出的话,因为在getTask中已经减一,所以这里不用理会
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount(); // 将worker完成的任务数加到completedTaskCount
// 从workers中移除当前worker
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
} // 检测线程池是否有资格设置状态为TERMINATED
tryTerminate(); int c = ctl.get();
// 此时的状态是RUNNING或SHUTDOWN
if (runStateLessThan(c, STOP)) {
// 1. 非正常退出的,addWorker()
// 2. 正常退出的, workerCount小于最小的线程数,就addWorker()
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
addWorker(null, false);
}
}

getTask是保证存在的线程不被销毁的核心,getTask则利用阻塞队列take方法,一直阻塞直到获取到任务为止。

当大于corePoolSize时,需要入队

// execute第二部分代码
// 线程池状态是RUNNING(只有RUNNING才可以接受新任务)
// 此时,workerCount >= corePoolSize, 将任务入队
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 此时线程池可能被shutdown了。
// 需要清除刚添加的任务,若任务还没有被执行,就可以让它不被执行
if (! isRunning(recheck) && remove(command))
reject(command);
// 若此时没有worker,新建一个worker去处理队列中的任务
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}

队列已满

       // execute第三部分代码
// addWorker第二个参数false表明,以maximumPoolSize为界限
else if (!addWorker(command, false))
// workerCount > maximumPoolSize 就对任务执行拒绝策略
reject(command);

我们就讲完了执行方法execute(),有兴趣的同学可以去看看关闭方法shutdown()以及shutdownNow(),看看他们的区别。当然也可以去研究一下其他方法的源码。

探究一些小问题

  1. runWorker为啥这样抛错
    try {
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
...
}

We separately handle RuntimeException, Error (both of which the specs guarantee that we trap) and arbitrary Throwables. Because we cannot rethrow Throwables within Runnable.run, we wrap them within Errors on the way out (to the thread's UncaughtExceptionHandler). Any thrown exception also conservatively causes thread to die.

大致意思就是,分别处理RuntimeException、Error和任何的Throwable。因为不能在 Runnable.run 中重新抛出 Throwables,所以将它们包装在 Errors中(到线程的 UncaughtExceptionHandler).

Runnable.run不能抛出Throwables的原因是,Runnable中的run并没有定义抛出任何异常,继承它的子类,抛错的范围不能超过父类

UncaughtExceptionHandler可以处理“逃逸的异常”,可以去了解一下。

  1. 创建线程池最好手动创建,参数根据系统自定义



    图中的设置线程数的策略只是初步设置,下一篇我们去研究具体的线程数调优

  2. 为什么创建线程开销大

    启动一个线程时,将涉及大量的工作

  • 必须为线程堆栈分配和初始化一大块内存。
  • 需要创建/注册native thread在host OS中
  • 需要创建、初始化描述符并将其添加到 JVM 内部数据结构中。

虽然启动一个线程的时间不长,耗费的资源也不大,但有个东西叫"积少成多"。就像

Doug Lea写的源码一样,有些地方的细节优化,看似没必要,但是请求一多起来,那些细节就是"点睛之笔"了。

当我们有大量需要线程时且每个任务都是独立的,尽量考虑使用线程池

总结

线程池的总体流程图

线程池新建线程,如何保证可以不断地获取任务,就是通过阻塞队列(BlockingQueue)的take方法,阻塞自己直到有任务才返回。

本篇博客也到这里就结束了,学习线程池以及输出博客,中间也拖了很久,最后送给大家以及自己最近看到的一句话

往往最难的事和自己最应该做的事是同一件事

参考

JAVA并发(8)-ThreadPoolExecutor的讲解的更多相关文章

  1. java并发初探ThreadPoolExecutor拒绝策略

    java并发初探ThreadPoolExecutor拒绝策略 ThreadPoolExecuter构造器 corePoolSize是核心线程池,就是常驻线程池数量: maximumPoolSize是最 ...

  2. Java并发编程--ThreadPoolExecutor

    概述 为什么要使用线程池? 合理利用线程池能够带来三个好处.第一:降低资源消耗.通过重复利用已创建的线程降低线程创建和销毁造成的消耗.第二:提高响应速度.当任务到达时,任务可以不需要等到线程创建就能立 ...

  3. 【Java 并发】详解 ThreadPoolExecutor

    前言 线程池是并发中一项常用的优化方法,通过对线程复用,减少线程的创建,降低资源消耗,提高程序响应速度.在 Java 中我们一般通过 Exectuors 提供的工厂方法来创建线程池,但是线程池的最终实 ...

  4. java并发线程池---了解ThreadPoolExecutor就够了

    总结:线程池的特点是,在线程的数量=corePoolSize后,仅任务队列满了之后,才会从任务队列中取出一个任务,然后构造一个新的线程,循环往复直到线程数量达到maximumPoolSize执行拒绝策 ...

  5. Java并发——ThreadPoolExecutor线程池解析及Executor创建线程常见四种方式

    前言: 在刚学Java并发的时候基本上第一个demo都会写new Thread来创建线程.但是随着学的深入之后发现基本上都是使用线程池来直接获取线程.那么为什么会有这样的情况发生呢? new Thre ...

  6. Java并发编程:ThreadPoolExecutor + Callable + Future(FutureTask) 探知线程的执行状况

    如题 (总结要点) 使用ThreadPoolExecutor来创建线程,使用Callable + Future 来执行并探知线程执行情况: V get (long timeout, TimeUnit ...

  7. Java并发必知必会第三弹:用积木讲解ABA原理

    Java并发必知必会第三弹:用积木讲解ABA原理 可落地的 Spring Cloud项目:PassJava 本篇主要内容如下 一.背景 上一节我们讲了程序员深夜惨遭老婆鄙视,原因竟是CAS原理太简单? ...

  8. Java并发机制(7)--线程池ThreadPoolExecutor的使用

    Java并发编程:线程池的使用整理自:博客园-海子-http://www.cnblogs.com/dolphin0520/p/3932921.html 1.什么是线程池,为什么要使用线程池: 1.1. ...

  9. Java并发编程:线程池的使用

    Java并发编程:线程池的使用 在前面的文章中,我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题: 如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了, ...

随机推荐

  1. Linux 仿真终端:SecureCRT 常用配置

    SecureCRT 有两类配置选项,分别是会话选项和全局选项. 会话选项:修改配置只针对当前会话有效 全局选项:修改配置对所有会话有效 一般会先选择全局选项修改全局配置,然后选择会话选项单独修改个别会 ...

  2. selenium多表单切换以及多窗口切换、警告窗处理

    selenium表单切换 在做UI自动化,有时候要定位的元素属性在页面上明明是唯一的.却怎么也不执行对元素的操作动作,这时候多半是iframe表单在作怪. 切入表单:iddriver.switch_t ...

  3. 【Azure 事件中心】azure-spring-cloud-stream-binder-eventhubs客户端组件问题, 实践消息非顺序可达

    问题描述 查阅了Azure的官方文档( 将事件发送到特定分区: https://docs.azure.cn/zh-cn/event-hubs/event-hubs-availability-and-c ...

  4. Nginx实战部署常用功能演示(超详细版),绝对给力~~~

    前言 上次分享了一些开发过程中常用的功能,但如果到真实环境中,其实还需要一些额外的配置,比如说跨域.缓存.配置SSL证书.高可用等,老规矩,还是挑几个平时比较常用的进行演示分享.上篇详见Nginx超详 ...

  5. 实战|教你用Python玩转Mysql

    爬虫采集下来的数据除了存储在文本文件.excel之外,还可以存储在数据集,如:Mysql,redis,mongodb等,今天辰哥就来教大家如何使用Python连接Mysql,并结合爬虫为大家讲解. 前 ...

  6. 模糊视频帧插值:CVPR2020论文点评

    模糊视频帧插值:CVPR2020论文点评 Blurry Video Frame Interpolation 论文链接:https://arxiv.org/pdf/2002.12259.pdf 摘要 现 ...

  7. TensorFlow Keras API用法

    TensorFlow Keras API用法 Keras 是与 TensorFlow 一起使用的更高级别的作为后端的 API.添加层就像添加一行代码一样简单.在模型架构之后,使用一行代码,可以编译和拟 ...

  8. TensorRT原理图示

    TensorRT原理图示 NVIDIA的核心 TensorRT是有助于在NVIDIA图形处理单元(GPU)的高性能推理一个C ++库.它旨在与TensorFlow,Caffe,PyTorch,MXNe ...

  9. 微调BERT:序列级和令牌级应用程序

    微调BERT:序列级和令牌级应用程序 Fine-Tuning BERT for Sequence-Level and Token-Level Applications 为自然语言处理应用程序设计了不同 ...

  10. Ucore lab1实验报告

    练习一 Makefile 1.1 OS镜像文件ucore.img 是如何一步步生成的? + cc kern/init/init.c + cc kern/libs/readline.c + cc ker ...