All, I have been given the job to multi-thread a large C# application. To do this I have chosen to go use async/await. I am well aware of the use of IProgress<T> to report progress to the UI (let’s call this ‘pushing’ information to the UI), but I also need to ‘pull’ data from the UI (in my case a SpreadsheetGear workbook, which contains data). It is this two-way interaction that I want some advice on…
Currently I fire a click event to start the processing, and the code has the following structure:
CancellationTokenSource cancelSource;
private async void SomeButton_Click(object sender, EventArgs e)
{
// Set up progress reporting.
IProgress<CostEngine.ProgressInfo> progressIndicator =
new Progress<CostEngine.ProgressInfo>();
// Set up cancellation support, and UI scheduler.
cancelSource = new CancellationTokenSource();
CancellationToken token = cancelSource.Token;
TaskScheduler UIScheduler = TaskScheduler.FromCurrentSynchronizationContext();
// Run the script processor async.
CostEngine.ScriptProcessor script = new CostEngine.ScriptProcessor(this);
await script.ProcessScriptAsync(doc, progressIndicator, token, UIScheduler);
// Do stuff in continuation...
...
}
Then in ProcessScriptAsync, I have the following:
public async Task ProcessScriptAsync(
SpreadsheetGear.Windows.Forms.WorkbookView workbookView,
IProgress<ProgressInfo> progressInfo,
CancellationToken token,
TaskScheduler UIScheduler)
{
// This is still on the UI thread.
// Here do some checks on the script workbook on the UI thread.
try
{
workbookView.GetLock();
// Now perform tests...
}
finally { workbookView.ReleaseLock(); }
// Set the main processor off on a background thread-pool thread using await.
Task<bool> generateStageTask = null;
generateStageTask = Task.Factory.StartNew<bool>(() =>
GenerateStage(workbookView,
progressInfo,
token,
UIScheduler));
bool bGenerationSuccess = await generateStageTask;
// Automatic continuation back on UI thread.
if (!bGenerationSuccess) { // Do stuff... }
else {
// Do other stuff
}
}
This, so far, seems fine. The problem I now have is in the method GenerateStage, which is now run on a background thread-pool thread
private bool GenerateStage(
SpreadsheetGear.WorkbookView workbookView,
IProgress<ProgressInfo> progressInfo,
CancellationToken token,
TaskScheduler scheduler)
{
...
// Get the required data using the relevant synchronisation context.
SpreadsheetGear.IWorksheet worksheet = null;
SpreadsheetGear.IRange range = null;
Task task = Task.Factory.StartNew(() =>
{
worksheet = workbookView.ActiveWorksheet;
range = worksheet.UsedRange;
}, CancellationToken.None,
TaskCreationOptions.None,
scheduler);
try
{
task.Wait();
}
finally
{
task.Dispose();
}
// Now perform operations with 'worksheet'/'range' on the thread-pool thread...
}
In this method I need to pull data from the UI and write data to the UI many times. For the writing I can clearly use ‘progressInfo’, but how to handle the pulling information from the UI. Here, I have used the UI thread synchronisation context, but this will be done many times. Is there a better way to perform these operations/are there any flaws in my current approach?
Note. Clearly I would wrap the Task.Factory.StartNew(...) code up into a reusable method, the above is shown explicitly for breivity.
If you’re constantly going back and forth between UI and thread pool threads, your code is going to be a bit messy.
You essentially have two options: have your “normal” context be the thread pool thread with portions scheduled to the UI thread (as you have it now), or have your “normal” context be the UI thread with portions scheduled to a thread pool thread.
I usually prefer the latter, since you can use the simpler
Task.Runinstead ofTask.Factory.StartNewon a specificTaskScheduler. But either way, the code is going to be a bit messy.