Improve InvokeOnMainThread mechanism

Add documentation to make the invoke mechanism easier to understand
Make it clear in documentation that code is invoked synchronously
Use Action and Func<T> for minimizing the amount of code to write
Use type parameter T to automatically adjust return value type
Throw exceptions on the calling thread, not the main thread
This commit is contained in:
ORelio 2021-05-15 16:31:02 +02:00
parent 9e5364a4ff
commit c1cfaf520d
6 changed files with 164 additions and 59 deletions

View file

@ -1402,12 +1402,13 @@ namespace MinecraftClient
/// Console.WriteLine("10 seconds has passed"); /// Console.WriteLine("10 seconds has passed");
/// }), 100); /// }), 100);
/// </example> /// </example>
protected void ScheduleTaskDelayed(Delegate task, int delayTicks = 0) // TODO: Adapt to new IMinecraftComHandler API
/*protected void ScheduleTaskDelayed(Delegate task, int delayTicks = 0)
{ {
if (delayTicks <= 0) if (delayTicks <= 0)
{ {
// Immediately schedule to run on next update // Immediately schedule to run on next update
Handler.ScheduleTask(task); Handler.InvokeOnMainThread(task);
} }
else else
{ {
@ -1416,17 +1417,19 @@ namespace MinecraftClient
delayTasks.Add(new DelayedTask(task, delayTicks)); delayTasks.Add(new DelayedTask(task, delayTicks));
} }
} }
} }*/
/// <summary> /// <summary>
/// Schedule a task to run on main thread. /// Schedule a task to run on main thread.
/// </summary> /// </summary>
/// <param name="task">Task to run</param> /// <param name="task">Task to run</param>
/// <returns>Any value returned from the task</returns> /// <returns>Any value returned from the task</returns>
// TODO: Adapt to new IMinecraftComHandler API
/*
protected object ScheduleTask(Delegate task) protected object ScheduleTask(Delegate task)
{ {
return Handler.ScheduleTask(task); return Handler.InvokeOnMainThread(task);
} }*/
/// <summary> /// <summary>
/// Command runner definition. /// Command runner definition.

View file

