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.
WorkbookViewhas to be accessed from the UI thread, butIWorksheetandIRangecan be used from another thread? Couldn't you just pass the two objects toGenerateStage()?workbookViewAsync = workbookViewwill not create a copy, it will just copy the reference to another variable. But the new variable will still point to the old object.worksheetorIRangeyou have to callGetLock()on the workbook set orWorkbookView, this again means accessing theWorkbookViewwhich is created on the UI thread.workbookViewinGenerateStageWITHOUT changing the synchronisation context - I don't know why this is happening, if I update other controls I get the usualInvalidOperationException?? Very odd...