这闻起来有点离题,但我会尽力使它回到正轨。
抢先式多任务处理意味着操作系统或内核可以挂起当前正在运行的线程,并根据已有的调度启发式方法切换到另一个线程。大多数情况下,正在运行的线程并不认为系统上还有其他事情在发生,这对您的代码而言意味着您必须谨慎设计代码,以便内核决定在线程中间暂停线程。多步操作(例如更改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()
将增加一个计数,等待1000ms,然后为其他线程运行。如果有两个字节进入UARTsleep()
有,您可能会错过它们,因为我们假设的UART在CPU忙于做其他事情时没有FIFO存储字符。
实现这个非常糟糕的例子的正确方法是 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()
花30ms会怎样?您将错过三个检查字符并回显字符的机会。
循环+计时器的实现通常比协作式多任务微内核的实现容易得多,因为可以将代码设计为更针对手头的任务。您实际上并没有执行多任务处理,而是设计了一个固定的系统,在该系统中,您可以给每个子系统一些时间以一种非常特定且可预测的方式来完成其任务。即使是协作式多任务系统,每个线程也必须具有通用的任务结构,下一个要运行的线程由调度功能确定,而调度功能可能会变得非常复杂。
这三个系统的锁定机制都相同,但是每个系统所需的开销却大不相同。
就我个人而言,我几乎总是将代码编码为最后一个标准,即loop + timer实现。我发现应谨慎使用线程。不仅编写和调试更加复杂,而且还需要更多的开销(抢先式多任务微内核总是比笨拙的计时器和主循环事件跟随器还要大)。
俗话说,任何从事线程工作的人都会体会到:
if you have a problem and use threads to solve it, yoeu ndup man with y pemro.bls
:-)