@ -32,7 +32,7 @@ namespace MinecraftClient
private Queue<string> chatQueue = new Queue<string>(); private Queue<string> chatQueue = new Queue<string>();
private static DateTime nextMessageSendTime = DateTime.MinValue; private static DateTime nextMessageSendTime = DateTime.MinValue;
private Queue<TaskWithResult> threadTasks = new Queue<TaskWithResult>(); private Queue<Action> threadTasks = new Queue<Action>();
private object threadTasksLock = new object(); private object threadTasksLock = new object();
private readonly List<ChatBot> bots = new List<ChatBot>(); private readonly List<ChatBot> bots = new List<ChatBot>();
@ -302,7 +302,6 @@ namespace MinecraftClient
/// <summary> /// <summary>
/// Allows the user to send chat messages, commands, and leave the server. /// Allows the user to send chat messages, commands, and leave the server.
/// Enqueue text typed in the command prompt for processing on the main thread.
/// </summary> /// </summary>
private void CommandPrompt() private void CommandPrompt()
{ {
@ -312,7 +311,7 @@ namespace MinecraftClient
while (client.Client.Connected) while (client.Client.Connected)
{ {
string text = ConsoleIO.ReadLine(); string text = ConsoleIO.ReadLine();
ScheduleTask(new Action(() => { HandleCommandPromptText(text); })); InvokeOnMainThread(() => HandleCommandPromptText(text));
} }
} }
catch (IOException) { } catch (IOException) { }
@ -641,14 +640,12 @@ namespace MinecraftClient
SendRespawnPacket(); SendRespawnPacket();
} }
lock (threadTasksLock) lock (threadTasksLock)
{ {
while (threadTasks.Count > 0) while (threadTasks.Count > 0)
{ {
var taskToRun = threadTasks.Dequeue(); Action taskToRun = threadTasks.Dequeue();
taskToRun.Execute(); taskToRun();
taskToRun.Release();
} }
} }
} }
@ -696,28 +693,43 @@ namespace MinecraftClient
} }
/// <summary> /// <summary>
/// Schedule a task to run on the main thread /// Invoke a task on the main thread, wait for completion and retrieve return value.
/// </summary> /// </summary>
/// <param name="task">Task to run</param> /// <param name="task">Task to run with any type or return value</param>
/// <returns>Any result returned from delegate</returns> /// <returns>Any result returned from task, result type is inferred from the task</returns>
public object ScheduleTask(Delegate task) /// <example>bool result = InvokeOnMainThread(methodThatReturnsAbool);</example>
/// <example>bool result = InvokeOnMainThread(() => methodThatReturnsAbool(argument));</example>
/// <example>int result = InvokeOnMainThread(() => { yourCode(); return 42; });</example>
/// <typeparam name="T">Type of the return value</typeparam>
public T InvokeOnMainThread<T>(Func<T> task)
{ {
if (!InvokeRequired()) if (!InvokeRequired())
{ {
return task.DynamicInvoke(); return task();
} }
else else
{ {
var taskAndResult = new TaskWithResult(task); TaskWithResult<T> taskWithResult = new TaskWithResult<T>(task);
lock (threadTasksLock) lock (threadTasksLock)
{ {
threadTasks.Enqueue(taskAndResult); threadTasks.Enqueue(taskWithResult.ExecuteSynchronously);
} }
taskAndResult.Block(); return taskWithResult.WaitGetResult();
return taskAndResult.Result;
} }
} }
/// <summary>
/// Invoke a task on the main thread and wait for completion
/// </summary>
/// <param name="task">Task to run without return value</param>
/// <example>InvokeOnMainThread(methodThatReturnsNothing);</example>
/// <example>InvokeOnMainThread(() => methodThatReturnsNothing(argument));</example>
/// <example>InvokeOnMainThread(() => { yourCode(); });</example>
public void InvokeOnMainThread(Action task)
{
InvokeOnMainThread(() => { task(); return true; });
}
/// <summary> /// <summary>
/// Check if calling thread is main thread or other thread /// Check if calling thread is main thread or other thread
/// </summary> /// </summary>

View file

@ -423,10 +423,9 @@ namespace MinecraftClient.Protocol.Handlers
int compressedDataSize = dataTypes.ReadNextInt(packetData); int compressedDataSize = dataTypes.ReadNextInt(packetData);
byte[] compressed = dataTypes.ReadData(compressedDataSize, packetData); byte[] compressed = dataTypes.ReadData(compressedDataSize, packetData);
byte[] decompressed = ZlibUtils.Decompress(compressed); byte[] decompressed = ZlibUtils.Decompress(compressed);
new Task(new Action(() => new Thread(() => {
{
pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, addBitmap, currentDimension == 0, chunksContinuous, currentDimension, new Queue<byte>(decompressed)); pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, addBitmap, currentDimension == 0, chunksContinuous, currentDimension, new Queue<byte>(decompressed));
})).Start(); }).Start();
} }
else else
{ {
@ -450,10 +449,9 @@ namespace MinecraftClient.Protocol.Handlers
else dataTypes.ReadData(1024 * 4, packetData); // Biomes - 1.15 and above else dataTypes.ReadData(1024 * 4, packetData); // Biomes - 1.15 and above
} }
int dataSize = dataTypes.ReadNextVarInt(packetData); int dataSize = dataTypes.ReadNextVarInt(packetData);
new Task(new Action(() => new Thread(() => {
{
pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, 0, false, chunksContinuous, currentDimension, packetData); pTerrain.ProcessChunkColumnData(chunkX, chunkZ, chunkMask, 0, false, chunksContinuous, currentDimension, packetData);
})).Start(); }).Start();
} }
} }
break; break;

View file

