2024年2月

使用了标准库头文件
<setjmp.h>
中的
setjmp

longjmp
两个函数,构建了一个简单的查询式协作多任务系统,支持
独立栈

共享栈
两种任务。

  1. 其中涉及到获取和设置栈的地址操作,因此还需要根据不同平台提供获取和设置栈的地址操作(一般是汇编语言,因为涉及到寄存器)
  2. 该调度系统仅运行在一个实际的线程中,因此本质上属于协程
  3. 独立栈任务都有自己独立的运行栈空间,互不干扰;共享栈任务共用一个运行栈空间。

特点

  • 无任务优先级抢占的功能。

  • 任务切换的时机完全取决于正在运行的任务,体现
    协作

  • 支持
    独立栈

    共享栈
    两种任务,根据不同的应用场景决定。

  • 查询式的调度方式,当前任务切换时,查询下个任务是否需要执行。

  • 移植性强,只需要修改设置栈和获取当前栈地址的宏即可。

  • 相对于
    时间片论法
    的任务调度来说,查询式协作多任务系统有以下特点:


    • 无需使用定时器做为任务调度
    • 每个任务都可以使用
      while
      循环,用于执行任务并保持程序的运行,程序结构清晰
    • 每个任务都可以随时阻塞等待,甚至可以在嵌套的子函数中阻塞等待
    • 通过阻塞等待,无需使用状态机等较为复杂的方式来优化缩减每个任务的执行时长
  • 相对于
    RTOS操作系统
    来说,查询式协作多任务系统有以下特点:


    • 没有任务优先级抢占式的功能,因此临界资源(中断除外)和优先级反转的问题也不存在
    • 允许用户或应用程序根据需要自由地切换到下一个就绪任务
    • 通过自主调度和管理任务,查询式协作多任务系统可以提高工作效率
    • 没有操作系统的复杂

功能设计

运行栈空间:程序运行中发生函数调用等情况需要使用的栈内存空间

独立栈任务(有栈任务)

每个独立栈任务
都拥有
自己独立的运行栈空间,可以随时随地阻塞等待,保存上下文后切换到下一个任务执行

独立栈任务在切换下一个任务时,不会操作运行栈,只对上下文切换

共享栈任务(无栈任务)

每个共享栈任务
都没有
自己独立的运行栈空间,虽然也能阻塞等待,但是仅限于在任务入口函数中使用,禁止在任务的子函数(嵌套函数)中阻塞等待;并且在该任务入口函数中不建议定义相关变量。

  • 每个任务有自己的独立备份栈(用来备份运行栈的栈顶部分数据);运行栈通常比备份栈要大很多,否则任务函数无法正常运行多级嵌套的函数
  • 共享栈任务在切换下一个任务时会将当前运行栈(共享栈)提前设置好的备份栈大小(宏配置)拷贝到内存备份起来,等下次即将执行时再从内存中拷贝到运行栈(共享栈)进行恢复
  • 通过修改加大备份栈大小(宏配置)的值,可以在共享栈任务入口函数定义变量,这样可以避免这些变量的值没有备份导致丢失,或者通过 static 定义局部变量
  • 该类型任务适合于轻量的任务处理,一般都是调用封装好的函数即可

注:这里的共享栈任务和常规的实现有一些差异,常规的实现是使用堆申请内存保存栈的数据,用多少申请多少进行保存,而这里的实现仅仅保存了一部分数据。

任务创建

  1. 在调度系统启动前,至少要先创建一个任务,否则直接退出
  2. 可以在任务中创建新的任务,不管是独立栈任务还是共享栈任务
    • 独立栈任务中可以创建新的独立栈任务和共享栈任务
    • 共享栈任务中同样可以创建新的独立栈任务和共享栈任务,而且在创建共享栈任务时可以使用同一个共享栈
  3. 独立栈任务和共享栈任务一共可以创建最多32个任务(需要修改宏配置)

任务销毁

  • 没有提供该功能接口函数,任务入口函数主动退出则自动将任务销毁。
  • 可以通过等待任务退出接口函数在其他任务中等待该任务退出。

任务阻塞

当前任务阻塞提供两种方式:

  • 时间阻塞:需要阻塞多长时间,等时间满足后才会继续执行
  • 事件阻塞:通过事件阻塞,只有事件触发后才会继续执行

使用说明

任务创建/退出

对于创建独立栈任务还是共享栈任务的示例代码:


uint8_t g_task1Stack[1024 * 2];
uint8_t g_task2Stack[1024 * 2];
uint8_t g_task3Stack[1024 * 2];

uint8_t g_sharedStack[1024 * 2];

// 执行完成就退出的任务
void taskfunc3(int arg)
{
    ...
    cotOs_Wait(1000);
    ...
    cotOs_Wait(1000);
}

