阿里的iOS协程库 coobjc 源码解析(一)——元组和协程

2023-10-27

Coobjc中的元组

底层主要依赖NSPointerArray进行实现,因为NSPointerArray支持插入nil指针,能配合元组中有对象为nil的特性。

比较引人入胜的设计

主要是co_tuple(...)这个宏定义。

co_tuple(...) [[COTuple alloc] initWithObjects:__VA_ARGS__, co_tupleSentinel()]

__VA_ARGS__作为传参,但是支持nil进行传值。以自定义的全局单例co_tupleSentinel()作为结束标识。这样就可以支持所有类型的外部输入了。

关于协程如何中断和继续

这里涉及到子程序的上下文切换,iOS 系统下,并没有提供直接的触及方式。所以coobjc是通过自己写的汇编,实现了上下文切换。

里面实现了arm64, armv7, x86_64, i386的上下文切换汇编代码,分别对应了64位和32位的真机和模拟器。

上下文切换主要是两个方法:

extern int coroutine_getcontext (coroutine_ucontext_t *__ucp);

extern int coroutine_setcontext (coroutine_ucontext_t *__ucp);

getcontext获取当前的上下文,也就是堆栈信息,setcontext设置当前的上下文。通过这样的方式,在iOS中实现了子程序调用的上下文切换。

image.png

coroutine.m 源码解读

这个文件里是关于协程创建、销毁,协程队列,协程调度管理的代码,里面代码量不多,总共只有300多行,皆由c实现。但由于可中断的设计,里面的源码阅读难度陡然上升。但是只要清楚概念,实际也并不会太难理解。

协程队列

协程队列是一条FIFO的队列,数据结构使用的是链表。每次新增一个协程到调度队列中,都通过尾插入的方式;每次从队列中pop出一个要被执行的协程,都是从链表的头取出。对应具体方法是:

// 添加协程到调度器的队列中
void scheduler_queue_push(coroutine_scheduler_t *scheduler, coroutine_t *co);
// 从调度器的协程队列中取出一个协程
coroutine_t *scheduler_queue_pop(coroutine_scheduler_t *scheduler);

协程和调度器的定义

协程

在coobjc里,协程用结构体进行定义,就是一个子程序的调度单位。在用户态里生成和销毁,十分轻量,上下文切换所涉及的资源量也很小,对CPU来说效率更高。

代码上,协程的定义也很精致,涉及到的数据并不多,定义如下:

    struct coroutine {
        coroutine_func entry;                   // Process entry.
        void *userdata;                         // Userdata.
        coroutine_func userdata_dispose;        // Userdata's dispose action.
        void *context;                          // Coroutine's Call stack data.
        void *pre_context;                      // Coroutine's source process's Call stack data.
        int status;                             // Coroutine's running status.
        uint32_t stack_size;                    // Coroutine's stack size
        void *stack_memory;                     // Coroutine's stack memory address.
        void *stack_top;                    // Coroutine's stack top address.
        struct coroutine_scheduler *scheduler;  // The pointer to the scheduler.
        
        struct coroutine *prev;
        struct coroutine *next;
        
        void *autoreleasepage;                  // If enable autorelease, the custom autoreleasepage.
        void *chan_alt;                         // If blocking by a channel, record the alt
        bool is_cancelled;                      // The coroutine is cancelled
        int8_t   is_scheduler;                  // The coroutine is a scheduler.
    };

结构体上的数据,可以归纳成几点:

  1. 带有协程的调用方法入口,带有始终标记着栈顶的指针;
  2. 带有协程当前对应的上下文 context,以及存储着由于开始协程的运行,而中断的前上下文 pre_context,每次在中断或完成自己的上下文时,调度器会回到写成的pre_context;
  3. 可以绑定一份用户数据;
  4. 协程的基本需求信息,开辟的栈内存,栈空间大小;
  5. 关联当前协程所处队列中的前一个协程与下一个协程;
  6. CSP阻塞时,channel的数据chan_alt
  7. 一些状态位。

由此可以一窥,协程在运行时,具体所需要的数据。

调度器

