diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 252a02031..e03ea882e 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -103,9 +103,6 @@ internal sealed class Framework : IInternalDisposableService, IFramework /// public DateTime LastUpdateUTC { get; private set; } = DateTime.MinValue; - /// - public TaskFactory FrameworkThreadTaskFactory { get; } - /// public TimeSpan UpdateDelta { get; private set; } = TimeSpan.Zero; @@ -125,6 +122,11 @@ internal sealed class Framework : IInternalDisposableService, IFramework /// internal bool DispatchUpdateEvents { get; set; } = true; + private TaskFactory FrameworkThreadTaskFactory { get; } + + /// + public TaskFactory GetTaskFactory() => this.FrameworkThreadTaskFactory; + /// public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) { @@ -138,6 +140,38 @@ internal sealed class Framework : IInternalDisposableService, IFramework return tcs.Task; } + /// + public Task RunOnFrameworkThreadAwaitable(Action action, CancellationToken cancellationToken = default) + { + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken); + } + + /// + public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default) + { + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken); + } + + /// + public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default) + { + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken).Unwrap(); + } + + /// + public Task RunOnFrameworkThreadAwaitable(Func> action, CancellationToken cancellationToken = default) + { + if (cancellationToken == default) + cancellationToken = this.FrameworkThreadTaskFactory.CancellationToken; + return this.FrameworkThreadTaskFactory.StartNew(action, cancellationToken).Unwrap(); + } + /// public Task RunOnFrameworkThread(Func func) => this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func); @@ -193,7 +227,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework this.DelayTicks(delayTicks, cancellationToken), }, _ => func(), - cancellationToken); + cancellationToken, + TaskContinuationOptions.HideScheduler, + this.frameworkThreadTaskScheduler); } /// @@ -218,7 +254,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework this.DelayTicks(delayTicks, cancellationToken), }, _ => action(), - cancellationToken); + cancellationToken, + TaskContinuationOptions.HideScheduler, + this.frameworkThreadTaskScheduler); } /// @@ -243,7 +281,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework this.DelayTicks(delayTicks, cancellationToken), }, _ => func(), - cancellationToken).Unwrap(); + cancellationToken, + TaskContinuationOptions.HideScheduler, + this.frameworkThreadTaskScheduler).Unwrap(); } /// @@ -268,7 +308,9 @@ internal sealed class Framework : IInternalDisposableService, IFramework this.DelayTicks(delayTicks, cancellationToken), }, _ => func(), - cancellationToken).Unwrap(); + cancellationToken, + TaskContinuationOptions.HideScheduler, + this.frameworkThreadTaskScheduler).Unwrap(); } /// @@ -491,9 +533,6 @@ internal class FrameworkPluginScoped : IInternalDisposableService, IFramework /// public DateTime LastUpdateUTC => this.frameworkService.LastUpdateUTC; - /// - public TaskFactory FrameworkThreadTaskFactory => this.frameworkService.FrameworkThreadTaskFactory; - /// public TimeSpan UpdateDelta => this.frameworkService.UpdateDelta; @@ -511,10 +550,29 @@ internal class FrameworkPluginScoped : IInternalDisposableService, IFramework this.Update = null; } + /// + public TaskFactory GetTaskFactory() => this.frameworkService.GetTaskFactory(); + /// public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default) => this.frameworkService.DelayTicks(numTicks, cancellationToken); + /// + public Task RunOnFrameworkThreadAwaitable(Action action, CancellationToken cancellationToken = default) => + this.frameworkService.RunOnFrameworkThreadAwaitable(action, cancellationToken); + + /// + public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default) => + this.frameworkService.RunOnFrameworkThreadAwaitable(action, cancellationToken); + + /// + public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default) => + this.frameworkService.RunOnFrameworkThreadAwaitable(action, cancellationToken); + + /// + public Task RunOnFrameworkThreadAwaitable(Func> action, CancellationToken cancellationToken = default) => + this.frameworkService.RunOnFrameworkThreadAwaitable(action, cancellationToken); + /// public Task RunOnFrameworkThread(Func func) => this.frameworkService.RunOnFrameworkThread(func); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs index c6d8c4e8b..0c86466e3 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/TaskSchedulerWidget.cs @@ -144,9 +144,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget _ = framework.RunOnTick(() => Log.Information("Framework.Update - In 2s+60f"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(2), delayTicks: 60); } - ImGui.SameLine(); - - if (ImGui.Button("Every 60 frames")) + if (ImGui.Button("Every 60f")) { _ = framework.RunOnTick( async () => @@ -154,6 +152,8 @@ internal class TaskSchedulerWidget : IDataWindowWidget for (var i = 0L; ; i++) { Log.Information($"Loop #{i}; MainThread={ThreadSafety.IsMainThread}"); + var it = i; + _ = Task.Factory.StartNew(() => Log.Information($" => Sub #{it}; MainThread={ThreadSafety.IsMainThread}")); await framework.DelayTicks(60, this.taskSchedulerCancelSource.Token); } }, @@ -162,6 +162,68 @@ internal class TaskSchedulerWidget : IDataWindowWidget ImGui.SameLine(); + if (ImGui.Button("Every 1s")) + { + _ = framework.RunOnTick( + async () => + { + for (var i = 0L; ; i++) + { + Log.Information($"Loop #{i}; MainThread={ThreadSafety.IsMainThread}"); + var it = i; + _ = Task.Factory.StartNew(() => Log.Information($" => Sub #{it}; MainThread={ThreadSafety.IsMainThread}")); + await Task.Delay(TimeSpan.FromSeconds(1), this.taskSchedulerCancelSource.Token); + } + }, + cancellationToken: this.taskSchedulerCancelSource.Token); + } + + ImGui.SameLine(); + + if (ImGui.Button("Every 60f (Await)")) + { + _ = framework.RunOnFrameworkThreadAwaitable( + async () => + { + for (var i = 0L; ; i++) + { + Log.Information($"Loop #{i}; MainThread={ThreadSafety.IsMainThread}"); + var it = i; + _ = Task.Factory.StartNew(() => Log.Information($" => Sub #{it}; MainThread={ThreadSafety.IsMainThread}")); + await framework.DelayTicks(60, this.taskSchedulerCancelSource.Token); + } + }, + this.taskSchedulerCancelSource.Token); + } + + ImGui.SameLine(); + + if (ImGui.Button("Every 1s (Await)")) + { + _ = framework.RunOnFrameworkThreadAwaitable( + async () => + { + for (var i = 0L; ; i++) + { + Log.Information($"Loop #{i}; MainThread={ThreadSafety.IsMainThread}"); + var it = i; + _ = Task.Factory.StartNew(() => Log.Information($" => Sub #{it}; MainThread={ThreadSafety.IsMainThread}")); + await Task.Delay(TimeSpan.FromSeconds(1), this.taskSchedulerCancelSource.Token); + } + }, + this.taskSchedulerCancelSource.Token); + } + + ImGui.SameLine(); + + if (ImGui.Button("As long as it's in Framework Thread")) + { + Task.Run(async () => await framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); })); + framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait(); + } + + ImGui.SameLine(); + if (ImGui.Button("Error in 1s")) { _ = framework.RunOnTick(() => throw new Exception("Test Exception"), cancellationToken: this.taskSchedulerCancelSource.Token, delay: TimeSpan.FromSeconds(1)); @@ -169,10 +231,18 @@ internal class TaskSchedulerWidget : IDataWindowWidget ImGui.SameLine(); - if (ImGui.Button("As long as it's in Framework Thread")) + if (ImGui.Button("Freeze 1s")) { - Task.Run(async () => await framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from non-framework.update thread"); })); - framework.RunOnFrameworkThread(() => { Log.Information("Task dispatched from framework.update thread"); }).Wait(); + _ = framework.RunOnFrameworkThread(() => Helper().Wait()); + static async Task Helper() => await Task.Delay(1000); + } + + ImGui.SameLine(); + + if (ImGui.Button("Freeze Completely")) + { + _ = framework.RunOnFrameworkThreadAwaitable(() => Helper().Wait()); + static async Task Helper() => await Task.Delay(1000); } if (ImGui.CollapsingHeader("Download")) @@ -217,7 +287,7 @@ internal class TaskSchedulerWidget : IDataWindowWidget this.downloadState = default; var factory = downloadUsingGlobalScheduler ? Task.Factory - : framework.FrameworkThreadTaskFactory; + : framework.GetTaskFactory(); this.downloadState = default; this.downloadTask = factory.StartNew( async () => diff --git a/Dalamud/Plugin/Services/IFramework.cs b/Dalamud/Plugin/Services/IFramework.cs index a93abd252..4b04b633e 100644 --- a/Dalamud/Plugin/Services/IFramework.cs +++ b/Dalamud/Plugin/Services/IFramework.cs @@ -1,11 +1,29 @@ using System.Threading; using System.Threading.Tasks; +using Dalamud.Interface.Internal.Windows.Data.Widgets; + namespace Dalamud.Plugin.Services; /// /// This class represents the Framework of the native game client and grants access to various subsystems. /// +/// +/// Choosing between RunOnFrameworkThread and RunOnFrameworkThreadAwaitable +///
    +///
  • If you do need to do use await and have your task keep executing on the main thread after waiting is +/// done, use RunOnFrameworkThreadAwaitable.
  • +///
  • If you need to call or , use +/// RunOnFrameworkThread.
  • +///