void taskfunc1(int arg)
{
   /* 不管taskfunc1是独立栈任务还是共享栈任务,都支持创建子任务 */
   cotOs_CreatTask(taskfunc3, COT_OS_UNIQUE_STACK, g_task3Stack, sizeof(g_task3Stack), 0);  // 创建独立栈任务
   cotOs_CreatTask(taskfunc3, COT_OS_SHARED_STACK, g_sharedStack, sizeof(g_sharedStack), 0); // 创建共享栈任务

    while (1)
    {
        ...
        cotOs_Wait(1000);
    }
}

void taskfunc2(int arg)
{
    while (1)
    {
        ...
        cotOs_Wait(10);
    }
}

int main(void)
{
    cotOs_Init(GetTimerMs);
#if 0
    /* 创建独立栈任务 */
    cotOs_CreatTask(taskfunc1, COT_OS_UNIQUE_STACK, g_task1Stack, sizeof(g_task1Stack), 0);
    cotOs_CreatTask(taskfunc2, COT_OS_UNIQUE_STACK, g_task2Stack, sizeof(g_task2Stack), 0);
#else
    /* 创建共享栈任务 */
    cotOs_CreatTask(taskfunc1, COT_OS_SHARED_STACK, g_sharedStack, sizeof(g_sharedStack), 0);
    cotOs_CreatTask(taskfunc2, COT_OS_SHARED_STACK, g_sharedStack, sizeof(g_sharedStack), 0);
#endif
    cotOs_Start();
}

任务限制

对于创建独立栈任务还是共享栈任务,共享栈任务有限制要求,禁止在任务入口函数的嵌套函数中阻塞


uint8_t g_task1Stack[1024 * 2];
uint8_t g_sharedStack[1024 * 2];

void func1_1(void)
{
    ...
    cotOs_Wait(1000);
    ...
    cotOs_Wait(1000);
}

/* 独立栈任务 */
void taskfunc1(int arg)
{
    int arr[10];   // 可以直接定义变量使用

    while (1)
    {
        func1_1();  // 可以在嵌套函数中使用阻塞等待
        ...
        cotOs_Wait(1000);
    }
}

void func2_1(void)
{
    ...
}

/* 共享栈任务 */
void taskfunc2(int arg)
{
    static int arr[10];  // 建议使用static定义任务内变量或者不定义变量

    while (1)
    {
        func2_1();  // 禁止在嵌套函数中使用阻塞等待
        ...
        cotOs_Wait(10);
    }
}

int main(void)
{
    cotOs_Init(GetTimerMs);

    /* 创建独立栈任务 */
    cotOs_CreatTask(taskfunc1, COT_OS_UNIQUE_STACK, g_task1Stack, sizeof(g_task1Stack), 0);

    /* 创建共享栈任务 */
    cotOs_CreatTask(taskfunc2, COT_OS_SHARED_STACK, g_sharedStack, sizeof(g_sharedStack), 0);

    cotOs_Start();
}

任务阻塞/退出

通过时间和事件的方式阻塞


uint8_t g_task1Stack[1024 * 2];
uint8_t g_task2Stack[1024 * 2];
uint8_t g_task3Stack[1024 * 2];

uint8_t g_sharedStack[1024 * 2];

CotOSCondition_t g_eventCv;

// 执行完成就退出的任务
void taskfunc3(int arg)
{
    ...
    cotOs_ConditionWait(&g_eventCv);
    ...
}

void taskfunc1(int arg)
{
   cotOsTask_t task = cotOs_CreatTask(taskfunc3, COT_OS_UNIQUE_STACK, g_task3Stack, sizeof(g_task3Stack), 0);

    while (1)
    {
        ...
        cotOs_Wait(1000);

        if (...)
        {
            // 等待 taskfunc3 任务运行结束后才退出 taskfunc1
            cotOs_Join(task);
            break;
        }
    }
}

void taskfunc2(int arg)
{
    while (1)
    {
        ...
        cotOs_Wait(10);

        if (...)
        {
            cotOs_ConditionNotify(&g_eventCv);  // 通知 taskfunc3 继续执行
        }
    }
}

int main(void)
{
    cotOs_Init(GetTimerMs);
    cotOs_CreatTask(taskfunc1, COT_OS_SHARED_STACK, g_sharedStack, sizeof(g_sharedStack), 0);
    cotOs_CreatTask(taskfunc2, COT_OS_SHARED_STACK, g_sharedStack, sizeof(g_sharedStack), 0);

    cotOs_Start();
}

不同栈类型任务应用场景

  • 独立栈任务(有栈任务)


    • 重量级任务: 提供更多的控制,适用于需要更精确地管理任务状态的情况和执行计算密集型任务的场景
    • 更可预测的内存使用: 在创建时分配栈空间,可以更好地控制内存使用,适用于需要更可预测内存行为的场景
    • 递归调用: 更容易处理递归调用,因为每个任务都有独立的栈空间
  • 共享栈任务(无栈任务)


    • 轻量级任务: 通常更轻量,适用于大量小任务的场景。
    • 内存效率: 适用于内存受限的环境,因为不需要为每个任务分配各自的栈空间(备份栈除外)。

