【第21期】任务拆分:从Super Loop到模块化Task

核心目标:解耦。让不同功能的代码在各自的平行时空中运行,不再相互掣肘。

1. 裸机时代的痛:超级循环 (Super Loop)

在裸机开发中,我们的 main 函数通常长这样

void main(void) {
    Init();
    while (1) {
        Scan_Keys();       // 1. 扫按键 (快)
        Read_Sensors();    // 2. 读传感器 (中)
        PID_Compute();     // 3. 算控制量 (快)
        Update_Display();  // 4. 刷屏幕 (慢,极慢!)
        Report_UART();     // 5. 报数据 (慢)
    }
}

逻辑死结Update_Display() 是个“耗时大户”。假设刷新整个屏幕需要 30ms(对于 SPI 屏很正常)。 这意味着,你的 Scan_Keys()PID_Compute() 也就是每 30ms 才能执行一次。

  • 按键体验:可能会觉得肉肉的,不灵敏。

  • PID 控制:控制周期不稳定,甚至太长导致系统震荡。

为了解决这个问题,裸机高手会把按键和 PID 放到定时器中断里。但中断里不能干太多事,否则主循环又跑不动了。这就是耦合 (Coupling) —— 一个模块的性能瓶颈,拖累了整个系统。


2. RTOS 拆分原则:各司其职

在 RTOS 中,我们将上述逻辑拆解为独立的任务 (Task)。每个任务都有自己的 while(1),自己的栈空间,以及最重要的——优先级

实战拆分案例:智能温控器

我们至少应该拆分为 3 个任务:

任务 A:交互任务 (Task_HMI)

  • 职责:负责按键扫描、旋钮读取。

  • 优先级 (High)。

  • 逻辑:人的操作是最需要即时反馈的。无论 CPU 在干什么,只要用户按了键,系统必须立刻响应(比如置个标志位或发个信号)。

任务 B:控制任务 (Task_Control)

  • 职责:读取传感器、跑 PID 算法、控制继电器/电机。

  • 优先级 (Medium) 或 (High)。

  • 逻辑:这是产品的核心业务,必须保证稳定的采样和计算周期(例如严格的 10ms)。可以使用 vTaskDelayUntil 来保证绝对周期。

任务 C:显示任务 (Task_Display)

  • 职责:刷新 LCD 界面、打印串口日志。

  • 优先级 (Low)。

  • 逻辑:这是最耗时且最不紧急的工作。屏幕每秒刷 10 帧还是 20 帧,对功能没影响。只有当按键没按下、PID 也没在算的时候,CPU 才会来刷屏。


3. 代码形态的对比

RTOS 下的代码结构:

// 1. 显示任务 (低优先级)
void Task_Display(void *pvParam) {
    while(1) {
        // 慢慢刷,不用担心阻塞别人
        LCD_DrawString("Temp: 25.5"); 
        vTaskDelay(100); // 10FPS 足够了
    }
}

// 2. 控制任务 (高优先级)
void Task_Control(void *pvParam) {
    TickType_t xLastWakeTime = xTaskGetTickCount();
    while(1) {
        // 严格每 10ms 执行一次
        float temp = Read_ADC();
        float out = PID_Calc(temp);
        Motor_Set(out);
        
        // 绝对延时
        vTaskDelayUntil(&xLastWakeTime, 10); 
    }
}

神奇的效果: 当 Task_Display 正在努力画图(执行那几千行 SPI 传输代码)时,如果 10ms 时间到了,RTOS 会强行打断画图任务,切换到 Task_Control 去算 PID。算完后,再切回来继续画图。

  • PID 工程师:我的算法周期稳如泰山。

  • UI 工程师:我终于可以用复杂的动画效果了,不用担心卡死按键。

这就是抢占式调度 (Preemptive Scheduling) 的魅力。


4. 警惕反模式:上帝任务 (God Task)

新手常犯的一个错误是:虽然用了 RTOS,但只创建了一个任务,然后把原来的裸机 while(1) 代码原封不动地拷进去。

// 错误示范:上帝任务
void Task_App(void *pvParam) {
    while(1) {
        Scan_Keys();
        PID();
        Display();
        vTaskDelay(10);
    }
}

这就失去了 RTOS 的所有意义。如果你发现你的某个任务代码行数超过了 500 行,或者它既管硬件又管界面还管算法,请立刻把它拆分。


