Минималистичная очередь задач на C.

Недавно тут появилась замечательная статья про реализацию кольцевого буфера на C++ [1]. Статья весьма ценная и полезная, но, как вполне справедливо заметили в комментариях, что бы писать на крестах под контроллеры надо весьма хорошо знать этот язык, иначе красивый и, вроде бы, элегантный плюсовый код собирается в ресурсоёмкое тормозилово. Я и сам хоть и пишу десктопные проги на плюсах, под контроллеры пока предпочитаю использовать чистый С. Так вот, по этому поводу вспомнилось мне, что я как раз недавно занимался шлифовкой очереди задач на основе кольцевого буфера на сях, и я решил поделиться своими наработками — вдруг кому пригодится. Код, разумеется, платформонезависимый (ну кроме конструкции ATOMIC_BLOCK), а вот оптимизация делалась под avr-gcc и на других платформах/компиляторах может быть излишней.
На всякий случай: я не говорю, что мой код лучше, или вообще хоть как-то сопоставим с тем, что описан в вышеуказанной статье — это совершенно разные вещи, проектировавшиеся для решения совершенно разных задач, статья упомянута просто по ассоциации, так как у этих алгоритмов общая основа — кольцевой буфер.
Итак, идея очереди задач проста как пять копеек — есть массив указателей и два индекса — начало и конец. Есть две функции — одна ставит задачу в конец очереди, другая берёт задачу из начала и выполняет её. В первом приближении выглядело это как-то так:

#define TASK_BUF_SIZE		32		/*размер буфера*/

//коды возврата функции-постановщика
#define TASK_BUF_ERR_NON	0		/*ошибок не возникло*/
#define TASK_BUF_ERR_FULL	1		/*переполнение очереди*/

//коды возврата функции-выполнятора
#define TASK_BUF_JOB_DONE	0		/*задача выполнена*/
#define TASK_BUF_EMPTY	1		/*очередь пуста*/

typedef uint8_t job_arg_t;				//определяем тип для аргумента, передаваемого задаче.
typedef void (*job_t)(job_arg_t);			//объявляем тип для задач (функция, принимающая аргумент job_arg_t и не возвращающая значения)
//макрос для объявления задач
#define DECLARE_JOB(a_JOB_NAME)		void a_JOB_NAME(job_arg_t a_arg)

//объявляем очередь задач и индексы начала и конца
struct
{
	job_t		job;
	job_arg_t	arg;
}				G_task_buf[TASK_BUF_SIZE]; //buffer
uint8_t		G_task_buf_start = 0;		//индекс начала буфера указывает на элемент, который нужно выполнить
uint8_t		G_task_buf_end = 0;		//индекс конца буфера указывает на последний положенный элемент

//функция, кладущая новую задачу в очередь
inline uint8_t post_job(job_t a_job, job_arg_t a_arg)
{
	if(a_job == NULL)		//защита от некорректного вызова
		return TASK_BUF_ERR_NON;
	ATOMIC_BLOCK(ATOMIC_RESTORESTATE)	//дальнейшие действия должны быть атомарными
	{
		if(G_task_buf[G_task_buf_end].job)		//если последняя поставленная в очередь задача ещё не выполнена 
		{
			uint8_t index = (G_task_buf_end < (TASK_BUF_SIZE-1)) ? G_task_buf_end + 1 : 0; //вычисляем индекс следующего элемента
			if(index == G_task_buf_start)	//если этот индекс совпадает с индексом начала буфера, то значит в буфере места нет 
				return TASK_BUF_ERR_FULL; //выход с ошибкой, ATOMIC BLOCK отрабатывает корректно
			G_task_buf_end = index; //сохраняем вычисленный индекс
		}
		G_task_buf[G_task_buf_end].job = a_job; //ставим задачу по нужному индексу
		G_task_buf[G_task_buf_end].arg = a_arg;
	}
	return TASK_BUF_ERR_NON;		//выходим из функции

}