代码链接

cot_os


引言


大家好,我是你们的老伙计秀才!今天带来的是[深入浅出Java多线程]系列的第八篇内容:volatile。大家觉得有用请点赞,喜欢请关注!秀才在此谢过大家了!!!

在当今的软件开发领域,多线程编程已经成为提高系统性能和响应速度的重要手段。Java作为广泛应用的多线程支持语言,其内存模型(JMM)设计巧妙地处理了并发环境下共享资源访问时可能遇到的问题。然而,在多线程间共享数据时,程序员往往会遭遇两个核心挑战:内存可见性和指令重排序。

内存可见性问题主要体现在当一个线程修改了共享变量后,其他线程未必能立即感知到这个变化。在Java内存模型中,主内存与每个线程私有的工作内存相互独立,对变量的读写操作可能会先缓存在工作内存中,进而导致不同线程对同一变量值的认知出现偏差。

指令重排序则是为了优化程序执行效率,编译器和CPU可以在不影响单线程语义的前提下重新安排指令执行顺序。然而,在多线程环境下,这种优化可能导致意想不到的结果,破坏程序的正确性。

volatile关键字在Java多线程编程中起到了关键作用,它为解决上述问题提供了有效的工具。通过使用volatile修饰的变量,可以确保多个线程间的共享状态更新能够及时、准确地传播,并且禁止编译器和处理器对其进行无序执行的优化。例如:

public class VolatileExample {
    volatile int sharedValue;

    public void writerThread() {
        sharedValue = 100// 对volatile变量的写入操作将立即刷新至主内存
    }

    public void readerThread() {
        int localValue = sharedValue; // 对volatile变量的读取操作会从主内存获取最新值
    }
}

在这个简单的示例中,
sharedValue
被声明为volatile类型,保证了writer线程对
sharedValue
的修改能够被reader线程立即看到。接下来的内容将进一步探讨volatile是如何实现这些特性的,以及在实际应用中如何利用volatile来增强多线程代码的安全性和一致性。


基本概念回顾


在深入探讨Java多线程中volatile关键字的特性和应用之前,有必要首先回顾几个关键的概念。
虽然之前的系列文章中已经讲过这些内容了,为了照顾没看过之前系列文章的小伙伴,这里快速带大家复习一下。如果对这部分内容感兴趣的小伙伴,可以去翻翻这个系列的其他文章。

内存可见性
内存可见性是Java并发编程中的一个核心议题。在Java内存模型(JMM)中,所有线程共享同一主内存区域,而每个线程有自己的工作内存(本地缓存)。当一个线程修改了主内存中的共享变量时,该变化可能并不会立即同步到其他线程的工作内存中,从而造成不同线程对同一变量值的读取不一致。例如:

public class VisibilityIssue {
    int sharedValue = 0;

    public void updateValue() {
        sharedValue = 1// 线程A修改了sharedValue
    }

    public void readValue() {
        System.out.println(sharedValue); // 线程B可能无法立即看到线程A的更新
    }
}

使用volatile修饰符则可以确保内存可见性,使得线程A对
sharedValue
的修改能够立刻对线程B可见。

重排序
为优化程序执行性能,编译器和处理器可能会改变代码指令的实际执行顺序,这种现象称为重排序。它发生在多个层面,包括编译阶段的指令优化以及运行时CPU流水线上的动态调整。然而,在多线程环境下,无限制的重排序可能导致不可预测的结果,破坏程序逻辑的一致性。

happens-before规则
为了帮助程序员理解和控制多线程环境下的执行顺序,JVM引入了happens-before规则。这是一个隐含的保证,只要按照这些规则编写代码,JVM就能确保指令在不同线程间按预期的顺序执行。例如,程序中对某个变量的写操作先行发生于随后对该变量的读操作,则写入的数据必定对读取线程可见。

结合上述概念,volatile关键字在Java 5及以后版本中得到了增强,不仅确保了其修饰的变量具有内存可见性,还严格限制了volatile变量与普通变量之间的重排序行为,进而保障了并发场景下数据的一致性和正确性。


volatile的内存语义


在Java多线程编程中,volatile关键字为变量提供了一种特殊的内存语义,确保了数据在多个线程间的正确同步和一致性。这部分将详细解释volatile如何保证内存可见性、禁止重排序以及通过内存屏障实现这些特性的机制。

内存可见性保证
volatile修饰符确保了当一个线程修改volatile变量时,所有其他线程都能立即看到这个更新后的值。考虑以下示例:

public class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1// step 1
        flag = true// step 2
    }

    public void reader() {
        if (flag) { // step 3
            System.out.println(a); // step 4
        }
    }
}

