@@ -388,3 +388,234 @@ The following tools may be used for performance and memory profiling of your man
388
388
- Visual Studio.
389
389
390
390
Profiling managed and unmanaged code at once is possible with both JetBrains tools and Visual Studio, but limited to Windows.
391
+
392
+ Using ``async ``/``await ``
393
+ -------------------------
394
+
395
+ You might face a scenario where you must ``await `` a method call.
396
+ You will notice that when you use ``await ``, you are required to mark the method you use it in as ``async ``,
397
+ and change the return type to an awaitable type, such as ``Task `` or ``Task<T> ``.
398
+ Consequently, you must call your now ``async `` method using ``await ``,
399
+ which propagates the problem all the way up the call chain.
400
+ This is why many people compare ``async ``/``await `` to a "zombie virus",
401
+ because it tends to spread once introduced.
402
+
403
+ In Godot, the conclusion to this spread is the entry point methods of a node, such as ``_Ready() `` or ``_Process() ``.
404
+ You will notice that the return types of these methods are ``void `` rather than ``Task ``.
405
+ It is considered conventional wisdom in C# to avoid ``async void `` at all times, with the exception of event handlers.
406
+ The problem is that it is impossible to change the signatures of these methods since they are defined by the classes they inherit.
407
+
408
+ There are a couple options to address this problem, but each option comes with its own caveats and considerations.
409
+ To compare these options, we will work with the following script:
410
+
411
+ .. code-block :: csharp
412
+
413
+ using Godot ;
414
+ using System ;
415
+ using System .Threading .Tasks ;
416
+
417
+ public partial class AsyncTestNode : Node
418
+ {
419
+ private int _taskCount = 0 ;
420
+ private DateTime start ;
421
+ public override void _Ready ()
422
+ {
423
+ start = DateTime .Now ;
424
+ }
425
+
426
+ public override void _Process (double delta )
427
+ {
428
+
429
+ }
430
+
431
+ // Prints the amount of time since _Ready started, the current thread, and the name of the calling method
432
+ // It prints this once when DoStuffAsync is first called, then once again after `duration` in seconds
433
+ private async Task DoStuffAsync (double duration , string methodName )
434
+ {
435
+ var taskId = ++ _taskCount ;
436
+ PrintCurrentThread ($" Task {taskId } started from {methodName }" );
437
+ await Task .Delay (TimeSpan .FromSeconds (duration ));
438
+ PrintCurrentThread ($" Task {taskId } completed" );
439
+ }
440
+
441
+ private void PrintCurrentThread (string info )
442
+ {
443
+ var timeStamp = DateTime .Now - start ;
444
+ GD .PrintT (timeStamp .ToString (@" mm\:ss\.ff" ), $" Thread: {System .Environment .CurrentManagedThreadId }" , info );
445
+ }
446
+ }
447
+
448
+ The first option is to start the task through the Task factory.
449
+
450
+ .. code-block :: csharp
451
+
452
+ // This function can be put in a global static class for convenience
453
+ public static Task StartTaskFromFactory (Func < Task > newTask )
454
+ {
455
+ return Task .Factory .StartNew (newTask ,
456
+ CancellationToken .None ,
457
+ TaskCreationOptions .None ,
458
+ TaskScheduler .FromCurrentSynchronizationContext ());
459
+ }
460
+
461
+ public override void _Ready ()
462
+ {
463
+ start = DateTime .Now ;
464
+
465
+ StartTaskFromFactory (async () => await DoStuffAsync (. 5 , nameof (_Ready )));
466
+ }
467
+
468
+ public override void _Process (double delta )
469
+ {
470
+ if (_taskCount < 3 )
471
+ StartTaskFromFactory (async () => await DoStuffAsync (. 5 , nameof (_Process )));
472
+ }
473
+
474
+ The second option is to mark the entry point method as async anyway.
475
+
476
+ .. code-block :: csharp
477
+
478
+ public override async void _Ready ()
479
+ {
480
+ start = DateTime .Now ;
481
+ await DoStuffAsync (. 5 , nameof (_Ready ));
482
+ }
483
+
484
+
485
+ public override async void _Process (double delta )
486
+ {
487
+ if (_taskCount < 3 )
488
+ await DoStuffAsync (. 5 , nameof (_Process ));
489
+ }
490
+
491
+ Both the manual task starting method and the ``async void `` method
492
+ behave identically to an equivalent script written in GDScript
493
+ that uses its version of the ``await `` keyword;
494
+ the method pauses once it reaches the ``await ``-ed method call.
495
+ The game loop will run until the task completes, at which point execution will continue on the main thread.
496
+
497
+ Let's look at the output from the above code:
498
+
499
+ .. code-block ::
500
+
501
+ 00:00.00 Thread: 1 Task 1 started from _Ready
502
+ 00:00.02 Thread: 1 Task 2 started from _Process
503
+ 00:00.03 Thread: 1 Task 3 started from _Process
504
+ 00:00.50 Thread: 1 Task 1 completed
505
+ 00:00.53 Thread: 1 Task 2 completed
506
+ 00:00.53 Thread: 1 Task 3 completed
507
+
508
+ The first observation from the output is that the game loop continues
509
+ without waiting for the completion of the ``_Ready() `` method.
510
+ This continuation can introduce issues, especially if methods like ``_Process() ``
511
+ rely on variables or objects that get initialized only after the ``await `` call in ``_Ready() ``.
512
+ Such asynchronous timing problems are termed `Race Condition <https://en.wikipedia.org/wiki/Race_condition#In_software/ >`_,
513
+ which is one of the main hazards when working with asynchronous code.
514
+ To avoid errors due to race conditions, be sure to check that values are initialized before you use them,
515
+ and use ``IsInstanceValid() `` after you ``await `` a function.
516
+
517
+ Here is a pattern you can adopt to avoid race conditions:
518
+
519
+ .. code-block :: csharp
520
+
521
+ public partial class SampleAsyncNode : Node
522
+ {
523
+ [Signal ] public delegate void InitializedEventHandler ();
524
+ [Export ] public int EntityID { get ; set ; } = 1 ;
525
+
526
+ readonly SomeCustomRepository _db = new ();
527
+ private int _health ;
528
+ private bool _init ;
529
+
530
+ // We will check IsInvalid after we await async methods
531
+ // Otherwise we risk the continuation running in a disposed context
532
+ private bool IsInvalid => ! IsInstanceValid (this ) || this .IsQueuedForDeletion ();
533
+
534
+ public override async void _Ready ()
535
+ {
536
+ var entity = await _db .FindAsync (EntityID );
537
+
538
+ // Even though we are still in _Ready(), we need to check IsInvalid
539
+ // It's possible that this node was freed by its parent, or some other source while awaiting
540
+ if (IsInvalid )
541
+ return ;
542
+
543
+ _health = entity .Health ;
544
+ _init = true ;
545
+ EmitSignal (SignalName .Initialized );
546
+ }
547
+
548
+ public async Task DealDamage (int damage )
549
+ {
550
+ // DealDamage depends on _health being inititalized
551
+ // Awaiting Initialized will cause all calls to DealDamage to queue up
552
+ // Once Initialized is emitted, all queued DealDamage calls will continue at once
553
+ await ToSignal (this , SignalName .Initialized );
554
+
555
+ // If you don't want to queue calls while waiting for initialization, just return if not initialized
556
+ // if (!_init)
557
+ // return;
558
+
559
+ if (IsInvalid )
560
+ return ;
561
+
562
+ _health -= damage ;
563
+
564
+ // If the number of queued calls to DealDamage is greater than the initial value of _health...
565
+ // This line will free the node before all calls to DealDamage are continued
566
+ // That is why it is important to check IsInvalid after awaiting
567
+ if (_health < 0 )
568
+ QueueFree ();
569
+ }
570
+
571
+ public override void _ExitTree ()
572
+ {
573
+ // If this unit was freed before initialization completed...
574
+ // Emit the Initialized signal so that everything awaiting it will be released
575
+ if (! _init )
576
+ EmitSignal (SignalName .Initialized );
577
+
578
+ _db .Dispose ();
579
+ }
580
+ }
581
+
582
+ The third option is to execute the ``async `` method synchronously.
583
+ This is most commonly done when you need to use an asynchronous
584
+ method from a third party library that has no synchronous equivalent,
585
+ and it is not feasible to convert everything upstream to ``async ``.
586
+
587
+ .. code-block :: csharp
588
+
589
+ public override void _Ready ()
590
+ {
591
+ start = DateTime .Now ;
592
+
593
+ Task .Run (async () => await DoStuffAsync (. 5 , nameof (_Ready ))).GetAwaiter ().GetResult ();
594
+ }
595
+
596
+ public override void _Process (double delta )
597
+ {
598
+ if (_taskCount < 3 )
599
+ Task .Run (async () => await DoStuffAsync (. 5 , nameof (_Process ))).GetAwaiter ().GetResult ();
600
+ }
601
+
602
+ Let's look at the output from the above code:
603
+
604
+ .. code-block ::
605
+
606
+ 00:00.00 Thread: 4 Task 1 started from _Ready
607
+ 00:00.50 Thread: 4 Task 1 completed
608
+ 00:00.52 Thread: 4 Task 2 started from _Process
609
+ 00:01.02 Thread: 4 Task 2 completed
610
+ 00:01.03 Thread: 4 Task 3 started from _Process
611
+ 00:01.53 Thread: 4 Task 3 completed
612
+
613
+ The output from running the tasks synchronously shows that
614
+ the tasks executed in the expected order for synchronous operations.
615
+ The output also shows that the code was executed on Thread 4,
616
+ rather than Thread 1 like in the first two options.
617
+ This is important to keep in mind, because any code that is not
618
+ executed on the main thread (Thread 1) cannot interact with the scene tree, as it is not thread safe.
619
+ You should use ``CallDeferred ``/``SetDeferred ``, ``CallThreadSafe ``/``SetThreadSafe ``,
620
+ or ``CallDeferredThreadGroup ``/``SetDeferredThreadGroup `` to interact with thread
621
+ safe objects or APIs from threads other than the main thread.
0 commit comments