//функция выполняющая задачу из очереди
inline uint8_t do_job()
{
	job_t job = NULL;	//временные переменные для хранения
	job_arg_t arg = 0;	//вынутой из очереди задачи

	ATOMIC_BLOCK(ATOMIC_RESTORESTATE)	//выемка задачи из очереди атомарна
	{
		while(job == NULL)	// пока не достанем из очереди ненулевую задачу
		{
			job = G_task_buf[G_task_buf_start].job;		//достаём из буфера
			arg = G_task_buf[G_task_buf_start].arg;		//первый элемент
			G_task_buf[G_task_buf_start].job = NULL;	//стираем вынутую задачу
			if(G_task_buf_start == G_task_buf_end)		//если конец очереди указывает на её начало, значит буфер пуст, или в нём только одна задача
				break;						//дальше не крутим
			G_task_buf_start = (G_task_buf_start < (TASK_BUF_SIZE-1)) ? G_task_buf_start + 1 : 0;	//вычисляем индекс следующего элемента
		}
	}

	if(job == NULL)	//не нашли ненулевого элемента - буфер пуст.
		return TASK_BUF_EMPTY;
	(*job)(arg);		//выполняем задачу
	return TASK_BUF_JOB_DONE;
}


Не трудно заметить, что код здесь весьма не оптимален. Тем не менее, посмотрим, что у нас получилось.
В памяти выделен массив на TASK_BUF_SIZE элементов, каждый из которых занимает по три байта (два байта адрес и один байт аргумент). В первый момент времени индексы начала и конца указывают на нулевой элемент. Выполнятор смотрит, что индексы равны, а адрес по этому индексу нулевой и считает, что очередь пуста. Когда нужно добавить один элемент, добавлятор смотрит, что он указывает на нулевой элемент и пишет задачу туда. Выполнятор вынимает задачу, смотрит, что индексы совпадают и считает, что в очереди был только один элемент, по-этому можно ничего не трогать. Когда элемент добавляется в не пустой буфер, добавлятор сначала инкрементирует индекс конца, а затем пишет новый элемент туда. Выполнятор при выемке из буфера, в котором более одного элемента, делает инкремент индекса начала. Если добавлятор видит, что после инкремента индекса конца он совпадает с индексом начала, значит буфер полностью забит и новый элемент не добавляет.
А теперь взглянем, что по этому поводу думает avr-gcc. Код функции post_job

inline uint8_t post_job(job_t a_job, job_arg_t a_arg)
{
	if(a_job == NULL)
		return TASK_BUF_ERR_NON;
	ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
 2a2:	3f b7       	in	r19, 0x3f	; 63
    return 1;
}