在这个例子中,如果
flag
没有被volatile修饰,那么线程A对
a
的修改可能不会及时反映到线程B读取的值上。然而,由于
flag
是volatile变量,在线程A写入后,JMM会强制将其值刷新至主内存,并且在随后线程B读取
flag
时,会从主内存获取最新的值,并使得线程B本地缓存中的
a
失效,从而重新从主内存加载最新值。

禁止重排序机制
旧版Java内存模型允许volatile变量与普通变量之间的重排序,这可能导致并发问题。为了纠正这一缺陷,JSR-133增强了volatile的内存语义,规定编译器和处理器不能随意重排volatile变量与其他变量的操作顺序。

例如,在双重检查锁定单例模式中,如果没有使用volatile修饰
instance
变量,则初始化过程可能会被重排序,导致返回未完全初始化的对象实例。而volatile可以避免这种风险:

public class Singleton {
    private volatile static Singleton instance; // 使用volatile防止重排序

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class{
                if (instance == null) {
                    instance = new Singleton(); // 不会发生步骤1-3-2的重排序
                }
            }
        }
        return instance;
    }
}

内存屏障作用
为了实现上述内存语义,JVM采用了内存屏障技术来限制编译器和处理器的重排序行为。内存屏障分为读屏障(Load Barrier)和写屏障(Store Barrier),它们分别起到阻止屏障两侧指令重排序和确保数据同步到主内存的作用。

具体来说,针对volatile变量的写操作,会在其前后插入StoreStore和StoreLoad屏障;对于volatile变量的读操作,会在其前后插入LoadLoad和LoadStore屏障。这些屏障的存在确保了volatile变量的写入对所有线程都可见,并且不会与其前后非volatile变量的读写操作发生重排序。

综上所述,volatile关键字通过内存可见性和禁止重排序这两个关键特性,有效地维护了多线程环境下共享变量的一致性和正确性,成为Java并发编程中的重要工具。


volatile的内存屏障实现细节


Java虚拟机(JVM)为了确保volatile变量的内存可见性和禁止重排序特性,采用了内存屏障这一底层硬件支持机制。内存屏障在硬件层面上主要有两种类型:读屏障(Load Barrier)和写屏障(Store Barrier)。它们不仅能够阻止屏障两侧指令的重排序,还负责协调CPU缓存与主内存的数据同步。

当编译器生成字节码时,会在volatile变量相关的读写操作前后插入特定类型的内存屏障:

  1. StoreStore屏障 : 在每个volatile写操作前插入StoreStore屏障,以保证在此屏障之前的普通写操作完成并刷新至主内存之后,才会执行volatile变量的写入操作。例如:
int a = 1// 普通写操作
volatile int v = 2// volatile写操作

// 实际执行时,会在v的写操作前插入StoreStore屏障,确保a的值已刷回主内存

  1. StoreLoad屏障 : 在每个volatile写操作后插入StoreLoad屏障,强制所有之前发生的写操作刷新到主内存,并且使当前处理器核心上的本地缓存无效,这样后续任何线程对volatile或非volatile变量的读取都会从主内存获取最新的数据。
  2. LoadLoad屏障 : 在每个volatile读操作后插入LoadLoad屏障,用于确保在这次volatile读操作之后的其他读操作(不论是volatile还是非volatile)能读取到比它更早的读操作所看到的数据。
  3. LoadStore屏障 : 在每个volatile读操作后再插入LoadStore屏障,防止此volatile读取操作与其后的写操作之间发生重排序,确保在此屏障之后的所有写操作,必须在读取volatile变量的操作完成之后才能执行。

由于不同的处理器架构可能对内存屏障的支持程度不同,Java内存模型采取了一种保守策略,在编译器级别统一插入上述四种内存屏障,从而确保在任意平台上都能获得正确的volatile内存语义。

例如,在双重检查锁定单例模式中,volatile关键字在
instance
变量声明处起着至关重要的作用。如果未使用volatile修饰,初始化过程可能会被重排序为如下错误序列:

Singleton instance; // 假设没有volatile修饰符
public static Singleton getInstance() {
    if (instance == null) { // 第一次检查
        synchronized (Singleton.class{
            if (instance == null) { // 第二次检查
                instance = new Singleton(); // 分解为分配内存、初始化对象、设置引用三个步骤
            }
        }
    }
    return instance;
}

若不使用volatile,初始化步骤可能发生1-3-2的重排序,导致其他线程在实例初始化完成前就访问到了尚未完全初始化的对象。而volatile通过内存屏障的插入,可以避免这种危险的重排序行为,确保了多线程环境下正确地创建单例对象。


volatile的实际应用和用途


作为轻量级同步机制
volatile在Java并发编程中扮演了轻量级的同步角色,它可以确保对单个变量的读/写操作具有原子性,并且提供了一种比锁更轻便的线程间通信方式。例如,在以下场景中,我们可以使用volatile来替代锁:

public class Counter {
    private volatile int count = 0;

    public void increment() {
        count++; // 单线程环境下,count++并不是原子操作,但在多线程环境下,
                 // volatile能保证每次自增后其他线程都能看到最新的值
    }

    public int getCount() {
        return count;
    }
}

尽管volatile提供了内存可见性和一定程度上的原子性,但它并不适合于需要保证复合操作整体原子性的场景,例如涉及多个变量的操作或者复杂的临界区代码块。

禁止重排序的应用场景
volatile的一个重要用途是禁止编译器和处理器进行可能导致程序逻辑错误的重排序行为。特别是在多线程环境中,重排序可能破坏数据依赖关系,导致不可预期的结果。下面以“双重检查锁定”单例模式为例说明这一点:

public class Singleton {
    private volatile static Singleton instance; // 使用volatile关键字

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class{
                if (instance == null) { // 第二次检查
                    instance = new Singleton(); // 实例化对象
                }
            }
        }
        return instance;
    }
}

