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);
}