根据coobjc官方架构介绍文档的表述,协程调度器本身也是一个协程,这个其实从代码上,也能看得出来。
调度器的具体代码,可以放在下一节里说,先来看看调度器具体定义:

     /**
     Define the scheduler.
     One thread own one scheduler, all coroutine run this thread shares it.
     */
    struct coroutine_scheduler {
        coroutine_t         *main_coroutine;
        coroutine_t         *running_coroutine;
        coroutine_list_t     coroutine_queue;
    };

调度器scheduler中一共有三个属性:

  1. main_coroutine: 调度器本身的协程,运行于当前线程中,里面的代码就是一个无限loop,不断将协程队列中的协程取出来运行,类似于iOS的NSRunloop;
  2. running_coroutine: 当前scheduler正在执行的协程,并非main_coroutine,即是从协程队列中取出来执行的协程。调度器的主协程和调度队列的协程类似于包含关系,大的主协程包含协程队列中的协程;
  3. coroutine_queue: 协程队列,一个链表,对应的是添加到当前调度队列中的所有协程。

如官方注释所说,每个线程仅拥有一个调度器,所有在同一线程运行的协程共享同一个调度器。

调度器如何调度协程

image.png

调度器创建与获取
coroutine_scheduler_t *coroutine_scheduler_self_create_if_not_exists(void) {
    
    if (!coroutine_scheduler_key) {
        pthread_key_create(&coroutine_scheduler_key, coroutine_scheduler_free);
    }
    
    void *schedule = pthread_getspecific(coroutine_scheduler_key);
    if (!schedule) {
        schedule = coroutine_scheduler_new();
        pthread_setspecific(coroutine_scheduler_key, schedule);
    }
    return schedule;
}

方法主要是coroutine_scheduler_self_create_if_not_exists,scheduler结构体通过pthread的接口,存储于线程身上。每次该方法都会去取当前线程绑定的scheduler,如果没有,则创建。

coroutine_scheduler_t *coroutine_scheduler_new(void) {
    
    coroutine_scheduler_t *scheduler = calloc(1, sizeof(coroutine_scheduler_t));
    coroutine_t *co = coroutine_create((void(*)(void *))coroutine_scheduler_main);
    co->stack_size = 16 * 1024; // scheduler does not need so much stack memory.
    scheduler->main_coroutine = co;
    co->scheduler = scheduler;
    co->is_scheduler = true;
    return scheduler;
}

该方法为创建scheduler的方法,一方面是创建scheduler这个结构体,另一方面是创建了调度器的主协程,并将主协程的入口定义为coroutine_scheduler_main

// The main entry of the coroutine's scheduler
// The scheduler is just a special coroutine, so we can use yield.
void coroutine_scheduler_main(coroutine_t *scheduler_co) {
    
    coroutine_scheduler_t *scheduler = scheduler_co->scheduler;
    for (;;) {
        
        // Pop a coroutine from the scheduler's queue.
        coroutine_t *co = scheduler_queue_pop(scheduler);
        if (co == NULL) {
            // Yield the scheduler, give back cpu to origin thread.
            coroutine_yield(scheduler_co);
            
            // When some coroutine add to the scheduler's queue,
            // the scheduler will resume again,
            // then will resume here, continue the loop.
            continue;
        }
        // Set scheduler's current running coroutine.
        scheduler->running_coroutine = co;
        // Resume the coroutine
        coroutine_resume_im(co);
        
        // Set scheduler's current running coroutine to nil.
        scheduler->running_coroutine = nil;
        
        // if coroutine finished, free coroutine.
        if (co->status == COROUTINE_DEAD) {
            coroutine_close_ifdead(co);
        }
    }
}

这里涉及了协程的两个最重要的方法,中断和继续方法,对应coroutine_yieldcoroutine_resume_im,正如前面所说,涉及到子程序的中断的代码,阅读难度都会陡然上升,这类似于编程语言中的goto。我们需要仔细判断,当前子程序中断后,后续的执行会去向何方,下次如何继续地回到中断的地方,才能明白这段代码。

好在这段代码写了很多注释,也解释了很多关键问题。