在这个例子中,如果不使用volatile修饰
instance
变量,则实例化过程可能会被重排序为分配内存、设置引用但未初始化对象、然后返回引用的顺序。而volatile能够通过插入内存屏障避免这种错误的重排序,确保当
getInstance()
方法返回时,实例已经正确地初始化完成。

总之,volatile的关键作用在于它能在不引入复杂锁机制的前提下,实现对共享变量的简单同步与通信。然而,开发者需要注意volatile不能替代锁用于处理复杂状态下的并发控制问题,而是应当根据具体应用场景选择最合适的同步工具。对于那些只需要保持单一变量可见性及有序性的简单同步需求,volatile是一个高效且实用的选择。


总结


volatile关键字在Java多线程编程中扮演了至关重要的角色,它提供了内存可见性和禁止重排序的保证,从而有效地提升了并发环境下的数据一致性与正确性。

首先,在内存可见性方面,volatile修饰的变量确保了当一个线程修改该变量时,其他线程能立即看到这个更新。例如,在如下代码示例中,当
flag
被设置为
true
时,所有读取它的线程都会感知到变化:

public class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;
        flag = true// 线程A对flag的修改对其他线程立即可见
    }

    public void reader() {
        if (flag) { // 线程B能立刻看到线程A设置的flag值
            System.out.println(a);
        }
    }
}

其次,volatile通过引入内存屏障机制严格限制了编译器和处理器的重排序行为,防止因为优化而引发的数据不一致问题。特别是在单例模式中的“双重检查锁定”场景,使用volatile关键字能够确保对象实例化过程不会因重排序导致返回未初始化的对象实例:

public class Singleton {
    private volatile static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class{
                if (instance == null) {
                    instance = new Singleton(); // volatile禁止这里的初始化步骤重排序
                }
            }
        }
        return instance; // 返回已正确初始化的对象
    }
}

然而,尽管volatile提供了一种轻量级的同步机制,但其功能相对有限,仅适用于简单状态共享和单个变量的原子操作。对于涉及复合操作或更复杂的临界区,锁仍然是实现更强同步控制的首选工具。因此,开发者需要根据实际需求权衡性能与安全性的考量,合理选择并运用volatile和锁来构建稳健、高效的并发程序。

本文使用
markdown.com.cn
排版

本文介绍
Git
软件与
GitHub
平台的基本内容、使用方法与应用场景等。

1 初步介绍

首先,什么是
GitHub

Git
?为什么我们要运用这些工具?

首先从
GitHub
说起。如果大家参加过数学建模比赛,或许都经历过这样的历程:一个队伍若干人负责一篇文档的不同部分,而后最终交给一位同学汇总。这时候,由于不断地修改、完善内容,每个人的电脑中或许都出现了无数个名字类似于“
第一部分1
”“
第一部分2
”“
第一部分3
”“
修改1
”“
修改2
”等等这样的
Word
文档,这繁多的
Word
文件无疑给最终的版本合并过程带来了很多烦恼。而数学建模中的
Word
与我们毕业设计中的代码文件类似,也是需要不断更新。

同时,上面所说的数学建模,往往只有3-4天的时间,因此
Word
文档再多也不会过于繁琐;而毕业设计不一样,从去年开始,再到现在,再到最终上交论文的4、5月份,这个时间跨度非常大,如果我们还采用上述这种“
文件名称后加编号
”的笨方法,可能最终的文件编号都要到三位数了;好滴,那我们不加编号了,直接在原有文件的基础上修改,又有新的问题出现了——如果我们在今天直接修改了前天所写的代码,并且保存文件退出了,可是随后发现今天的修改是有问题的,还是前天的那个版本比较正确,但是已经无法撤回,也不好再凭借记忆手动把修改的内容一一准确地重新调整为前天修改前的版本。因此,是不是很烦恼?

那么,
GitHub
就可以解决这样的烦恼。
GitHub
很专业的介绍网上也有,大家百度就可以;况且我也不专业,所以就通俗的说:
GitHub
是一个可以管理同一个文件的不同版本的仓库。或许这么说不太清楚,看了下面一张表,相信大家就了解了:

