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");
/// }), 100);
/// </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)
{
// Immediately schedule to run on next update
Handler.ScheduleTask(task);
Handler.InvokeOnMainThread(task);
}
else
{
@ -1416,17 +1417,19 @@ namespace MinecraftClient
delayTasks.Add(new DelayedTask(task, delayTicks));
}
}
}
}*/
/// <summary>
/// Schedule a task to run on main thread.
/// </summary>
/// <param name="task">Task to run</param>
/// <returns>Any value returned from the task</returns>
// TODO: Adapt to new IMinecraftComHandler API
/*
protected object ScheduleTask(Delegate task)
{
return Handler.ScheduleTask(task);
}
return Handler.InvokeOnMainThread(task);
}*/
/// <summary>
/// Command runner definition.

View file

@ -32,7 +32,7 @@ namespace MinecraftClient
private Queue<string> chatQueue = new Queue<string>();
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 readonly List<ChatBot> bots = new List<ChatBot>();
@ -302,7 +302,6 @@ namespace MinecraftClient
/// <summary>
/// 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>
private void CommandPrompt()
{
@ -312,7 +311,7 @@ namespace MinecraftClient
while (client.Client.Connected)
{
string text = ConsoleIO.ReadLine();
ScheduleTask(new Action(() => { HandleCommandPromptText(text); }));
InvokeOnMainThread(() => HandleCommandPromptText(text));
}
}
catch (IOException) { }
@ -641,14 +640,12 @@ namespace MinecraftClient
SendRespawnPacket();
}
lock (threadTasksLock)
{
while (threadTasks.Count > 0)
{
var taskToRun = threadTasks.Dequeue();
taskToRun.Execute();
taskToRun.Release();
Action taskToRun = threadTasks.Dequeue();
taskToRun();
}
}
}
@ -696,28 +693,43 @@ namespace MinecraftClient
}
/// <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>
/// <param name="task">Task to run</param>
/// <returns>Any result returned from delegate</returns>
public object ScheduleTask(Delegate task)
/// <param name="task">Task to run with any type or return value</param>
/// <returns>Any result returned from task, result type is inferred from the task</returns>
/// <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())
{
return task.DynamicInvoke();
return task();
}
else
{
var taskAndResult = new TaskWithResult(task);
TaskWithResult<T> taskWithResult = new TaskWithResult<T>(task);
lock (threadTasksLock)
{
threadTasks.Enqueue(taskAndResult);
threadTasks.Enqueue(taskWithResult.ExecuteSynchronously);
}
taskAndResult.Block();
return taskAndResult.Result;
return taskWithResult.WaitGetResult();
}
}
/// <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>
/// Check if calling thread is main thread or other thread
/// </summary>

View file

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

View file

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

View file

@ -41,11 +41,24 @@ namespace MinecraftClient.Protocol
ILogger GetLogger();
/// <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>
/// <param name="task">Task to run</param>
/// <returns>Any result returned from delegate</returns>
object ScheduleTask(Delegate task);
/// <param name="task">Task to run with any type or return value</param>
/// <returns>Any result returned from task, result type is inferred from the task</returns>
/// <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>
/// Called when a network packet received or sent

View file

@ -6,42 +6,121 @@ using System.Threading;
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;
public TaskWithResult(Delegate task)
/// <summary>
/// Create a new asynchronous task with return value
/// </summary>
/// <param name="task">Delegate with return value</param>
public TaskWithResult(Func<T> task)
{
Task = task;
this.task = task;
}
/// <summary>
/// Execute the delegate and set the <see cref="Result"/> property to the returned value
/// Check whether the task has finished running
/// </summary>
/// <returns>Value returned from delegate</returns>
public object Execute()
public bool HasRun
{
Result = Task.DynamicInvoke();
return Result;
get
{
return taskRun;
}
}
/// <summary>
/// Block the program execution
/// Get the task result (return value of the inner delegate)
/// </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>
/// Resume the program execution
/// Get the exception thrown by the inner delegate, if any
/// </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;
}
}
}