@ -162,12 +162,12 @@ namespace MinecraftClient.Protocol.Handlers
} }
//We have our chunk, save the chunk into the world //We have our chunk, save the chunk into the world
handler.ScheduleTask(new Action(() => handler.InvokeOnMainThread(() =>
{ {
if (handler.GetWorld()[chunkX, chunkZ] == null) if (handler.GetWorld()[chunkX, chunkZ] == null)
handler.GetWorld()[chunkX, chunkZ] = new ChunkColumn(); handler.GetWorld()[chunkX, chunkZ] = new ChunkColumn();
handler.GetWorld()[chunkX, chunkZ][chunkY] = chunk; handler.GetWorld()[chunkX, chunkZ][chunkY] = chunk;
})); });
//Pre-1.14 Lighting data //Pre-1.14 Lighting data
if (protocolversion < Protocol18Handler.MC114Version) if (protocolversion < Protocol18Handler.MC114Version)
@ -192,10 +192,10 @@ namespace MinecraftClient.Protocol.Handlers
if (chunksContinuous && chunkMask == 0) if (chunksContinuous && chunkMask == 0)
{ {
//Unload the entire chunk column //Unload the entire chunk column
handler.ScheduleTask(new Action(() => handler.InvokeOnMainThread(() =>
{ {
handler.GetWorld()[chunkX, chunkZ] = null; handler.GetWorld()[chunkX, chunkZ] = null;
})); });
} }
else else
{ {
@ -214,12 +214,12 @@ namespace MinecraftClient.Protocol.Handlers
chunk[blockX, blockY, blockZ] = new Block(queue.Dequeue()); chunk[blockX, blockY, blockZ] = new Block(queue.Dequeue());
//We have our chunk, save the chunk into the world //We have our chunk, save the chunk into the world
handler.ScheduleTask(new Action(() => handler.InvokeOnMainThread(() =>
{ {
if (handler.GetWorld()[chunkX, chunkZ] == null) if (handler.GetWorld()[chunkX, chunkZ] == null)
handler.GetWorld()[chunkX, chunkZ] = new ChunkColumn(); handler.GetWorld()[chunkX, chunkZ] = new ChunkColumn();
handler.GetWorld()[chunkX, chunkZ][chunkY] = chunk; handler.GetWorld()[chunkX, chunkZ][chunkY] = chunk;
})); });
} }
} }
@ -248,10 +248,10 @@ namespace MinecraftClient.Protocol.Handlers
if (chunksContinuous && chunkMask == 0) if (chunksContinuous && chunkMask == 0)
{ {
//Unload the entire chunk column //Unload the entire chunk column
handler.ScheduleTask(new Action(() => handler.InvokeOnMainThread(() =>
{ {
handler.GetWorld()[chunkX, chunkZ] = null; handler.GetWorld()[chunkX, chunkZ] = null;
})); });
} }
else else
{ {
@ -297,12 +297,12 @@ namespace MinecraftClient.Protocol.Handlers
for (int blockX = 0; blockX < Chunk.SizeX; blockX++) for (int blockX = 0; blockX < Chunk.SizeX; blockX++)
chunk[blockX, blockY, blockZ] = new Block(blockTypes.Dequeue(), blockMeta.Dequeue()); chunk[blockX, blockY, blockZ] = new Block(blockTypes.Dequeue(), blockMeta.Dequeue());
handler.ScheduleTask(new Action(() => handler.InvokeOnMainThread(() =>
{ {
if (handler.GetWorld()[chunkX, chunkZ] == null) if (handler.GetWorld()[chunkX, chunkZ] == null)
handler.GetWorld()[chunkX, chunkZ] = new ChunkColumn(); handler.GetWorld()[chunkX, chunkZ] = new ChunkColumn();
handler.GetWorld()[chunkX, chunkZ][chunkY] = chunk; handler.GetWorld()[chunkX, chunkZ][chunkY] = chunk;
})); });
} }
} }
} }

View file