关键点总结

  1. 解耦:将快任务(按键、控制)与慢任务(显示、日志)分离。

  2. 优先级:紧急的给高优先级,耗时的给低优先级。

  3. 各扫门前雪:每个任务只关注自己的逻辑,不用再担心“我这一行代码会不会卡死整个系统”。

/******************************************************
 * 本文为作者《嵌入式开发基础与工程实践》系列文章之一。
 * 关注即可订阅后续内容更新,采用异步推送机制,无需主动轮询。
 * 转发本文可视为一次网络广播,有助于更多节点接收该信息。
 ******************************************************/

# demo_scrapy_playwright_split.py # 拆分版本:Scrapy 爬虫管理浏览器生命周(启动单例、关闭、可选监控) # BrowserManager 独立管理浏览器,任务类管理上下文和超时 # Scrapyd 友好,支持 Windows/Linux import os import asyncio import logging import time from pathlib import Path import scrapy from scrapy import signals from playwright.async_api import async_playwright, TimeoutError as PWTimeout ''' 我已经把浏览器管理逻辑独立成 BrowserManager 类: BrowserManager → 负责启动、关闭、监控浏览器; PlaywrightTaskManager → 负责任务执行与超时控制; DemoSpider → 只负责调度和生命周绑定。 这样结构更清晰,后续要扩展资源监控(CPU、内存、上下文数等)时,可以直接在 BrowserManager 里实现。 ''' SCREENSHOT_DIR = Path("screenshots") SCREENSHOT_DIR.mkdir(exist_ok=True) class BrowserManager: def __init__(self): self._pw = None self.browser = None self._started = False async def start(self): if not self._started: self._pw = await async_playwright().start() self.browser = await self._pw.chromium.launch(headless=True) self._started = True logging.getLogger(__name__).info("Browser singleton started") async def close(self): if self._started: try: await self.browser.close() finally: await self._pw.stop() self._started = False logging.getLogger(__name__).info("Browser singleton closed") def is_running(self): return self._started and self.browser is not None class PlaywrightTaskManager: def __init__(self, browser_manager: BrowserManager, timeout: int = 20): self.browser_manager = browser_manager self.timeout = timeout async def _task_logic(self, url: str): context = await self.browser_manager.browser.new_context() page = await context.new_page() screenshot_path = None try: await page.goto(url, wait_until="load") title = await page.title() # time.sleep(5) await asyncio.sleep(5) screenshot_path = str(SCREENSHOT_DIR / f"{int(asyncio.get_event_loop().time())}.png") await page.screenshot(path=screenshot_path, full_page=True) return {"url": url, "title": title, "screenshot_path": screenshot_path} finally: try: await context.close() except Exception: pass async def run_task_with_timeout(self, url: str): try: result = await asyncio.wait_for(self._task_logic(url), timeout=self.timeout) return result except asyncio.TimeoutError: return {"url": url, "error": f"timeout after {self.timeout}s"} except PWTimeout: return {"url": url, "error": f"Playwright timeout after {self.timeout}s"} except Exception as e: return {"url": url, "error": str(e)} class DemoSpider(scrapy.Spider): name = "demo_playwright_task_manager1" start_urls = [ "https://www.hao123.com/", "http://www.people.com.cn/", "http://renshi.people.com.cn/", ] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.timeout = 3 self._loop = None self.browser_manager = BrowserManager() self.task_manager = None @classmethod def from_crawler(cls, crawler, *args, **kwargs): spider = super(DemoSpider, cls).from_crawler(crawler, *args, **kwargs) crawler.signals.connect(spider.spider_opened, signal=signals.spider_opened) crawler.signals.connect(spider.spider_closed, signal=signals.spider_closed) return spider def spider_opened(self): # 独立事件循环,避免 "no running event loop" self._loop = asyncio.new_event_loop() asyncio.set_event_loop(self._loop) self._loop.run_until_complete(self.browser_manager.start()) self.task_manager = PlaywrightTaskManager(self.browser_manager, timeout=self.timeout) def spider_closed(self): if self.browser_manager.is_running(): self._loop.run_until_complete(self.browser_manager.close()) self._loop.close() def start_requests(self): for url in self.start_urls: self.logger.info("Dispatching task for %s", url) result = self._loop.run_until_complete(self.task_manager.run_task_with_timeout(url)) print(result) yield { "url": result.get("url"), "title": result.get("title"), "screenshot": result.get("screenshot_path"), "error": result.get("error"), }
10-12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值