先撇开yieldresume的代码细节,暂时只需要知道这对方法是用来中断和继续协程用的。我们再从宏观的角度,来理解调度器主函数,究竟做了什么。

  1. 首先这是一个无限的循环,前面说了,类似NSRunloop,不断从调度器的协程队列中pop出协程来执行,具体执行一个协程,就是通过coroutine_resume_im函数;
  2. 如果协程队列空了,就会yield掉主协程。这时候loop就暂停了,当前线程的执行去哪了,没有了调度器阻塞住线程的执行资源,当前的线程会回到它本身的runloop调用中,如果有输入的block,它就会执行;
  3. 当下次有新的协程被添加进调度器的队列中,调度器的主协程就会继续,会回到主线程之前yield的地方。而继续调度器主协程的方法,写在了coroutine_add中,如下;
if (!scheduler->running_coroutine) {
    coroutine_resume_im(co->scheduler->main_coroutine);
}

coroutine_add是将协程添加到调度器队列的主要方法,细节后面再说。

协程的新建、销毁

新建

协程的创建,由coroutine_create方法完成:

coroutine_t *coroutine_create(coroutine_func func) {
    coroutine_t *co = calloc(1, sizeof(coroutine_t));
    co->entry = func;
    co->stack_size = STACK_SIZE;
    co->status = COROUTINE_READY;
    
    // check debugger is attached, fix queue debugging.
    co_rebind_backtrace();
    return co;
}

协程的创建,主要涉及协程的入口方法,以及协程的栈空间大小,以及协程的初始状态赋值。co_rebind_backtrace();方法主要是为了修复连接调试器时的一些bug,不展开介绍。

#define STACK_SIZE      (512*1024)

coobjc 为每个协程分配的栈空间是512KB,默认分配的空间实际上挺多的。但是 coobjc 并不会为每个加入到协程队列中的协程直接开辟栈空间,只有真实运行了的协程(resume过),才会拥有栈的内存。

销毁

协程的销毁主要是释放协程的栈空间、记录的上下文,以及它这个结构体本身,代码并不复杂。

void coroutine_close_ifdead(coroutine_t *co) {
    if (co->status == COROUTINE_DEAD) {
        coroutine_close(co);
    }
}

void coroutine_close(coroutine_t *co) {
    
    coroutine_setuserdata(co, nil, nil);
    if (co->stack_memory) {
        coroutine_memory_free(co->stack_memory, co->stack_size);
    }
    free(co->context);
    free(co->pre_context);
    free(co);
}

协程的启动、中断与继续执行

这涉及了协程整个运行周期,主要分为以下这些函数与阶段。

添加协程到协程队列

协程的启动从将协程添加到调度器的队列中开始,添加协程主要是调用coroutine_add:

void coroutine_add(coroutine_t *co) {
    if (!co->is_scheduler) {
        coroutine_scheduler_t *scheduler = coroutine_scheduler_self_create_if_not_exists();
        co->scheduler = scheduler;
        if (scheduler->main_coroutine->status == COROUTINE_DEAD) {
            coroutine_close_ifdead(scheduler->main_coroutine);
            coroutine_t *main_co = coroutine_create(coroutine_scheduler_main);
            main_co->is_scheduler = true;
            main_co->scheduler = scheduler;
            scheduler->main_coroutine = main_co;
        }
        scheduler_queue_push(scheduler, co);
        
        if (!scheduler->running_coroutine) {
            coroutine_resume_im(co->scheduler->main_coroutine);
        }
    }
}

这里主要是几个点:

  1. 这里面有一些容错代码,只有当协程不是一个调度器时,才能够被添加到协程队列中;如果调度器的主协程已经运行结束了,就重建一个——但这从逻辑上不太可能,因为调度器主协程,前面说到,就是一个死循环;
  2. 将协程推入到调度器的队列中;
  3. 如果调度器目前是挂起的状态,则驱动调度器运行(这在前面也提到了,调度器就是在这个方法中正式启动的)。
正式启动/继续一个协程