是的,你没有看错,每一个文件都与其对应的用户、时间与版本修改内容等一一对应,并且每一份文件都可以下载,从而轻松回溯版本。说白了,
GitHub
就是为了实现上述这一工作。而
GitHub
实现上述这一工作是基于线上平台的,换句话说其是一个实现这一目的的网站,而
Git
则是与
GitHub
相呼应的电脑软件;二者结合,从而实现利用
Git
软件将电脑中的代码文件上传至
GitHub
网页中。

值得一提的是,
Git
并非是实现这一功能的唯一电脑软件,但其得到了广大程序员朋友的好评与大量运用。而国内也有类似
GitHub
的网站,例如码云
Gitee
等,与
GitHub
相比,速度和稳定性或许会更好,同时相信大家都是龙的传人、支持本土企业,因此大家都可以多多尝试。

2 使用方法

一般的,
GitHub
的使用有两种方法,一是首先在
GitHub
网页中建立自己的文件,随后同步到本地电脑;二是首先在本地电脑中完成代码文件,随后同步到
GitHub
网页中。在这里我个人认为第一种方法比较方便,因此以第一种为例。

2.1 GitHub配置

首先,我们登录
GitHub
官网:
https://github.com/
。注册账号后,点击屏幕左侧的“
New
”按钮,从而新建
Repositories
。这个
Repositories
就是大家代码等文件的存放之处。随后,为自己的
Repositories
取个名字、加一个简介,其他选项依照下图即可。

完成后,大家就会看到在新建的
Repositories
中已经有了
README

License
文件。

随后,在
Repositories
界面左上角的
Code
界面,依据下图依次选择,并复制对应的
SSH
备用。

2.2 Git配置

首先,我们需要下载
Git
软件,随后安装即可;关于安装的具体方法,我们将在后续的博客中介绍。

随后,我们选择一个合适的文件夹,在这里右键,选择“
Git Bash Here
”打开
Git
,输入代码:

git clone git@github.com:Chutj/Crop_Yield_Prediction_with_Machine_Learning.git

在这里,
clone
后的代码需要替换成大家自己上面复制的
SSH
即可。

随后,就可以发现,前述操作中生成的
README

License
文件都已经在本地文件夹中了。

2.3 代码上传至GitHub

接下来,我们便可以进行代码版本管理与上传。在本地文件夹中写好代码,随后在这一文件夹中右击鼠标打开
Git Bash

以我的
CropYield_DL_Old.py
文件为例,输入代码:

git add CropYield_DL_Old.py

随后输入:

git commit -m"Modify This File"

其中,引号中的内容为文件修改备注,方便大家了解每一次文件修改的详细情况,具体内容可以自行修改。

可以看到,文件修改的具体信息已经被列在代码下方。

最后,输入代码:

git push

即将我们刚刚修改的
CropYield_DL_Old.py
文件上传至
GitHub
中。

至此,即完成了
GitHub

Git
的简单操作。

上述内容和互联网其他关于
GitHub

Git
的操作教程相比,确实十分粗略——由于我不是专业的程序员,因此上述未涉及
Git
的高级操作(例如版本回溯)等,大家可以参照其他更深入的教程加以进一步学习。

这是一篇关于
Fantastic-admin
这款后台管理框架的年终总结。

不过与其说是年终总结,更像是一场回顾,看看这一年
Fantastic-admin
都做了哪些与众不同的功能,也给大家提供一些创造思路。或许有些功能,你可以在自己的项目里实现。

以下按 commit 时间顺序汇总:

可阅读时间

fantastic-admin.gitee.io_pro-example_.png

这是一个将传统日期转化为一个可阅读的时间,在管理系统一些对时效性有要求的模块会有帮助,例如审核模块。

403 无权限访问

Kapture 2024-01-02 at 21.29.41.gif

大部分(几乎所有)同类产品都没有做 403 页面的支持,它们的做法是直接将没有权限的路由剔除掉,所以无论是访问一个不存在的路由,还是访问一个无权限的路由,都会进入 404 页面。

同时也实现了权限变更后,侧边导航菜单动态更新,你可以看上图最左侧导航的变化(当权限变更后,从3个变成了2个)。这也是同类产品无法实现的效果,因为技术方案限制,它们必须刷新浏览器才能实现。

如果你对这个功能的技术实现思路,可以阅读《
这样在管理后台里实现 403 页面实在是太优雅了
》这篇文章以及评论,因为评论中给予我了一个新的思路,所以目前的实现方案和文章中介绍的稍微有点不同。

标签栏持久化

Kapture 2024-01-02 at 21.34.37.gif

虽然 Vue 提供了 SPA 的使用体验,但在实际使用场景中,总会有各种各样的怪异情况发生,导致用户需要整体刷新页面。

