这闻起来有点跑题了,但我会尽量让它回到正轨。
抢先式多任务处理意味着操作系统或内核可以暂停当前运行的线程并根据其现有的调度启发式切换到另一个线程。大多数情况下,运行的线程并不知道系统上正在发生其他事情,这对您的代码意味着您必须小心设计它,以便如果内核决定在一个线程中间挂起一个线程多步操作(例如更改 PWM 输出、选择新的 ADC 通道、从 I2C 外设读取状态等)并让另一个线程运行一段时间,这两个线程不会相互干扰。
任意示例:假设您是多线程嵌入式系统的新手,并且您有一个带有 I2C ADC、SPI LCD 和 I2C EEPROM 的小系统。您认为拥有两个线程是个好主意:一个从 ADC 读取并将样本写入 EEPROM,另一个读取最后 10 个样本,对它们进行平均并在 SPI LCD 上显示它们。没有经验的设计看起来像这样(大大简化):
char i2c_read(int i2c_address, char databyte)
{
turn_on_i2c_peripheral();
wait_for_clock_to_stabilize();
i2c_generate_start();
i2c_set_data(i2c_address | I2C_READ);
i2c_go();
wait_for_ack();
i2c_set_data(databyte);
i2c_go();
wait_for_ack();
i2c_generate_start();
i2c_get_byte();
i2c_generate_nak();
i2c_stop();
turn_off_i2c_peripheral();
}
char i2c_write(int i2c_address, char databyte)
{
turn_on_i2c_peripheral();
wait_for_clock_to_stabilize();
i2c_generate_start();
i2c_set_data(i2c_address | I2C_WRITE);
i2c_go();
wait_for_ack();
i2c_set_data(databyte);
i2c_go();
wait_for_ack();
i2c_generate_start();
i2c_get_byte();
i2c_generate_nak();
i2c_stop();
turn_off_i2c_peripheral();
}
adc_thread()
{
int value, sample_number;
sample_number = 0;
while (1) {
value = i2c_read(ADC_ADDR);
i2c_write(EE_ADDR, EE_ADDR_REG, sample_number);
i2c_write(EE_ADDR, EE_DATA_REG, value);
if (sample_number < 10) {
++sample_number;
} else {
sample_number = 0;
}
};
}
lcd_thread()
{
int i, avg, sample, hundreds, tens, ones;
while (1) {
avg = 0;
for (i=0; i<10; i++) {
i2c_write(EE_ADDR, EE_ADDR_REG, i);
sample = i2c_read(EE_ADDR, EE_DATA_REG);
avg += sample;
}
/* calculate average */
avg /= 10;
/* convert to numeric digits for display */
hundreds = avg / 100;
tens = (avg % 100) / 10;
ones = (avg % 10);
spi_write(CS_LCD, LCD_CLEAR);
spi_write(CS_LCD, '0' + hundreds);
spi_write(CS_LCD, '0' + tens);
spi_write(CS_LCD, '0' + ones);
}
}
这是一个非常粗略和快速的例子。不要这样编码!
现在请记住,抢先式多任务操作系统可以在代码中的任何一行(实际上是在任何汇编指令处)暂停这些线程中的任何一个,并让另一个线程有时间运行。
考虑一下。想象一下,如果操作系统决定adc_thread()
在设置 EE 地址以写入和写入实际数据之间暂停会发生什么。lcd_thread()
将运行,与 I2C 外围设备一起读取它需要的数据,当adc_thread()
轮到它再次运行时,EEPROM 将不会处于与它离开时相同的状态。事情根本不会很好地工作。更糟糕的是,它甚至可能在大部分时间都有效,但并非一直有效,而且你会发疯地试图弄清楚为什么你的代码在它看起来应该有效的时候却不工作!
这是一个最好的例子;操作系统可能会决定从 ' 的上下文中抢占并i2c_write()
从adc_thread()
' 的上下文中再次开始运行它lcd_thread()
!事情很快就会变得非常混乱。
当您编写代码以在先发制人的多任务环境中工作时,您必须使用锁定机制来确保如果您的代码在不合时宜的时间暂停,所有地狱都不会崩溃。
另一方面,协作多任务意味着每个线程都可以控制何时放弃其执行时间。编码更简单,但必须仔细设计代码,以确保所有线程都有足够的时间运行。另一个人为的例子:
char getch()
{
while (! (*uart_status & DATA_AVAILABLE)) {
/* do nothing */
}
return *uart_data_reg;
}
void putch(char data)
{
while (! (*uart_status & SHIFT_REG_EMPTY)) {
/* do nothing */
}
*uart_data_reg = data;
}
void echo_thread()
{
char data;
while (1) {
data = getch();
putch(data);
yield_cpu();
}
}
void seconds_counter()
{
int count = 0;
while (1) {
++count;
sleep_ms(1000);
yield_cpu();
}
}
该代码不会按照您的想法工作,或者即使它看起来确实有效,但随着回显线程的数据速率增加,它也不会工作。再一次,让我们花一点时间来看看它。
echo_thread()
等待一个字节出现在 UART 上,然后获取它并等待直到有空间写入它,然后写入它。完成后,它让其他线程轮流运行。seconds_counter()
将增加一个计数,等待 1000 毫秒,然后让其他线程有机会运行。如果在这段时间发生时有两个字节进入 UART sleep()
,您可能会错过看到它们,因为我们假设的 UART 没有 FIFO 来存储字符,而 CPU 正忙于做其他事情。
实现这个非常糟糕的例子的正确方法是把yield_cpu()
你有一个繁忙的循环放在哪里。这将有助于事情进展,但可能会导致其他问题。例如,如果时间很关键,并且您将 CPU 交给另一个花费比您预期更长的线程,您可能会放弃您的时间。抢占式多任务操作系统不会出现此问题,因为它会强制挂起线程以确保正确调度所有线程。
现在这与计时器和后台循环有什么关系?定时器和后台循环与上面的协作多任务示例非常相似:
void timer_isr(void)
{
++ticks;
if ((ticks % 10)) == 0) {
ten_ms_flag = TRUE;
}
if ((ticks % 100) == 0) {
onehundred_ms_flag = TRUE;
}
if ((ticks % 1000) == 0) {
one_second_flag = TRUE;
}
}
void main(void)
{
/* initialization of timer ISR, etc. */
while (1) {
if (ten_ms_flag) {
if (kbhit()) {
putch(getch());
}
ten_ms_flag = FALSE;
}
if (onehundred_ms_flag) {
get_adc_data();
onehundred_ms_flag = FALSE;
}
if (one_second_flag) {
++count;
update_lcd();
one_second_flag = FALSE;
}
};
}
这看起来非常接近协作线程示例;您有一个设置事件的计时器和一个查找它们并以原子方式对其进行操作的主循环。您不必担心 ADC 和 LCD“线程”相互干扰,因为其中一个永远不会中断另一个。您仍然需要担心“线程”花费的时间太长;例如,如果get_adc_data()
需要 30 毫秒会发生什么?您将错过三个检查字符并回显它的机会。
循环+定时器的实现通常比协作式多任务微内核更容易实现,因为您的代码可以针对手头的任务设计得更具体。您实际上并没有多任务处理,而是设计了一个固定系统,在该系统中,您给每个子系统一些时间以非常具体和可预测的方式完成其任务。即使是协作式多任务系统也必须为每个线程提供一个通用的任务结构,并且下一个要运行的线程由可能变得非常复杂的调度函数确定。
所有三个系统的锁定机制都是相同的,但每个系统所需的开销却大不相同。
就个人而言,我几乎总是按照最后一个标准进行编码,即循环+计时器实现。我发现线程应该非常谨慎地使用。不仅编写和调试更复杂,而且还需要更多开销(抢占式多任务微内核总是比愚蠢的简单计时器和主循环事件跟随器更大)。
还有一种说法是任何从事线程工作的人都会欣赏:
if you have a problem and use threads to solve it, yoeu ndup man with y pemro.bls
:-)