这个方法就是coroutine_resume_im,上面的函数里,也有调用。其实这个就是正式启动,或者说继续一个协程的核心函数。里面涉及到上下文切换函数的调用。

void coroutine_resume_im(coroutine_t *co) {
    switch (co->status) {
        case COROUTINE_READY:
        {
            co->stack_memory = coroutine_memory_malloc(co->stack_size);
            co->stack_top = co->stack_memory + co->stack_size - 3 * sizeof(void *);
            // get the pre context
            co->pre_context = malloc(sizeof(coroutine_ucontext_t));
            BOOL skip = false;
            coroutine_getcontext(co->pre_context);
            if (skip) {
                // when proccess reenter(resume a coroutine), skip the remain codes, just return to pre func.
                return;
            }
#pragma unused(skip)
            skip = true;
            
            free(co->context);
            co->context = calloc(1, sizeof(coroutine_ucontext_t));
            coroutine_makecontext(co->context, (IMP)coroutine_main, co, (void *)co->stack_top);
            // setcontext
            coroutine_begin(co->context);
            
            break;
        }
        case COROUTINE_SUSPEND:
        {
            BOOL skip = false;
            coroutine_getcontext(co->pre_context);
            if (skip) {
                // when proccess reenter(resume a coroutine), skip the remain codes, just return to pre func.
                return;
            }
#pragma unused(skip)
            skip = true;
            // setcontext
            coroutine_setcontext(co->context);
            
            break;
        }
        default:
            assert(false);
            break;
    }
}

代码也比较长,首先Ready是协程的初始状态,也就是当一个协程从未运行过,它的状态会是Ready,代码也会走进Ready的分支。

而Suspend则是代表,协程曾经运行过,但是被中断了,resume时,也相对简单,就是保存但前的上下文,并且切换到协程之前中断的上下文中。

而首次启动协程,涉及到的内容,相对多一些。也会保存当前的上下文到pre_context指针中,除此外主要是以下几点:

  1. 初始化协程对应的栈空间,记录栈顶的位置;
  2. 利用coroutine_makecontext函数,新建一个程序调用上下文,并且堆栈从coroutine_main开始;
  3. 利用coroutine_begin来切换到新建的上下文中,功能其实和coroutine_setcontext是类似。

这里的第二条可能比较难理解,贴出一张调试示意图解释:

也就是当前堆栈的栈顶,从coroutine_main函数开始了,也看不到coroutine_resume_im的踪迹了。

这里这么做的核心原因自然是这样更省栈空间,其次它带来了一些好处。我们在调试我们相关的代码时,不需要再关注协程的中间代码了。

协程的中断

协程的中断,并不是被动行为,而是主动行为。如果协程自己不交出运行权限,实际上调度器永远不会调度到下一个协程。这既是协程自己的特点,也是协程和线程不同的点。

协程和线程一样,会霸占CPU的时间片,但和线程不一样的是,协程不会定时切换。所以只要协程没有主动让渡执行权限,它就会一直运行。所以某种程度上,同一协程队列里的多任务,并不是同时进行的,它们是串行的。

但是它可以在遇到IO阻塞时,主动让渡执行权限。在这种模式下,只要设计得当,协程可以实现经典的生产者消费者模型,也可以解决多线程中的竞态条件问题,并且会比多线程加锁、以及信号量这些方式,高效得多。

coobjc中,协程的中断是coroutine_yield实现的:

void coroutine_yield(coroutine_t *co)
{
    if (co == NULL) {
        // if null
        co = coroutine_self();
    }
    BOOL skip = false;
    coroutine_getcontext(co->context);
    if (skip) {
        return;
    }
#pragma unused(skip)
    skip = true;
    co->status = COROUTINE_SUSPEND;
    coroutine_setcontext(co->pre_context);
}

依然是通过自己实现的上下文切换汇编代码,来获取当前上下文coroutine_getcontext和设置上下文coroutine_setcontext

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