这时候标签栏持久化就提供了很好的使用体验,之前访问过的标签页都会被保留,无需再从导航菜单中翻找并重新打开。

标签栏的数据跟随当前账号并存放在本地 sessionStorage 里,当然框架也提供了简单的配置,可以将数据存放到 localStorage 里。

页面切换动效

Kapture 2024-01-02 at 21.44.47.gif

这是一个比较克制的动效,当然整个框架内提供的动效都是比较克制的,我并没有提供太多“酷炫”的切换动效,因为那样就有点喧宾夺主了。

像那种页面反转、旋转的切换动效,持续时间太短在性能较差的电脑上可能会掉帧,但持续时间太长又会影响用户体验。

如果你对 Fantastic-admin 内使用到了哪些动效感兴趣,可以阅读《
我是如何设计后台框架里那些锦上添花的动画效果
》这篇文章,不过时间过去有点久了,文章和现在正在使用的动效会有点差异。

用户偏好设置

localhost_9000_.png

框架将一部分与开发模式无关的配置项提取出来,提供了一个偏好设置模块。只需替换后端接口,就能实现用户自定义使用习惯,而不是每次登录都是系统提供的默认配置。

收藏夹

Kapture 2024-01-02 at 22.00.06.gif

用户可以将导航中常用的模块添加进收藏夹,方便快速访问。这在一些功能模块超级多的大型系统里特别实用,节省了翻找导航的时间。

收藏夹的数据默认跟随当前账号存放在 localStorage 里,当然框架也提供了简单的配置,可以将数据和接口对接,存放到后端数据库。

储物箱

Kapture 2024-01-02 at 22.07.23.gif

这是一个比较 “抽象” 的组件,它的功能是能存放各种数据,仅此而已,数据跟随页面存放在本地 localStorage 内。

一个比较经典的场景就是有一个具有N多筛选项的列表页,而你又经常需要输入不同条件去查询结果,这时候这个组件就派上用场了,可以实现一个筛选条件的常用模版,将常用的筛选条件保存下来,方便下次可以一键切换。

你如果感兴趣,可以点
这里
去实际操作体验一番。

明暗切换过渡动效

Kapture 2024-01-02 at 22.13.58.gif

申明:该创意并非我原创

在实现这个效果后,我也尝试在 VitePress 里实现了这一效果,并提交了
pr
,很幸运,我的前端开发偶像 antfu 当时也在
推特
宣传了一番。虽然最后 VitePress 团队处于各种考虑,并没有合并我的 pr 将这一特性作为默认设置,而是作为一个自定义功能,收录到了[文档](
扩展默认主题 | VitePress
)中。但也因为这个 pr ,我发现社区很多工具都借鉴并集成了这个创意,比如 nuxt devtools ,也算是我对开源社区的一点点小贡献吧。

Hello, 所有UI组件库们

这是今年浓墨重彩的一次更新,彻底从一款基于 Element Plus 的后台管理框架,变成了一款
通用的
后台管理框架。因为之前专门写过一篇文章,这里就不多介绍了,感兴趣的可以阅读《
用1100天做一款通用的管理后台框架
》。

支持拼音搜索

Kapture 2024-01-02 at 22.33.18.gif

一点小优化,支持拼音和拼音首字母快捷搜索。

新窗口打开

Snipaste_2024-01-02_23-28-48.png

有时候我们确实需要多开一个页面,可能是用来对比不同页面内的展示信息,于是就在标签页上增加了这一功能。

同时在导航菜单处,按住 Ctrl/Command + 鼠标点击,也支持在新窗口打开,这个不起眼的功能,我发现有很多同类产品是不支持的。

工具栏自定义布局

Kapture 2024-01-02 at 22.42.31.gif

原先工具栏只能控制功能模块的开启/关闭,现在支持自定义布局。

导航栏自动收起

Kapture 2024-01-02 at 22.48.52.gif

导航栏本身有提供展开/收起的配置,但发现实际使用中,有人会希望能有临时展开的功能,于是开发了这一特性。

标签栏动效调整

Kapture 2024-02-05 at 11.24.44.gif

Kapture 2024-02-05 at 11.31.37.gif

给其中一种风格做了动效调整,当标签页宽度设置为自适应时,动效过渡会更灵动。


个人的年终总结呢?

很奇怪对吧,为什么不是我个人的年终总结?其实是因为我很少给自己设立各种目标,包括做这款产品也一样。因为我发现目标一旦达成了,它也就消失了,这就导致需要不停设立新的目标,结果就是被目标赶着前进,这会让我感觉有点本末倒置,忽略做这件事的初心。(当然工作中被迫还是需要设立目标,以结果为导向)

相反,我做这款产品更多是遵循自己的信念和热爱,我从中提升技能、实践想法、学习营销,至于能走到哪一步,我没有一个清晰的目标,但我知道只要我一直在进步,这就够了。