static __inline__ uint8_t __iCliRetVal(void)
{
    cli();
 2a4:	f8 94       	cli
	{
		if(G_task_buf[G_task_buf_end].job)
 2a6:	20 91 05 01 	lds	r18, 0x0105
 2aa:	82 2f       	mov	r24, r18
 2ac:	90 e0       	ldi	r25, 0x00	; 0
 2ae:	fc 01       	movw	r30, r24
 2b0:	ee 0f       	add	r30, r30
 2b2:	ff 1f       	adc	r31, r31
 2b4:	e8 0f       	add	r30, r24
 2b6:	f9 1f       	adc	r31, r25
 2b8:	e7 5e       	subi	r30, 0xE7	; 231
 2ba:	fe 4f       	sbci	r31, 0xFE	; 254
 2bc:	80 81       	ld	r24, Z
 2be:	91 81       	ldd	r25, Z+1	; 0x01
 2c0:	89 2b       	or	r24, r25
 2c2:	59 f0       	breq	.+22     	; 0x2da
		{
			uint8_t index = (G_task_buf_end < (TASK_BUF_SIZE-1)) ? G_task_buf_end + 1 : 0; 
 2c4:	2f 31       	cpi	r18, 0x1F	; 31
 2c6:	08 f4       	brcc	.+2      	; 0x2ca
 2c8:	b2 c0       	rjmp	.+356    	; 0x42e
 2ca:	90 e0       	ldi	r25, 0x00	; 0
			if(index == G_task_buf_start)
 2cc:	80 91 04 01 	lds	r24, 0x0104
 2d0:	89 17       	cp	r24, r25
 2d2:	89 f0       	breq	.+34     	; 0x2f6 <__vector_9+0xd8>
				return TASK_BUF_ERR_FULL; 
			G_task_buf_end = index;
 2d4:	90 93 05 01 	sts	0x0105, r25
 2d8:	29 2f       	mov	r18, r25
		}
		G_task_buf[G_task_buf_end].job = a_job; //Post job
 2da:	82 2f       	mov	r24, r18
 2dc:	90 e0       	ldi	r25, 0x00	; 0
 2de:	fc 01       	movw	r30, r24
 2e0:	ee 0f       	add	r30, r30
 2e2:	ff 1f       	adc	r31, r31
 2e4:	e8 0f       	add	r30, r24
 2e6:	f9 1f       	adc	r31, r25
 2e8:	e7 5e       	subi	r30, 0xE7	; 231
 2ea:	fe 4f       	sbci	r31, 0xFE	; 254
 2ec:	87 ec       	ldi	r24, 0xC7	; 199
 2ee:	90 e0       	ldi	r25, 0x00	; 0
 2f0:	91 83       	std	Z+1, r25	; 0x01
 2f2:	80 83       	st	Z, r24
		G_task_buf[G_task_buf_end].arg = a_arg;
 2f4:	12 82       	std	Z+2, r1	; 0x02
    (void)__s;
}

static __inline__ void __iRestore(const  uint8_t *__s)
{
    SREG = *__s;
 2f6:	3f bf       	out	0x3f, r19	; 63


		return TASK_BUF_ERR_NON;
	ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
	{
		if(G_task_buf[G_task_buf_end].job)
		{
			uint8_t index = (G_task_buf_end < (TASK_BUF_SIZE-1)) ? G_task_buf_end + 1 : 0; 
 42e:	92 2f       	mov	r25, r18
 430:	9f 5f       	subi	r25, 0xFF	; 255
 432:	4c cf       	rjmp	.-360    	; 0x2cc


Видно, что много времени уходит на работу с массивом — более десяти тактов, что бы обратиться по адресу. Причём между считыванием значения адреса в текущей ячейке у нас адрес может измениться, а может и не измениться, в зависимости от считанного значения, по-этому адрес приходится рассчитывать два раза! Зато инкремент довольно быстрый. А если для обнуления вместо условия сделать наложение маски, то получится ещё быстрее. Код выполнятора, пожалуй, приводить не буду — там всё примерно тоже, с тем лишь отличием, что адрес вычислять требуется только один раз.
Что мы можем сделать, что бы облегчить контроллеру работу? Во-первых очевидно, что надо попытаться убрать необходимость второй раз вычислять адрес элемента в функци post_job. Решение здесь так же очевидно и лежит на поверхности: просто уберём лишнюю проверку, чуть-чуть изменив логику:

	{
		if(G_task_buf[G_task_buf_end].job)		//если место занято
			return TASK_BUF_ERR_FULL; 	//выход с ошибкой					G_task_buf[G_task_buf_end].job = a_job;	//ставим задачу по нужному индексу
		G_task_buf[G_task_buf_end].arg = a_arg;
		uint8_t index = (G_task_buf_end < (TASK_BUF_SIZE-1)) ? G_task_buf_end + 1 : 0; //вычисляем индекс следующего элемента
		G_task_buf_end = index; //сохраняем вычисленный индекс
	}

Теперь мы предполагаем, что индекс конца всегда указывает на первый свободный элемент (ну, конечно, если не произошло переполнение), а не на последний элемент в буфере. Соответственно, теперь мы автоматически делаем инкремент индекса конца всякий раз, когда добавляем новый элемент. Процессору легче, да и код читабельнее. Но мы изменили алгоритм работы буфера, а значит должен как-то поменяться алгоритм и второй функции. Для функции-выполнятора единственным существенным отличием будет ситуация с полностью заполненным буфером: если раньше индексы начала и конца были равны только при пустом буфере, то сейчас они совпадают ещё и когда буфер заполнен. Для приведённой выше функции это черевато тем, что если буфер заполнится, то перестанет происходить инкремент индекса начала и очередь задач переклинит. Дабы привести эту функцию в соответствие с новым алгоритмом достаточно поменять местами инкремент и условие.
Едем дальше. Второй шаг чуть менее очевиден — попробуем упростить компилатору работу и объясним ему, что надо делать с массивами:

		if((G_task_buf+G_task_buf_end)->job)		//если место занято
			return TASK_BUF_ERR_FULL; 		//выход с ошибкой
		(G_task_buf+G_task_buf_end)->job = a_job;	//ставим задачу по нужному индексу
		(G_task_buf+G_task_buf_end)->arg = a_arg;

Вот во что это собирается

		if((G_task_buf+G_task_buf_end)->job)
 248:	20 91 05 01 	lds	r18, 0x0105
 24c:	83 e0       	ldi	r24, 0x03	; 3
 24e:	28 9f       	mul	r18, r24
 250:	f0 01       	movw	r30, r0
 252:	11 24       	eor	r1, r1
 254:	e7 5e       	subi	r30, 0xE7	; 231
 256:	fe 4f       	sbci	r31, 0xFE	; 254
 258:	80 81       	ld	r24, Z
 25a:	91 81       	ldd	r25, Z+1	; 0x01
 25c:	89 2b       	or	r24, r25
 25e:	59 f4       	brne	.+22     	; 0x276
			return TASK_BUF_ERR_FULL;
		(G_task_buf+G_task_buf_end)->job = a_job;
 260:	84 ec       	ldi	r24, 0xC4	; 196
 262:	90 e0       	ldi	r25, 0x00	; 0
 264:	91 83       	std	Z+1, r25	; 0x01
 266:	80 83       	st	Z, r24
		(G_task_buf+G_task_buf_end)->arg = a_arg;
 268:	12 82       	std	Z+2, r1	; 0x02

Не сильно-то сократился код на самом деле — вместо пачки сложений появилось одно умножение и шаманство, необходимое для работы gcc после умножения (обнуление r1 — __zero_reg__). Если аккуратно посчитать получается экономия в два такта (надо отметить, при работе с большими массивами разница между этими двумя подходами заметна значительно сильнее [2]). А если вообще убрать вычисление смещения? Вместо индексов будем использовать указатели. Получается примерно так:

		if(G_task_buf_end->job)
 26e:	e0 91 00 01 	lds	r30, 0x0100
 272:	f0 91 01 01 	lds	r31, 0x0101
 276:	80 81       	ld	r24, Z
 278:	91 81       	ldd	r25, Z+1	; 0x01
 27a:	89 2b       	or	r24, r25
 27c:	79 f4       	brne	.+30     	; 0x29c
			return TASK_BUF_ERR_FULL; 
		G_task_buf_end->job = a_job;
 27e:	88 ed       	ldi	r24, 0xD8	; 216
 280:	90 e0       	ldi	r25, 0x00	; 0
 282:	91 83       	std	Z+1, r25	; 0x01
 284:	80 83       	st	Z, r24
		G_task_buf_end->arg = a_arg;
 286:	12 82       	std	Z+2, r1	; 0x02

Тут выигрыш уже семь тактов, что в целом неплохо, зато теперь мы оперируем двух-байтными адресами, что заметно сказывается на коде инкремента. Кроме того, здесь уже не соптимизируешь инкремент. В общем, в данной ситуации такую адресацию лучше не применять, хотя иметь ввиду её нужно.
Ещё, глядя на этот код, в голову приходит, что если выровнять структуру (сделать элемент четырёх-байтным), то можно будет упростить адресацию. На практике один фиг — компилятор не видит особой разницы между трех-байтными элементами массива и четырёх-байтными. Ну есть ещё пол вагона мелких оптимизаций, типа выкидывания лишних проверок и исключения ненужных накладных расходов, напрмер, атомик блок в функции-постановщие можно вынести наружу, тогда при её вызове из прерывания можно не делать лишний раз cli, а функцию выполнятор можно вообще целиком разместить в main.
В общем, мои размышления на тему, наверно, уже надоели, так что я закругляюсь и выкладываю итоговый результат изысканий. Далее приведён код минималистичной двухприоритетной очереди задач.

defs.h

typedef uint8_t job_arg_t;
typedef void (*job_t)(job_arg_t);
#define DECLARE_JOB(a_JOB_NAME)		void a_JOB_NAME(job_arg_t a_arg)

#define TASK_BUF_SIZE		32
#define TASK_BUF_SIZE_MASK	TASK_BUF_SIZE-1

#define TASK_BUF_ERR_NON	0
#define TASK_BUF_ERR_FULL	1

#define TASK_BUF_JOB_DONE	0
#define TASK_BUF_EMPTY		1

struct task_buf_el_t
{
	job_t		job;
	job_arg_t	arg;
};

inline uint8_t post_rt_job_(job_t a_job, job_arg_t a_arg);	//высокоприоритетные задачи
inline uint8_t post_bg_job_(job_t a_job, job_arg_t a_arg);	//низкоприоритетные задачи

//макросы для вызова post_job с отключением прерываний
#define POST_RT_JOB(a_JOB, a_ARG)		\
{						\
	ATOMIC_BLOCK(ATOMIC_RESTORESTATE)	\
	{					\
		post_rt_job_(a_JOB, a_ARG);	\
	}					\
}
#define POST_BG_JOB(a_JOB, a_ARG)		\
{						\
	ATOMIC_BLOCK(ATOMIC_RESTORESTATE)	\
	{					\
		post_bg_job_(a_JOB, a_ARG);	\
	}					\
}


main.c

struct task_buf_el_t			G_task_buf_rt[TASK_BUF_SIZE]; //buffer for real-time tasks
uint8_t					G_task_buf_rt_start = 0;
uint8_t					G_task_buf_rt_end = 0;
struct task_buf_el_t			G_task_buf_bg[TASK_BUF_SIZE]; //buffer for background tasks
uint8_t					G_task_buf_bg_start = 0;
uint8_t					G_task_buf_bg_end = 0;


inline uint8_t post_rt_job_(job_t a_job, job_arg_t a_arg)
{
	if(a_job == NULL)
		return TASK_BUF_ERR_NON;
	if((G_task_buf_rt+G_task_buf_rt_end)->job)
	{
		return TASK_BUF_ERR_FULL;
	}
	(G_task_buf_rt+G_task_buf_rt_end)->job = a_job; //Post job
	(G_task_buf_rt+G_task_buf_rt_end)->arg = a_arg;
	G_task_buf_rt_end ++;
	G_task_buf_rt_end &= TASK_BUF_SIZE_MASK;
	return TASK_BUF_ERR_NON;
}
inline uint8_t post_bg_job_(job_t a_job, job_arg_t a_arg)
{
	if(a_job == NULL)
		return TASK_BUF_ERR_NON;
	if((G_task_buf_bg+G_task_buf_bg_end)->job)
	{
		return TASK_BUF_ERR_FULL;
	}
	(G_task_buf_bg+G_task_buf_bg_end)->job = a_job; //Post job
	(G_task_buf_bg+G_task_buf_bg_end)->arg = a_arg;
	G_task_buf_bg_end ++;
	G_task_buf_bg_end &= TASK_BUF_SIZE_MASK;
	return TASK_BUF_ERR_NON;
}

int main(void)
{
	job_t job = NULL;
	job_arg_t arg = 0;
	memset(G_task_buf_rt, 0, sizeof(G_task_buf_rt));
	memset(G_task_buf_bg, 0, sizeof(G_task_buf_bg));
[...]
	while(1)
	{
		cli();	//выборка задачи - запрещаем прерывания
		if((G_task_buf_rt+G_task_buf_rt_start)->job)
		{
			job = (G_task_buf_rt+G_task_buf_rt_start)->job;
			arg = (G_task_buf_rt+G_task_buf_rt_start)->arg;
			(G_task_buf_rt+G_task_buf_rt_start)->job = NULL;
			G_task_buf_rt_start ++;
			G_task_buf_rt_start &= TASK_BUF_SIZE_MASK;
		} else if((G_task_buf_bg+G_task_buf_bg_start)->job) {
			job = (G_task_buf_bg+G_task_buf_bg_start)->job;
			arg = (G_task_buf_bg+G_task_buf_bg_start)->arg;
			(G_task_buf_bg+G_task_buf_bg_start)->job = NULL;
			G_task_buf_bg_start ++;
			G_task_buf_bg_start &= TASK_BUF_SIZE_MASK;
		} else {
			sei();
			continue; //если задач нет ни в одном буфере переходим к следующуй интерации
		}
		sei();	//перед выполнением задачи разрешить прерывания
		(*job)(arg);
	};
}

Пример использования

DECLARE_JOB(led_ctrl)
{
	if(a_arg == 1)
		LED_ON;
	else if(a_arg == 2)
		LED_INV;
	else
		LED_OFF;
}

ISR(TIMER2_COMPA_vect)
{
	post_bg_job_(&led_ctrl, 2);
}


Временные параметры диспетчера:
первоначальный вариант
  • постановка задачи в очередь — 52 такта
  • время между окончанием одной задачи и запуском следующей (включает время работы в главном цикле) — примерно 52 такта
  • работа выполнятора на холостом ходу — 37 тактов
  • время выемки задачи из очереди (в очереди один элемент) — 35 тактов
окночательный вариант
  • постановка задачи в очередь — 24 такта
  • время между окончанием одной задачи и запуском следующей (включает время работы в главном цикле) — примерно 34 тактов для rt задач и 52 — для bg
  • работа выполнятора на холостом ходу — 32 такта
  • время выемки задачи из очереди (в очереди один элемент) — 28 тактов для rt задач и 44 — для bg
Ссылки
  1. Кольцевой буфер на С++ для МК
  2. Заметка о работе с массивами

  • +3
  • 01 апреля 2011, 09:48
  • Alatar

Комментарии (5)

RSS свернуть / развернуть
Получается у вас есть две идентичные очереди, но задания из первой выполняются в первую очередь.
Я думаю, если упаковать эти очереди в структуру и в функцию post_job передавать указатель на нужную очередь и соответственно избавившись от дублирующегося кода в post_bg_job_ и post_rt_job_ размер кода этого примера сократится по крайней мере на треть и без потери производительности. Сейчас попробую отпишусь.
0
Похоже я не совсем понимаю Вашу идею. Что ласт упаковывание очередей в структуру и как именно из надо упаковать? И потом, указатель на очередь мы передадим, а откуда мы будем знать какие индексы использовать? Выбирать по значению адреса — накладно. У меня стояла задача минимизации по скорости, а не по коду.
Если смущает дублирование кода в исходнике, могу написать макрос, который будет создавать нужные функции.
0
А, понял, индексы хранятся в структуре. Это да, надо пробовать и смотреть, что скажет компилятор… По идее немного увеличатся накладные расходы на загрузку индексов из памяти в регистры, потому что надо будет вычислять их адреса.
0
Вот, что я имел ввиду:
typedef struct
{
	struct task_buf_el_t G_task_buf[TASK_BUF_SIZE];
	uint8_t G_task_buf_start;
	uint8_t G_task_buf_end;
}job_queue_t;


job_queue_t bg_queue;
job_queue_t rt_queue;


static uint8_t post_job_(job_queue_t *queue, job_t a_job, job_arg_t a_arg)
{
        if(a_job == NULL)
                return TASK_BUF_ERR_NON;
        if((queue->G_task_buf+queue->G_task_buf_end)->job)
        {
                return TASK_BUF_ERR_FULL;
        }
        (queue->G_task_buf+queue->G_task_buf_end)->job = a_job; //Post job
        (queue->G_task_buf+queue->G_task_buf_end)->arg = a_arg;
        queue->G_task_buf_end ++;
       	queue->G_task_buf_end &= TASK_BUF_SIZE_MASK;
        return TASK_BUF_ERR_NON;
}

Размер кода получается действительно чуть-чуть побольше 380 против 364 байт для полного примера. Получается это потому, что компилятор вычисляет индекс в массиве с помощью:
add     r30, r30
adc     r31, r31
add     r30, r24
adc     r31, r25

Потому, что не знает влезет ли результат в 8*8 бит умножение. В остальном код получается идентичен.
0
Ну да, я уже понял, что Вы это имели ввиду. В принципе от этих накладных расходов тоже можно избавиться, если адреса индексов тоже передавать в функцию.
Получится что-то типа


#define POST_RT_JOB(a_JOB, a_ARG)               \
{                                               \
        ATOMIC_BLOCK(ATOMIC_RESTORESTATE)       \
        {                                       \
                post_job_(rt_queue.G_task_buf, &rt_queue.G_task_buf_end, a_JOB, a_ARG);     \
        }                                       \
}
#define POST_RT_JOB_(a_JOB, a_ARG)               \
{                                               \
        post_job_(rt_queue.G_task_buf, &rt_queue.G_task_buf_end, a_JOB, a_ARG);     \
}
#define POST_BG_JOB(a_JOB, a_ARG)               \
{                                               \
        ATOMIC_BLOCK(ATOMIC_RESTORESTATE)       \
        {                                       \
                post_job_(bg_queue.G_task_buf, &bg_queue.G_task_buf_end, a_JOB, a_ARG);     \
        }                                       \
}
#define POST_BG_JOB_(a_JOB, a_ARG)               \
{                                               \
        post_job_(bg_queue.G_task_buf, &bg_queue.G_task_buf_end, a_JOB, a_ARG);     \
}

static uint8_t post_job_(task_buf_el_t *a_task_buf, uint8_t *a_task_buf_end, job_t a_job, job_arg_t a_arg)
{
        if(a_job == NULL)
                return TASK_BUF_ERR_NON;
        if((a_task_buf+*a_task_buf_end)->job)
        {
                return TASK_BUF_ERR_FULL;
        }
        (a_task_buf+*a_task_buf_end)->job = a_job; //Post job
        (a_task_buf+*a_task_buf_end)->arg = a_arg;
        *a_task_buf_end ++;
        *a_task_buf_end &= TASK_BUF_SIZE_MASK;
        return TASK_BUF_ERR_NON;
}


(пишу не вскидку, могут быть ошибки)
Правда тогда и группировать уже не надо =).
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.