+/// The game is likely to completely lock up if you call above synchronous function and getter, because starting +/// a new task by default runs on , which would make the task run on the framework +/// thread if invoked via RunOnFrameworkThreadAwaitable. This includes Task.Factory.StartNew and +/// Task.ContinueWith. Use Task.Run if you need to start a new task from the callback specified to +/// RunOnFrameworkThreadAwaitable, as it will force your task to be run in the default thread pool. +/// See to see the difference in behaviors, and how would a misuse of these +/// functions result in a deadlock. +///
public interface IFramework { /// @@ -29,11 +47,6 @@ public interface IFramework /// public DateTime LastUpdateUTC { get; } - /// - /// Gets a that runs tasks during Framework Update event. - /// - public TaskFactory FrameworkThreadTaskFactory { get; } - /// /// Gets the delta between the last Framework Update and the currently executing one. /// @@ -49,20 +62,97 @@ public interface IFramework ///
public bool IsFrameworkUnloading { get; } + /// Gets a that runs tasks during Framework Update event. + /// The task factory. + public TaskFactory GetTaskFactory(); + /// /// Returns a task that completes after the given number of ticks. /// /// Number of ticks to delay. /// The cancellation token. /// A new that gets resolved after specified number of ticks happen. + /// The continuation will run on the framework thread by default. public Task DelayTicks(long numTicks, CancellationToken cancellationToken = default); + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Function to call. + /// The cancellation token. + /// Task representing the pending or already completed function. + /// + /// Starting new tasks and waiting on them synchronously from this callback will completely lock up + /// the game. Use await if you need to wait on something from an async callback. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// + public Task RunOnFrameworkThreadAwaitable(Action action, CancellationToken cancellationToken = default); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Return type. + /// Function to call. + /// The cancellation token. + /// Task representing the pending or already completed function. + /// + /// Starting new tasks and waiting on them synchronously from this callback will completely lock up + /// the game. Use await if you need to wait on something from an async callback. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// + public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Function to call. + /// The cancellation token. + /// Task representing the pending or already completed function. + /// + /// Starting new tasks and waiting on them synchronously from this callback will completely lock up + /// the game. Use await if you need to wait on something from an async callback. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// + public Task RunOnFrameworkThreadAwaitable(Func action, CancellationToken cancellationToken = default); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Return type. + /// Function to call. + /// The cancellation token. + /// Task representing the pending or already completed function. + /// + /// Starting new tasks and waiting on them synchronously from this callback will completely lock up + /// the game. Use await if you need to wait on something from an async callback. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// + public Task RunOnFrameworkThreadAwaitable(Func> action, CancellationToken cancellationToken = default); + /// /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. /// /// Return type. /// Function to call. /// Task representing the pending or already completed function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// public Task RunOnFrameworkThread(Func func); /// @@ -70,6 +160,16 @@ public interface IFramework /// /// Function to call. /// Task representing the pending or already completed function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// public Task RunOnFrameworkThread(Action action); /// @@ -78,6 +178,16 @@ public interface IFramework /// Return type. /// Function to call. /// Task representing the pending or already completed function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// [Obsolete($"Use {nameof(RunOnTick)} instead.")] public Task RunOnFrameworkThread(Func> func); @@ -86,6 +196,16 @@ public interface IFramework /// /// Function to call. /// Task representing the pending or already completed function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// [Obsolete($"Use {nameof(RunOnTick)} instead.")] public Task RunOnFrameworkThread(Func func); @@ -98,6 +218,16 @@ public interface IFramework /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. /// Cancellation token which will prevent the execution of this function if wait conditions are not met. /// Task representing the pending function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); /// @@ -108,6 +238,16 @@ public interface IFramework /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. /// Cancellation token which will prevent the execution of this function if wait conditions are not met. /// Task representing the pending function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); /// @@ -119,6 +259,16 @@ public interface IFramework /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. /// Cancellation token which will prevent the execution of this function if wait conditions are not met. /// Task representing the pending function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); /// @@ -129,5 +279,15 @@ public interface IFramework /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. /// Cancellation token which will prevent the execution of this function if wait conditions are not met. /// Task representing the pending function. + /// + /// await, Task.Factory.StartNew or alike will continue off the framework thread. + /// Awaiting on the returned from RunOnFrameworkThread, + /// RunOnFrameworkThreadAwaitable, or RunOnTick right away inside the callback specified to this + /// function has a chance of locking up the game. Do not do await framework.RunOnFrameworkThread(...); + /// directly or indirectly from the delegate passed to this function. + /// See the remarks on if you need to choose which one to use, between + /// RunOnFrameworkThreadAwaitable and RunOnFrameworkThread. Note that RunOnTick is a fancy + /// version of RunOnFrameworkThread. + /// public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default); }