阿里的iOS协程库 coobjc 源码解析(一)——元组和协程 的相关文章

  • 如何为移动应用程序创建无密码登录

    我有兴趣在移动应用程序和 API 之间构建某种无密码登录 假设我可以控制两者 动机是必须登录对用户来说非常烦人并且存在安全风险 例如 用户将重复使用现有密码 我希望用户能够立即开始使用该应用程序 我想知道是否有一些可行的技术 例如 在移动设
  • 自定义 OpenVPN iOS 客户端 [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我正在开发一个自定义 iOS OpenVPN 客户端 我找到了这个原生 OpenVPN 客户端核心源码https staging op
  • Facebook iOS 选择好友表空白

    我正在尝试将 选择的朋友 添加到我的 iOS 应用程序中 我设置了登录视图 登录后 我打开朋友选择器 但它显示为空白 我看到带有 完成 和 取消 按钮的表 但表中没有加载任何朋友 IBAction selectFriendsButtonAc
  • 方法调用中的插入符[重复]

    这个问题在这里已经有答案了 我正在阅读本教程 并遇到了这行代码 这让我感到困惑 localSearch startWithCompletionHandler MKLocalSearchResponse response NSError er
  • Xcode 6.4 Swift 单元测试无法编译:“GPUImage.h 未找到”“无法导入桥接标头”

    我的 Xcode 项目构建并运行良好 它有 Swift 和 Objective C 代码 它已安装 GPUImage 我向它添加了单元测试 现在它将不再编译 找不到 GPUImage h 文件 导入桥接标头失败 以下是我发现并尝试过的解决方
  • Objective-C++ 中的 boost::shared_ptr

    这是对我之前提出的一个问题的更好理解 我有以下 Objective C 对象 interface OCPP MyCppobj cppobj end implementation OCPP OCPP init cppobj new MyCpp
  • 将 Xcode 4.5 新 XIB 文件恢复到 iOS<6

    我已经安装了Xcode 4 5 with iOS6 SDK以及其他用于测试目的的旧 SDK 从 4 3 到 6 0 很美 但是有一个BIG问题 生成一个新的 XIB 文件以兼容 iOS6 这是一个问题 因为我的应用程序需要运行在旧设备 不只
  • React Native ios运行问题

    我是反应本机和运行新手yarn ios我的 React Native 项目不断失败并出现以下错误 构建失败 以下构建命令失败 编译C Users gift Library Developer Xcode DerivedData gainer
  • swift 中闭包和函数作为参数的区别

    我有将近 4 年的 Objective C 经验 并且是 swift 的新手 我试图从 Objective C 的角度理解 swift 的概念 所以如果我错了 请指导我 在目标 c 中 我们有块 可以稍后异步执行的代码块 这绝对是完全合理的
  • 通过应用程序组在应用程序之间通信和保存数据

    iOS 8 昨天发布了一个有关应用程序组的新 API 以前在应用程序之间共享数据和通信有点混乱 我相信这正是应用程序组旨在纠正的问题 在我的应用程序中 我启用了应用程序组并添加了一个新组 但我找不到任何有关如何使用它的文档 文档和 API
  • 如何通过我的 ios 应用程序的指示打开苹果地图应用程序

    我的目标是从 ios 应用程序打开带有方向的地图应用程序 我可以打开地图应用程序 但它没有显示方向 我编写的代码如下 NSString mystr NSString alloc initWithFormat http maps apple
  • 如何在 iOS 中设置视图的最大宽度?

    我的应用程序有一个基本的登录屏幕 一个外框以及其中的一些文本字段和按钮 我将框设置为填满屏幕 然而 在某些设备上这个盒子会太大 如何设置视图的最大宽度和高度 您可以使用自动布局约束 使框适应屏幕尺寸 但不超过给定的宽度和高度 为此 请对宽度
  • 在 UIView 中绘制彩色文本 -drawRect: 方法

    我正在尝试在我的中绘制彩色文本UIView子类 现在我正在使用单视图应用程序模板 用于测试 除了以下内容外 没有任何修改drawRect method 文本已绘制 但无论我将颜色设置为什么 它始终是黑色的 void drawRect CGR
  • Cordova Phonegap“导出失败”错误代码 70 构建 ios 时

    我目前正在使用 Cordova Phonegap 构建 iOS 应用程序 本来工作正常 但现在运行时出现错误cordova build ios在终端中 我收到以下错误 导出失败 错误 命令的错误代码 70 带有参数的 xcodebuild
  • 将 UIRefreshControl 用于 UIWebView

    我在 iOS 6 中看到了 UIRefreshControl 我的问题是是否可以通过下拉来刷新 WebView 而不是像在邮件中那样让它弹出 我使用 rabih 的代码是 WebView UIRefreshControl refreshCo
  • 如何检测 iOS 8 上的包含应用程序是否启用了应用程序扩展?

    我正在 iOS 8 beta 上开发一个自定义键盘 我想告诉用户如果我的自定义键盘未启用 如何在包含应用程序中启用它 有什么方法可以检测应用程序扩展是否已启用 首先让我们设置一些常量 以便于彼此理解 包含应用程序 安装扩展并保存扩展二进制文
  • TableView 中图像的大小不正确

    我正在使用来自 URL 的图像创建一个表视图 但图像不会调整到所有视图的大小 直到我将其按入行中 知道为什么会发生这种情况吗 这是一个自定义的表格视图 我的代码是 UITableViewCell tableView UITableView
  • CIAdditionCompositing 给出不正确的效果

    我正在尝试通过平均其他几个图像来创建图像 为了实现这一点 我首先将每个图像变暗 其系数等于我平均的图像数量 func darkenImage by multiplier CGFloat gt CIImage let divImage CII
  • 弱变量中间为零

    弱变量什么时候变为零 weak var backgroundNode SKSpriteNode texture SKTexture image initialBackgroundImage backgroundNode position C
  • 我如何用 javascript/jquery 进行两指拖动?

    我正在尝试创建当有两个手指放在 div 上时拖动 div 的功能 我已将 div 绑定到 touchstart 和 touchmove 事件 我只是不确定如何编写这些函数 就像是if event originalEvent targetTo

随机推荐

  • Keil写STM32程序.axf: Error: L6218E: Undefined symbol HAL_RTC_Init (referred from rtc.o)报错解决办法

    在写RTC的程序时 报如下的错误 Output atk f103 axf Error L6218E Undefined symbol HAL RTC Init referred from rtc o 显示没有定义 网上有很多解决办法 这里提
  • 【Docker 1(2),分享一波阿里、字节、腾讯、美团等精选大厂面试题

    8 卸载 yum remove docker ce docker ce cli containerd io rm rf var lib docker rm rf var lib containerd 三 run的流程和docker原理 1
  • 还在愁csdn进不去吗,看这里

    浏览器其他页面都可以发送请求进入 唯独www csdn net进不去 一直处于访问不了状态 目录 1 DNS的问题 2 关闭防火墙 3 清除浏览器数据 4 关闭代理服务器 5 VPN 6 切换其他网络 相信很多的网页是这样的 然后打开Win
  • 定时器每隔n秒请求n条数据,setInterval分批请求数据

  • 前端测试——端对端测试框架 Playwright 总结

    在进行前端测试前 我们需要明确我们需要怎样的前端测试 前端测试类型总结 前端应用测试分为几种常见类型 端到端 e2e 一个辅助机器人 表现得像一个用户 在应用程序周围点击 并验证其功能是否正确 常见的测试框架是 Playwright 单元
  • 引导过程以及服务控制

    目录 服务器开机过程 开机自检 BIOS MBR主引导程序 grub菜单 加载内核 init初始化 步骤说明 初步检测 mbr引导 加载linux内核 驱动系统 系统初始化 命令 控制类型 运行级别相关命令 运行级命令 服务器开机过程 开机
  • 管理科学与工程 国内核心期刊 国外a刊及SCI

    国内 管理科学与工程 管理科学学报 A 匿名审稿 绝对牛刊 不比一般的SCi期刊的质量差 系统工程理论与实践 A 实名审稿 关系稿很多 尤其是挂编委的文章很多 但质量尚可 系统工程学报 A 匿名审稿 侧重方法论多写 编辑部的老师特好 中国管
  • unity 判断是否点击在某个面板身上

    using System Collections using System Collections Generic using UnityEngine public class TestImage MonoBehaviour Use thi
  • 随机变量序列的收敛性质分类

    分类 X n 趋向某个固定的数 X n 趋向某个确定函数的输出值 X n 的概率分布越来越接近某个特定的随机变量的概率分布 X n 和某个特定随机变量的差别的平均值 数学期望值 趋向于0 X n 和某个特定随机变量的差别的方差趋向于0 约束
  • 面试题:String 和 StringBuilder、StringBuffer 的区别?

    Java 平台提供两种类型的字符串 String 和 StringBuffer StringBuilder 它们可以存储和操作字符串 其中String是只读字符串 也就意味着String 引用的字符串内容是不能被改变的 而StringBuf
  • 多益网络校招笔试题

    马上要参加多益的笔试了 所以在网上找了一下多益的笔试题 原文 我感觉我想出了一个更简单的方法 时间复杂度O 1 如果有问题希望大家及时指正 题目如下 给定一个数x x gt 5 找到该数与3 4之间的关系 关系如下 x 3 n 4 m 然后
  • 最近我在忙什么之【毕业设计大纲】

    毕业设计工作日志 误差校正仿真 理论部分 Stewart平台位姿误差分析与标定研究 仿真部分 基于Matlab的全局搜索 单通道控制算法设计 滑模论文 根据论文仿真 填入参数 获取具体的传递函数 改进滑模的论文 扰动及对照实验设计 稳定平台
  • Ubuntu下使用MySQL(C++,Cmake)

    安装需要使用的库 sudo apt get install libmysqlclient dev 头文件 usr include mysql mysql的头文件在这里 引入头文件 include mysql h 如果找不到就 include
  • python web.py+requests 视频接收与发送

    web py是python中一个相对容易上手的web服务器搭建工具 1 安装方式 web py可以直接通过pip install 的方式安装即可 即 pip install web py 2 服务器 2 1 完整程序 import web
  • 迷宫问题—回溯法

    文章目录 一 项目分析的一般步骤 二 迷宫问题的具体解决 1 需求分析 2 问题分析 2 1 问题分析 2 2 数据结构设计的分析 3 设计 流程图设计 代码设计 3 1流程图设计 3 2代码设计 4 代码测试 5 完成交付 一 项目分析的
  • Springboot+Mybatis,dao加上@Repository注解无法注入

    在springboot 中 给mapper的接口上加上 Repository 无法生成相应的bean 从而无法 Autowired 这是因为spring扫描注解时 自动过滤掉了接口和抽象类 这种情况下可以在启动的类前加 上 MapperSc
  • 如何在 Python 中创建元组字典

    本演练是关于在 Python 中创建元组字典的全部内容 此数据结构存储键值对 通过组合字典和元组 可以创建元组字典 好处是以结构化格式组织且可访问的数据 可以轻松表示每个键的多个值 例如学生成绩或联系信息 让我们看看它如何有效地存储和检索复
  • 抖音生活小妙招类短视频创作技巧分享,几个方面带你了解整个流程

    想做抖音 又不想真人出镜 该选择什么项目做呢 更多精彩干货请关注共众号 萤火宠 免费领取108个抖音小项目 我们的学员中有宝妈 有大学生 也有不少职场人员 他们大多数都非常普通 没有什么很强的职业技能 也没有什么丰富的专业知识 但是他们有人
  • 找实习、工作的一点浅见

    一 实习的必要性 为什么需要去实习 1 实习能帮助自己增进对于具体职场的认识 包括具体工作的职责 内容 工作氛围 是否有较大压力等等 2 通过一段时间的实习经历 能帮助自己作出未来是否能胜任类似的工作的判断 如果有留用 是否考虑留下 如果没
  • 阿里的iOS协程库 coobjc 源码解析(一)——元组和协程

    Coobjc中的元组 底层主要依赖NSPointerArray进行实现 因为NSPointerArray支持插入nil指针 能配合元组中有对象为nil的特性 比较引人入胜的设计 主要是co tuple 这个宏定义 co tuple COTu