views:

133

answers:

2

Any creative ideas to avoid deadlocks on a yield or sleep with cooperative/non-preemptive multitasking without doing an O/S Thread.Sleep(10)? Typically the yield or sleep call will call back into the scheduler to run other tasks. But this can sometime produce deadlocks.

Some background:

This application has enormous need for speed and, so far, it's extremely fast as compared to other systems in the same industry. One of the speed techniques is cooperative/non-preemptive threading rather then the cost of a context switch from O/S threads.

The high level design a priority manager which calls out to tasks depending on priority and processing time. Each task does one "iteration" of work and returns to wait its turn again in the priority queue.

The tricky thing with non-preemptive threading is what to do when you want to a particular task to stop in the middle of work and wait for some other event from a different task before continuing.

In this case, we have 3 tasks, A B and C where A is a controller that must synchronize the activity of B and C. First, A starts both B and C. Then B yields so C gets invoked. When C yields, A sees they are both inactive, decides it's time for B to run but not time for C yet. Well B is now stuck in a yield that has called C, so it can never run.

Sincerely, Wayne

+1  A: 

I think that probably the cleanest way to handle this is to separate yielding (a thread deciding it has processed enough for a while) from blocking (waiting for a specific event). That makes it relatively easy to give time to threads that have yielded, but avoid a deadlock from trying to run a thread that's blocked. Generally, you want to do a topological sort on what thread is blocking on what other thread, so you can give time to the thread(s) others are waiting on. This should give a DAG -- any cycle in the graph indicates a deadlock.

Jerry Coffin
Okay, good points. More info: We do that already. What you call "yielding" we handle by a task simply does a return so that the stack unwinds back to the scheduler. That means all the Yields and Sleeps in this question discussion are blocking. Like this: while( !xevent) Yield();We handle the DAG already and that solves all other cases. But the one in the question above, if you study it more closely isn't solved that way.There is zero dependency between B and C. It's simply they must be synchronized. So they must each pause and wait for notification which will run next.
Wayne
The fundamental problem here is that each task ties up the stack while waiting. One solution which will be messy is to split of B and C up into 2 tasks each.So B becomes B1 and B2. That way B1 and B2 can run by switching state back and forth to each other. If a block is needed that will be at the end of B1 so it can exit the task after disabling the B2 so B2 won't start running until reactivated by A.That way the stack gets released in both cases.It's ugly to split of the lovely code into pieces like that. but if there's no better ideas.
Wayne
Maybe I'm just short of sleep, but I'm still not quite following the problem here. Which thread has the `while (!xevent) Yield();`, and which are you expecting to set `xevent`? Changing the rules a bit, there is a rather different possibility: use cooperative multitasking to reduce overhead, BUT use a watchdog timer that preempts *only* in case of a deadlock.
Jerry Coffin
That's a clever idea. The only problem is that if it does preempt during the deadlock, the task that needs to run still has another thread blocking inside it. To continue it the preempt thread would need to reenter that task which would make that task require thread-safe code. The beauty of this system so far as all the tasks can be written non-thread safe (simplicity!) since they a guaranteed to never be reentered by another thread. Of course, they might get a different thread at any execution but only one at a time.
Wayne
We came up with an idea to try. We're thinking to marking these "dangerous" tasks manually with a property "IsDeadlockable". Then if a thread is in a yield or sleep in one task, it is disallowed from entering another task that is marked "IsDeadlockable".That will solve this specific situation, I think. Unfortunately, it in not an automated discovery. We can't see any way to automatically foresee and prevent deadlocks like this.That's because the scheduler only knows it will deadlock after it has deadlocks and then it's impossible to unwind the stack without damaging the program logic.
Wayne
OUCH! That will only work if there are 2 tasks involved that are deadlockable and 2 O/S threads because of 2 CPU cores. When you increase the number of tasks, after 2 of them go into a yield, none of the others can get any processing time.What will have to happen is that the tasks that need to yield will have to save their current state as "paused" somehow and completely return to the scheduler. Then if that task needs to be continued, it can "continue" where it left off.What other way can there be until C# supports continuations with save of the stack, if ever.
Wayne
Detecting the deadlock ahead of time has an eerie sound to it -- the phrase "halting problem" springs to mind...
Jerry Coffin
A: 

Well, realized the ideal solution to this would be if the C# language supports true "continuations" to unwide the stack and continue where left off later.

In the absence of that we are making our own makeshift replacement by allow the tasks in this situation to set an "isInterrupted" flag to true and return--thereby unwinding stack.

Then when the scheduler wants to schedule processing time to that task again, it will see the isInterrupted and skip the processing that was already done to jump right to the interrupt location using simple if statement.

Sincerely, Wayne

Wayne