所以我觉得我更像在打磨一款
艺术品
,我经常不按照大多数同类产品的做法去做功能,你能在这款产品里看到很多我的奇思妙想。当然最终结果也还不错,幸运得到了很多人的赏识,这也让我和我的家人在物质生活上变得更好,很感谢这一年新增的
100
多位付费用户。

差点忘了!

2022 年的时候,我以 Fantastic-admin 的名义赞助了 Element Plus 组织 4000 元,感谢 Element Plus 提供这么好的 UI 组件库。

2023 年框架有了大的更新(上面有提到),加上整年的收入勉强还行,所以我又以 Fantastic-admin 的名义赞助了 Vue 团队 15000 元。另外单独赞助了托尼
antfu
大佬,不管是框架中还是工作中,都用到太多他开发的项目了。还赞助了智子
sxzz
,因为框架侧边栏导航的实现,借鉴了 element-plus 的 el-menu 组件的实现思路,而 el-menu 组件最初就是智子写的。除了这些大家比较熟悉开源大佬,我也赞助了一些小工具作者,比如
vite-plugin-fake-server
的作者,不过赞助那天听闻他刚被裁员,不知是喜是悲。

这一年看到了一些令人唏嘘的新闻,愈发觉得做开源开发者不容易。我自知自己没有能力和运气去做开源,所以通过商业化赚了点钱后,也希望能回馈给开源作者一些实质性的鼓励。

开启新的一年

以上就是 2023 年
Fantastic-admin
新增的大部分特性,更多特性欢迎大家来官网自行体验一番,相信你会发现我们与其他产品的不同。

2024 年我也会继续深耕这款产品,提供更多有趣又实用的新特性,那就一起拭目以待吧!

最后祝所有看到这里的大伙们,新年快乐,功不唐捐!

前言

Docker是一个开源的应用容器引擎,是用Go语言实现的,Docker可以让开发者打包他们的应用和依赖包到轻量级、可移植的容器中,然后可以发布到任何Linux机器中。容器的优点很多,比如打包应用和依赖方便部署;做环境隔离,在做深度学习时就可以在容器中进行,安装各种包,不会污染到宿主机;容器开销性能很低,我们可以无所忌惮地创建容器。下图是在宿主机中创建了三个容器。

在正式学习Docker之前,先介绍一下Docker中的三个基本概念:

镜像(Image)
:Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。
容器(Container)
:镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
仓库(Repository)
:仓库可看成一个代码控制中心,用来保存镜像。

介绍了Docker的使用场景、优点和基本概念,下面讲解一下,Docker的安装。

1.Ubuntu Docker安装

下面是官方提供的安装方法,安装的是Docker Engine,非Docker桌面版,满足要求的Ubuntu系统为64位的20.04、22.04、23.10版本

# 增加 Docker 官方的 GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# 增加源仓库到 Apt 源:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

# 安装最新版本的 Docker
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

# 运行 hello-world 镜像来验证 Docker 是否安装成功
sudo docker run hello-world

# 也可以运行下面的命令查看 Docker 的运行状态
sudo systemctl status docker

2.Ubuntu nvidia-docker安装

在使用Docker做深度学习时,需要使用cuda资源,但是原生的Docker不支持在容器中使用nvidia GPU资源,那么就需要安装nvidia-docker,它是对docker的封装,提供了一些必要的组件,可以方便地在容器中使用GPU资源执行代码。

curl https://get.docker.com | sh
sudo systemctl start docker && sudo systemctl enable docker

# 设置stable存储库和GPG密钥
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -s -L https://nvidia.github.io/nvidia-docker/gpgkey | sudo apt-key add -
curl -s -L https://nvidia.github.io/nvidia-docker/$distribution/nvidia-docker.list | sudo tee /etc/apt/sources.list.d/nvidia-docker.list

# nvidia-docker2更新软件包清单后,安装软件包(和依赖项)
sudo apt-get update
sudo apt-get install -y nvidia-docker2

# 设置默认运行时后,重新启动Docker守护程序以完成安装
sudo systemctl restart docker

安装显卡驱动后就可以使用Docker做深度学习相关的工作了

sudo apt-get install nvidia-driver-535

3.修改Docker默认存储路径

这一步是可选操作,不想在系统盘存储镜像和容器时,就可以更改Docker的存储目录

# 清空docker存储,这条命令慎重使用
docker system prune -a

# 编辑 /etc/docker/daemon.json 文件
vi /etc/docker/daemon.json 
{
  "data-root": "/data/docker-data"
}

# 然后重启 docker 服务
systemctl restart docker

# 再次查看 docker 信息,就可以看见Docker Root Dir发生了变化 
docker info

4.总结

完成了nvidia-docker安装之后,就可以随意在容器中做实验和部署服务了,也不用担心把系统环境污染了和把系统搞坏了。做深度学习时推荐在容器中安装一个jupyter-lab,使用起来非常方便。