My mapping script Maproom needs to download images to be able to build maps. Lots of small images of 256*256 pixels are downloaded and glued together. Downloading multiple images at the same time makes total sense. Until now I’ve had little success with using the backgroundworker system for multithreading. It sort of works, but I failed to make it stable. Lonerobot has a great article on it though.
In .net there’s also the Async/Await pattern which is supposed to make asynchronous actions a lot easier to set up. And that’s true when working in C#. But when combining C# and Maxscript some extra love and care is needed to make it work.
What’s Async/Await?
First of all, what does async/await do? It lets you set up a task, have that task do something (async) and continue with other stuff while that task is still working. In the mean time, you can expect the results from that task to come back to you (await). It’s like cooking. You put the water on and at the same time you cut the vegetables. But because you need to water to be boiling before you put the vegetables in, you wait for the water. There’s a lot more, but this is as much as I need to understand to get going. If you want more, Stephen Toub has you covered here.
UI Deadlock
Stephen Toub describes a ui deadlock which happens when you use the async system incorrectly. Basically, the ui is waiting for the async task to finish, but the async task can’t tell it’s finished because the ui is locked. Combining C# and Maxscript is very prone to this issue. This can be solved by using the ConfigureAwait(false)
method on your async methods in C#.
Maxscript
Here’s the Maxscript to download one single file ten times asynchronously. Note that every await has the ConfigureAwait(false)
added to it.
(
--Klaas Nienhuis, 2016
clearListener()
--this .net class sets up async downloads
local strAssembly = \
"
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace Downloader
{
public class Download
{
public static async Task DownloadFileAsync(string remoteUrl, string localUrl)
{
using (HttpClient client = new HttpClient())
{
using (HttpResponseMessage responseMessage = await client.GetAsync(remoteUrl).ConfigureAwait(false))
{
if (responseMessage.IsSuccessStatusCode)
{
var byteArray = await responseMessage.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
using (FileStream filestream = new FileStream(localUrl, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize:4096, useAsync:true))
{
await filestream.WriteAsync(byteArray, 0, byteArray.Length);
}
}
}
}
}
public static async Task DownloadMultipleFilesAsync(string[] remoteUrl, string[] localUrl)
{
List allTasks = new List();
for (int n = 0; n < remoteUrl.Length; n++)
{
allTasks.Add(DownloadFileAsync(remoteUrl[n], localUrl[n]));
}
await Task.WhenAll(allTasks).ConfigureAwait(false);
}
}
}
"
function fn_loadClass strAssembly =
(
/*
Description
Loads a .net class from a string
Arguments
Return
compilerResults which can be instanciated
*/
local csharpProvider = dotnetobject "Microsoft.CSharp.CSharpCodeProvider"
local compilerParams = dotnetobject "System.CodeDom.Compiler.CompilerParameters"
compilerParams.ReferencedAssemblies.AddRange #("System.Net.Http.dll","System.dll", "System.Management.dll")
compilerParams.GenerateInMemory = on
local compilerResults = csharpProvider.CompileAssemblyFromSource compilerParams #(strAssembly)
)
local compilerResults = fn_loadClass strAssembly
local Download = compilerResults.CompiledAssembly.CreateInstance "Downloader.Download"
--set up a folder to receive the downloaded files
local localFolder = (dotnetClass "system.IO.Path").Combine (pathConfig.removePathLeaf (getSourceFileName())) "output"
makeDir localFolder
local arrUrl = #()
local arrFilePath = #()
for n = 1 to 10 do
(
append arrUrl @"http://a.tile.openstreetmap.org/8/131/84.png"; --we'll download this file
append arrFilePath ((dotnetClass "system.IO.Path").Combine localFolder (n as string + ".png"))
)
local theTask = Download.DownloadMultipleFilesAsync arrUrl arrFilePath
--we need to wait for the async download task to finish, otherwise we might get in trouble with the rest of the script. If we don't
--wait, we're counting on the files to be downloaded while they might not be there yet.
theTask.Wait()
local arrFile = getFiles (localFolder + @"\*.png")
format "Downloaded % files\r\n" arrFile.count
)