@ -41,11 +41,24 @@ namespace MinecraftClient.Protocol
ILogger GetLogger(); ILogger GetLogger();
/// <summary> /// <summary>
/// Schedule a task to run on the main thread /// Invoke a task on the main thread, wait for completion and retrieve return value.
/// </summary> /// </summary>
/// <param name="task">Task to run</param> /// <param name="task">Task to run with any type or return value</param>
/// <returns>Any result returned from delegate</returns> /// <returns>Any result returned from task, result type is inferred from the task</returns>
object ScheduleTask(Delegate task); /// <example>bool result = InvokeOnMainThread(methodThatReturnsAbool);</example>
/// <example>bool result = InvokeOnMainThread(() => methodThatReturnsAbool(argument));</example>
/// <example>int result = InvokeOnMainThread(() => { yourCode(); return 42; });</example>
/// <typeparam name="T">Type of the return value</typeparam>
T InvokeOnMainThread<T>(Func<T> task);
/// <summary>
/// Invoke a task on the main thread and wait for completion
/// </summary>
/// <param name="task">Task to run without return value</param>
/// <example>InvokeOnMainThread(methodThatReturnsNothing);</example>
/// <example>InvokeOnMainThread(() => methodThatReturnsNothing(argument));</example>
/// <example>InvokeOnMainThread(() => { yourCode(); });</example>
void InvokeOnMainThread(Action task);
/// <summary> /// <summary>
/// Called when a network packet received or sent /// Called when a network packet received or sent

View file

@ -6,42 +6,121 @@ using System.Threading;
namespace MinecraftClient namespace MinecraftClient
{ {
public class TaskWithResult /// <summary>
/// Holds an asynchronous task with return value
/// </summary>
/// <typeparam name="T">Type of the return value</typeparam>
public class TaskWithResult<T>
{ {
private Delegate Task; private AutoResetEvent resultEvent = new AutoResetEvent(false);
private AutoResetEvent ResultEvent = new AutoResetEvent(false); private Func<T> task;
private T result = default(T);
private Exception exception = null;
private bool taskRun = false;
private object taskRunLock = new object();
public object Result; /// <summary>
/// Create a new asynchronous task with return value
public TaskWithResult(Delegate task) /// </summary>
/// <param name="task">Delegate with return value</param>
public TaskWithResult(Func<T> task)
{ {
Task = task; this.task = task;
} }
/// <summary> /// <summary>
/// Execute the delegate and set the <see cref="Result"/> property to the returned value /// Check whether the task has finished running
/// </summary> /// </summary>
/// <returns>Value returned from delegate</returns> public bool HasRun
public object Execute()
{ {
Result = Task.DynamicInvoke(); get
return Result; {
return taskRun;
}
} }
/// <summary> /// <summary>
/// Block the program execution /// Get the task result (return value of the inner delegate)
/// </summary> /// </summary>
public void Block() /// <exception cref="System.InvalidOperationException">Thrown if the task is not finished yet</exception>
public T Result
{ {
ResultEvent.WaitOne(); get
{
if (taskRun)
{
return result;
}
else throw new InvalidOperationException("Attempting to retrieve the result of an unfinished task");
}
} }
/// <summary> /// <summary>
/// Resume the program execution /// Get the exception thrown by the inner delegate, if any
/// </summary> /// </summary>
public void Release() public Exception Exception
{ {
ResultEvent.Set(); get
{
return exception;
}
}
/// <summary>
/// Execute the task in the current thread and set the <see cref="Result"/> property or <see cref=""/>to the returned value
/// </summary>
public void ExecuteSynchronously()
{
// Make sur the task will not run twice
lock (taskRunLock)
{
if (taskRun)
{
throw new InvalidOperationException("Attempting to run a task twice");
}
}
// Run the task
try
{
result = task();
}
catch (Exception e)
{
exception = e;
}
// Mark task as complete and release wait event
lock (taskRunLock)
{
taskRun = true;
}
resultEvent.Set();
}
/// <summary>
/// Wait until the task has run from another thread and get the returned value or exception thrown by the task
/// </summary>
/// <returns>Task result once available</returns>
/// <exception cref="System.Exception">Any exception thrown by the task</exception>
public T WaitGetResult()
{
// Wait only if the result is not available yet
bool mustWait = false;
lock (taskRunLock)
{
mustWait = !taskRun;
}
if (mustWait)
{
resultEvent.WaitOne();
}
// Receive exception from task
if (exception != null)
throw exception;
return result;
} }
} }
} }