Tics Realtime -----


 
Home Services Products Tutorials Contact Us
- - - - -

Accessing and Communicating Through Global Data in Multi-tasking Systems

One of the first questions that a new real-time multi-tasking programmer may ask is: what are all these elaborate schemes for inter-task communication and synchronization for? Why can't tasks communicate through global data? The answer is that under certain conditions tasks can communicate through global data, and furthermore, in some situations, this technique is preferable.

Threads vs. Processes

The techniques outlined here apply to threads only, since processes cannot share global data. For more information on threads and processes see the related article.

Communicating with Global Data

There are no mechanisms necessary for tasks to communicate through global data; tasks share global data just like C functions do. Consider taskA and taskB. taskA reads analog data, scales it, and stores the scaled value in a global variable. taskB displays the scaled data graphically every 500 milliseconds. taskA and taskB might be coded as follows.
void taskA(void)
{
	int data;

	while (TRUE) {
		data = readAnalogData();
		GlobalData = scaleAnalogData(data);
		yield();
	}
}

void taskB(void)
{
	while (TRUE) {
		pause(500L);
		display(GlobalData);
	}
}

The solution given above is clean and easy to understand; there is no need for formal inter-task communication through message queues, mail, etc.

Preemption Can Cause Problems

The example above assumes that preemption cannot occur. Preemption can occur when tasks run at different priorities or time-slicing is enabled. If preemption is allowed, then communicating though global data can cause problems. Assume that the example above is running on an 8 bit microprocessor, on which 16 bit integers are composed of two bytes. To write a 16 bit integer, two bus cycles are required: one to write the first byte, and a second cycle to write the second byte. Consider what happens when taskB is running at a higher priority and the 500 ms timer expires just after taskA writes out the first byte of GlobalData. At this point the value of GlobalData is undefined; the first byte contains the new value, while the second byte still has the old value. Continuing with the scenario, taskB now preempts taskA and displays an invalid value.

Solutions to Preemption Problems

There are various ways to avoid the preemption problem for the example above. The following solutions are listed by preference.
  1. Avoid preemption by running all tasks cooperatively.
  2. taskA sends taskB a message when new data is available.
  3. Use a server task to serialize access to GlobalData.
  4. Disable interrupts while accessing GlobalData.
  5. Use a semaphore to serialize access to GlobalData.
Solution 1 requires no explanation and is the preferred approach because of its simplicity.

Solution 2 requires taskA to send taskB a message whenever new data is to be displayed. taskA and taskB would be modified as follows:

void taskA(void)
{
	int data;
	typeMsg * msg;

	while (TRUE) {
		data = readAnalogData();
		msg = makeMsg(TcbB, DATA);
		msg->iData = scaleAnalogData(data);
		sendMsg(msg);
		pause(500L);
	}
}

void taskB(void)
{
	typeMsg * msg;

	while (TRUE) {
		msg = waitMsg(DATA);
		display(msg->iData);
		freeMsg(msg);
	}
}
Note that the data corruption problem cannot occur, since the data value is encapsulated in the message. The fact that taskB is at a higher priority simply means that when taskA sends taskB the message, taskA will be preempted so that taskB can run and receive the message; no data corruption can occur. This is the preferred solution for preemptive systems.

Solution 2 is acceptable as long as taskB is the only task that needs the latest data value. However, if multiple tasks need access to GlobalData, then we are back to the original concept of using a global integer. However, access to GlobalData must be managed in such a way that data corruption is avoided. This can be accomplished by solutions 3, 4, and 5.

Server Task Solution

A server is a separate task whose job is to serialize access to a resource. In our example, a server task would be implemented to accept READ and WRITE messages. The server task would be implemented as follows.
void globalDataServer(void)
{
	typeMsg * msg;
	static int globalData;

	while (TRUE) {
		msg = waitMsg(ANY_MSG);

		switch (msg->msgNum) {
		case READ:
			msg->iData = globalData;
			reply(msg, DONE);
			break;
		case WRITE:
			globalData = msg->iData;
			freeMsg(msg);
			break;
		}
	}
}
A task desiring to change globalData would code the following:
	msg = makeMsg(TcbGlobalDataServer, WRITE);
	msg->iData = newValue;
	sendMsg(msg);
A task desiring to read GlobalData would code the following:
	msg = makeMsg(TcbGlobalDataServer, READ);
	sendMsg(msg);
	msg = waitMsg(DONE);
	currentGlobalDataValue = msg->iData;
	freeMsg(msg);
Note that since messages are queued, multiple READ/WRITE requests are simply queued in the server task's message queue. Note also that the variable globalData is a static encapsulated in the server task itself, and therefore, the server has sole access to it.

Disabling Interrupts

This is a harsh, but effective solution, the rationale being that all preemptions outside the current task's control occur by way of an interrupt. The solution then is to disallow interrupts during the critical period.
	disableInterrupts();
	GlobalData = newValue;
	enableInterrupts();
This approach can be error prone, however. Consider functionA that has disabled interrupts, then calls functionB which executes the code above. The problem is that when control is returned back to functionA, interrupts are enabled, which can cause problems for functionA, since functionA presumes that interrupts are still disabled. There are various solutions - for example, embed intelligence into the enable/disable functions so that the problem cannot occur.

Semaphore

A semaphore with an initial count of 1 can be created to control access as follows.
	acquireSem(Global);
	GlobalData = newValue;
	releaseSem(Global);
Although clean in appearance, this approach is generally undesirable and under certain conditions can cause problems such as priority inversion. However, with care and proper design, this approach can be useful also.

Comments

The preferred approach for safely accessing global data is to run tasks cooperatively. This approach does not preclude preemption; it simply means that all tasks that access global data must run at the same priority with time-slicing disabled (i.e. cooperatively). Higher priority tasks can preempt tasks within the cooperative group as long as the higher priority tasks do not access global data.

The techniques outlined above are also useful for accessing non-reentrant code and libraries.


We welcome comments. Let us know what subjects you would like written up. Send comments to Mike@TicsRealtime.com

Copyright © 2000, Tics Realtime