Fiber.cs
Go to the documentation of this file.
1 /*
2 
3 Author: Aaron Oneal, http://aarononeal.info
4 
5 Copyright (c) 2012 Spicy Pixel, http://spicypixel.com
6 
7 Permission is hereby granted, free of charge, to any person obtaining
8 a copy of this software and associated documentation files (the
9 "Software"), to deal in the Software without restriction, including
10 without limitation the rights to use, copy, modify, merge, publish,
11 distribute, sublicense, and/or sell copies of the Software, and to
12 permit persons to whom the Software is furnished to do so, subject to
13 the following conditions:
14 
15 The above copyright notice and this permission notice shall be
16 included in all copies or substantial portions of the Software.
17 
18 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
19 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20 MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
22 LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
23 OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
24 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25 
26 */
27 using System;
28 using System.Collections;
30 using System.Linq;
31 using System.Threading;
32 
33 namespace SpicyPixel.Threading
34 {
67  public partial class Fiber
68  {
72  [ThreadStatic]
73  private static Fiber currentFiber;
74 
75  private static FiberFactory fiberFactory = new FiberFactory();
76 
80  private static int nextId = 0;
81 
82  private IEnumerator coroutine;
83  private Stack<IEnumerator> nestedCoroutines;
84 
85  private Action action;
86  private Action<object> actionObject;
87  private object objectState;
88 
89  private Func<FiberInstruction> func;
90  private Func<object, FiberInstruction> funcObject;
91 
92  private FiberScheduler scheduler;
93  private int status = (int)FiberStatus.Created;
94  // must be int to work with Interlocked on Mono
95  private IDictionary<string, object> properties;
96 
97  internal CancellationToken cancelToken;
98  internal Fiber antecedent;
99  internal Queue<FiberContinuation> continuations;
100 
107  public static Fiber CurrentFiber {
108  get {
109  return currentFiber;
110  }
111  private set {
112  currentFiber = value;
113  }
114  }
115 
120  public static FiberFactory Factory {
121  get {
122  return fiberFactory;
123  }
124  }
125 
126  static int CheckTimeout(TimeSpan timeout)
127  {
128  try {
129  return checked ((int)timeout.TotalMilliseconds);
130  } catch (System.OverflowException) {
131  throw new ArgumentOutOfRangeException("timeout");
132  }
133  }
134 
150  public IDictionary<string, object> Properties {
151  get {
152  if (properties == null)
153  properties = new Dictionary<string, object>();
154 
155  return properties;
156  }
157  }
158 
165  internal FiberScheduler Scheduler {
166  get { return scheduler; }
167  set { scheduler = value; }
168  }
169 
176  public string Name { get; set; }
177 
182  public object ResultAsObject { get; internal set; }
183 
188  public bool IsCanceled {
189  get {
190  return (FiberStatus)status == FiberStatus.Canceled;
191  }
192  }
193 
204  public bool IsCompleted {
205  get {
206  return (FiberStatus)status >= FiberStatus.RanToCompletion;
207  }
208  }
209 
217  public bool IsFaulted {
218  get {
219  return (FiberStatus)status == FiberStatus.Faulted;
220  }
221  }
222 
227  public Exception Exception {
228  get;
229  private set;
230  }
231 
238  public FiberStatus Status {
239  get { return (FiberStatus)status; }
240  set { status = (int)value; }
241  }
242 
251  public Fiber Antecedent {
252  get { return antecedent; }
253  }
254 
268  get { return cancelToken; }
269  }
270 
274  private void FinishCompletion()
275  {
276  if (continuations == null)
277  return;
278 
279  while (true) {
280  if (continuations.Count == 0)
281  return;
282 
283  var continuation = continuations.Dequeue();
284  continuation.Execute();
285  }
286  }
287 
292  internal void CancelContinuation()
293  {
294  status = (int)FiberStatus.Canceled;
295  FinishCompletion();
296  }
297 
304  public int Id { get; private set; }
305 
312  public Fiber(IEnumerator coroutine) : this(coroutine, CancellationToken.None)
313  {
314  }
315 
323  public Fiber(IEnumerator coroutine, CancellationToken cancellationToken)
324  {
325  Id = nextId++;
326  this.coroutine = coroutine;
327  this.cancelToken = cancellationToken;
328  }
329 
336  public Fiber(Action action) : this(action, CancellationToken.None)
337  {
338  }
339 
347  public Fiber(Action action, CancellationToken cancellationToken)
348  {
349  Id = nextId++;
350  this.action = action;
351  this.cancelToken = cancellationToken;
352  }
353 
363  public Fiber(Action<object> action, object state) : this(action, state, CancellationToken.None)
364  {
365  }
366 
377  public Fiber(Action<object> action, object state, CancellationToken cancellationToken)
378  {
379  Id = nextId++;
380  this.actionObject = action;
381  this.objectState = state;
382  this.cancelToken = cancellationToken;
383  }
384 
391  public Fiber(Func<FiberInstruction> func) : this(func, CancellationToken.None)
392  {
393  }
394 
402  public Fiber(Func<FiberInstruction> func, CancellationToken cancellationToken)
403  {
404  Id = nextId++;
405  this.func = func;
406  this.cancelToken = cancellationToken;
407  }
408 
418  public Fiber(Func<object, FiberInstruction> func, object state) : this(func, state, CancellationToken.None)
419  {
420  }
421 
432  public Fiber(Func<object, FiberInstruction> func, object state, CancellationToken cancellationToken)
433  {
434  Id = nextId++;
435  this.funcObject = func;
436  this.objectState = state;
437  this.cancelToken = cancellationToken;
438  }
439 
443  public void Start()
444  {
445  Start(FiberScheduler.Current);
446  }
447 
458  public void Start(FiberScheduler scheduler)
459  {
460  // It would be unusual to attempt to start a Fiber more than once,
461  // but to be safe and to support calling from any thread
462  // use Interlocked with boxing on the enum.
463 
464  var originalState = (FiberStatus)Interlocked.CompareExchange(ref status, (int)FiberStatus.WaitingToRun, (int)FiberStatus.Created);
465  if (originalState != FiberStatus.Created) {
466  originalState = (FiberStatus)Interlocked.CompareExchange(ref status, (int)FiberStatus.WaitingToRun, (int)FiberStatus.WaitingForActivation);
467  if (originalState != FiberStatus.WaitingForActivation) {
468  throw new InvalidOperationException("A fiber cannot be started again once it has begun running or has completed.");
469  }
470  }
471 
472  this.scheduler = scheduler;
473  ((IFiberScheduler)this.scheduler).QueueFiber(this);
474  }
475 
482  internal FiberInstruction Execute()
483  {
484  // Sanity check the scheduler. Since this is a scheduler
485  // only issue, this test happens first before execution
486  // and before allowing an exception handler to take over.
487  if (IsCompleted)
488  throw new InvalidOperationException("An attempt was made to execute a completed Fiber. This indicates a logic error in the scheduler.");
489 
490  // Setup thread globals with this scheduler as the owner.
491  // The sync context is also setup when the scheduler changes.
492  // Setting the sync context isn't a simple assign in .NET
493  // so don't do this until needed.
494  //
495  // Doing this setup in Execute() frees derived schedulers from the
496  // burden of setting up the sync context or scheduler. It also allows the
497  // current scheduler to change on demand when there is more than one
498  // scheduler running per thread.
499  //
500  // For example, each MonoBehaviour in Unity is assigned its own
501  // scheduler since the behavior determines the lifetime of tasks.
502  // The active scheduler then can vary depending on which behavior is
503  // currently executing tasks. Unity will decide outside of this framework
504  // which behaviour to execute in which order and therefore which
505  // scheduler is active. The active scheduler in this case can
506  // only be determined at the time of fiber execution.
507  //
508  // Unlike CurrentFiber below, this does not need to be stacked
509  // once set because the scheduler will never change during fiber
510  // execution. Only something outside of the scheduler would
511  // change the scheduler.
512  if (FiberScheduler.Current != scheduler)
513  FiberScheduler.SetCurrentScheduler(scheduler, true);
514 
515  // Push the current fiber onto the stack and pop it in finally.
516  // This must be stacked because the fiber may spawn another
517  // fiber which the scheduler may choose to inline which would result
518  // in a new fiber temporarily taking its place as current.
519  var lastFiber = Fiber.CurrentFiber;
520  Fiber.CurrentFiber = this;
521 
522  try {
523  // The loop will execute until hitting a valid instruction.
524  // Specifically, nested coroutines need to execute this until
525  // a yield instruction is hit.
526  while (true) {
527  object result = null;
528 
529  // Execute the coroutine or action
530  if (nestedCoroutines != null && nestedCoroutines.Count > 0) {
531  var nestedCoroutine = nestedCoroutines.Peek();
532  if (nestedCoroutine.MoveNext()) {
533  result = nestedCoroutine.Current;
534 
535  // A stop instruction in a nested coroutine
536  // means stop that execution, not the parent
537  if (result is StopInstruction) {
538  nestedCoroutines.Pop();
539  continue;
540  }
541  } else {
542  // Nested routine finished
543  nestedCoroutines.Pop();
544  continue;
545  }
546  } else if (coroutine != null) {
547  // Execute coroutine
548  if (coroutine.MoveNext()) {
549  // Get result of execution
550  result = coroutine.Current;
551 
552  // If the coroutine returned a stop directly
553  // the fiber still needs to process it
554  if (result is StopInstruction)
555  Stop(FiberStatus.RanToCompletion);
556  } else {
557  // Coroutine finished executing
558  result = Stop(FiberStatus.RanToCompletion);
559  }
560  } else if (action != null) {
561  // Execute action
562  action();
563 
564  // Action finished executing
565  result = Stop(FiberStatus.RanToCompletion);
566  } else if (actionObject != null) {
567  // Execute action
568  actionObject(objectState);
569 
570  // Action finished executing
571  result = Stop(FiberStatus.RanToCompletion);
572  } else if (func != null) {
573  result = func();
574  func = null;
575 
576  if (result is StopInstruction)
577  Stop(FiberStatus.RanToCompletion);
578  } else if (funcObject != null) {
579  result = funcObject(objectState);
580  funcObject = null;
581 
582  if (result is StopInstruction)
583  Stop(FiberStatus.RanToCompletion);
584  } else {
585  // Func execution nulls out the function
586  // so the scheduler will return to here
587  // when complete and then stop.
588  result = Stop(FiberStatus.RanToCompletion);
589  }
590 
591  // Treat null as a special case
592  if (result == null)
594 
595  // Return instructions or throw if invalid
596  var instruction = result as FiberInstruction;
597  if (instruction == null) {
598  // If the result was an enumerator there is a nested coroutine to execute
599  if (result is IEnumerator) {
600  // Lazy create
601  if (nestedCoroutines == null)
602  nestedCoroutines = new Stack<IEnumerator>();
603 
604  // Push the nested coroutine onto the stack
605  nestedCoroutines.Push(result as IEnumerator);
606 
607  // For performance we execute nested coroutines until hitting
608  // an actual instruction at the deepest nest level.
609  result = null;
610  continue;
611  } else if (result is Fiber) {
612  // Convert fibers into yield instructions
613  instruction = new YieldUntilComplete(result as Fiber);
614  } else {
615  // Pass through other values
616  return new ObjectInstruction(result);
617  }
618  }
619 
620  if (instruction is FiberResult) {
621  ResultAsObject = ((FiberResult)instruction).Result;
622  result = Stop(FiberStatus.RanToCompletion);
623  }
624 
625  // Verify same scheduler
626  if (instruction is YieldUntilComplete && ((YieldUntilComplete)instruction).Fiber.Scheduler != FiberScheduler.Current)
627  throw new InvalidOperationException("Currently only fibers belonging to the same scheduler may be yielded to. FiberScheduler.Current = "
628  + (FiberScheduler.Current == null ? "null" : FiberScheduler.Current.ToString())
629  + ", Fiber.Scheduler = " + (((YieldUntilComplete)instruction).Fiber.Scheduler == null ? "null" : ((YieldUntilComplete)instruction).Fiber.Scheduler.ToString()));
630 
631  var yieldToFiberInstruction = instruction as YieldToFiber;
632  if (yieldToFiberInstruction != null) {
633  // Start fibers yielded to that aren't running yet
634  Interlocked.CompareExchange(ref yieldToFiberInstruction.Fiber.status, (int)FiberStatus.WaitingToRun, (int)FiberStatus.Created);
635  var originalState = (FiberStatus)Interlocked.CompareExchange(ref yieldToFiberInstruction.Fiber.status, (int)FiberStatus.Running, (int)FiberStatus.WaitingToRun);
636  if (originalState == FiberStatus.WaitingToRun)
637  yieldToFiberInstruction.Fiber.scheduler = scheduler;
638 
639  // Can't switch to completed fibers
640  if (yieldToFiberInstruction.Fiber.IsCompleted)
641  throw new InvalidOperationException("An attempt was made to yield to a completed fiber.");
642 
643  // Verify scheduler
644  if (yieldToFiberInstruction.Fiber.Scheduler != FiberScheduler.Current)
645  throw new InvalidOperationException("Currently only fibers belonging to the same scheduler may be yielded to. FiberScheduler.Current = "
646  + (FiberScheduler.Current == null ? "null" : FiberScheduler.Current.ToString())
647  + ", Fiber.Scheduler = " + (yieldToFiberInstruction.Fiber.Scheduler == null ? "null" : yieldToFiberInstruction.Fiber.Scheduler.ToString()));
648  }
649 
650  return instruction;
651  }
652  } catch (System.Threading.OperationCanceledException cancelException) {
653  // Handle as proper cancellation only if the token matches.
654  // Otherwise treat it as a fault.
655  if (cancelException.CancellationToken == cancelToken) {
656  this.Exception = null;
657  return Stop(FiberStatus.Canceled);
658  } else {
659  this.Exception = cancelException;
660  return Stop(FiberStatus.Faulted);
661  }
662  } catch (Exception fiberException) {
663  this.Exception = fiberException;
664  return Stop(FiberStatus.Faulted);
665  } finally {
666  // Pop the current fiber
667  Fiber.CurrentFiber = lastFiber;
668  }
669  }
670 
671  private StopInstruction Stop(FiberStatus finalStatus)
672  {
673  // Interlocked isn't necessary because we don't need
674  // the initial value (stops are final). They are also
675  // only called by the scheduler thread during Execute().
676  status = (int)finalStatus;
677  FinishCompletion();
678  return FiberInstruction.Stop;
679  }
680  }
681 }
Schedules fibers for execution.
Fiber(Action< object > action, object state, CancellationToken cancellationToken)
Initializes a new instance of the SpicyPixel.Threading.Fiber class.
Definition: Fiber.cs:377
Yield execution until the watched fiber on the same scheduler is complete.
Fiber(Action< object > action, object state)
Initializes a new instance of the SpicyPixel.Threading.Fiber class.
Definition: Fiber.cs:363
Wraps an object as an instruction
A Fiber is a lightweight means of scheduling work that enables multiple units of processing to execut...
An instruction to stop fiber execution and set a result on the fiber.
Definition: FiberResult.cs:8
Represents a fiber instruction to be processed by a FiberScheduler.
A Fiber Factory for creating fibers with the same options.
Definition: FiberFactory.cs:9
Fiber(Action action)
Initializes a new instance of the SpicyPixel.Threading.Fiber class.
Definition: Fiber.cs:336
Fiber(IEnumerator coroutine, CancellationToken cancellationToken)
Initializes a new instance of the SpicyPixel.Threading.Fiber class.
Definition: Fiber.cs:323
void Start()
Start executing the fiber using the default scheduler on the thread.
Definition: Fiber.cs:443
Fiber(Func< FiberInstruction > func)
Initializes a new instance of the SpicyPixel.Threading.Fiber class.
Definition: Fiber.cs:391
Yield execution to a specific fiber belonging to the same scheduler as the current fiber...
Definition: YieldToFiber.cs:35
static YieldToAnyFiber YieldToAnyFiber
An instruction to cause the current fiber to yield to any ready fiber.
static FiberScheduler Current
Gets the default fiber scheduler for the thread.
Fiber(Func< object, FiberInstruction > func, object state)
Initializes a new instance of the SpicyPixel.Threading.Fiber class.
Definition: Fiber.cs:418
Fiber(Func< object, FiberInstruction > func, object state, CancellationToken cancellationToken)
Initializes a new instance of the SpicyPixel.Threading.Fiber class.
Definition: Fiber.cs:432
Fiber(Func< FiberInstruction > func, CancellationToken cancellationToken)
Initializes a new instance of the SpicyPixel.Threading.Fiber class.
Definition: Fiber.cs:402
An instruction to terminate execution of the current fiber.
Fiber(IEnumerator coroutine)
Initializes a new instance of the SpicyPixel.Threading.Fiber class.
Definition: Fiber.cs:312
void Start(FiberScheduler scheduler)
Start executing the fiber using the specified scheduler.
Definition: Fiber.cs:458
Fiber(Action action, CancellationToken cancellationToken)
Initializes a new instance of the SpicyPixel.Threading.Fiber class.
Definition: Fiber.cs:347
static StopInstruction Stop
An instruction to terminate execution of the current fiber.
Fiber Fiber
Gets the fiber to yield to.
Definition: YieldToFiber.cs:43
FiberStatus
Represents the current state of a fiber.
Definition: FiberStatus.cs:34
When no continuation options are specified, default behavior should be used to execute a continuation...
static Fiber CurrentFiber
Gets the currently executing fiber on this thread.
Definition: Fiber.cs:107