0
\$\begingroup\$

I've recently answered a question on Stack Overflow where the asker was wondering how to read from the standard out of a child process he was spawning inside a Task.Run, presumably so as to avoid blocking until it starts up and starts producing output.

My answer, cleaned up a bit as per Visual Studio+R#s code analysis suggestions, and having added the usage, was this:

using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;

namespace MyDism
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var dismOutputTask = new Program().WithDismAsync("/?", async sr =>
            {
                string line;
                while ((line = await sr.ReadLineAsync().ConfigureAwait(false)) != null)
                {
                    if (line.TrimEnd().EndsWith(":"))
                    {
                        return line.ToLower();
                    }
                }

                return "oopsie-daisy";
            });
            var dismOutput = await dismOutputTask.ConfigureAwait(false);
            await Console.Out.WriteLineAsync(dismOutput).ConfigureAwait(false);
        }

        static string GetDismPath()
        {
            var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
            var systemDir = Environment.Is64BitOperatingSystem ? "SysWOW64" : "System32";
            var dismExePath = Path.Combine(windowsDir, systemDir, "dism.exe");

            return dismExePath;
        }
        public Task<TResult> WithDismAsync<TResult>(string args, Func<StreamReader, Task<TResult>> func)
        {
            return Task.Run(async () =>
            {
                var proc = new Process
                {
                    StartInfo =
                    {
                        FileName = GetDismPath(),
                        Verb = "runas",
                        Arguments = args,
                        WindowStyle = ProcessWindowStyle.Hidden,
                        CreateNoWindow = true,
                        UseShellExecute = false,
                        RedirectStandardOutput = true
                    }
                };

                proc.Start();

                // do something with the child's stdout
                var result = await func(proc.StandardOutput).ConfigureAwait(false);

                // try and make sure the process exits cleanly but don't wait for this before returning result
                Cleanup(proc);

                return result;
            });
        }

        private void Cleanup(Process proc)
        {
            Task.Run(async () =>
            {
                proc.StandardInput.Close();
                char[] buf = new char[0x1000];
                while (await proc.StandardOutput.ReadBlockAsync(buf, 0, buf.Length).ConfigureAwait(false) != 0) { }
                while (await proc.StandardError.ReadBlockAsync(buf, 0, buf.Length).ConfigureAwait(false) != 0) { }
                proc.WaitForExit();
                proc.Dispose();
            });
        }
    }
}

In brief testing it seemed to work fine, and "dism /?" seems to produce enough output to trigger the possible bug where the child process could block indefinitely on writing to its stdout.

However, I'm not an expert on the details of practical use of async..await, so I figured out this would be a good way to learn through advice on how to do things better.


Areas of interest I've thought about until now are:

  1. I admit I've peppered the code with ConfigureAwait(false) because the R# checker complained I should really do that. That said I don't know precisely what difference using false vs. true as the parameter here means, and what effect would it have.

    I'm guessing it means that if I use true, the continuation generated by the compiler will run on the same thread as the code that was running when the await statement that corresponds to the thus-configured task was encountered. But I'm hardly sure.

  2. Following up on the previous: what would be the appropriate values for the parameter to ConfigureAwait() in this context. My intuition is that the Main() method and the lambdas in it should maybe be using true so as to have all the using code execute on the same thread; but then again, is it necessary if the async..await mechanism should take care of making sure it runs in the sequence in which it is written?

  3. Assuming it is desirable to have all of the code within Main execute on the same thread, how would I achieve this with the async lambda passed as the func parameter to WithDismAsync()? The call to it is always awaited in the context of a thread grabbed from a pool by Task.Run(), so I'm not sure how to get the right context, or if I do have to bother.

  4. Does the way the cleanup method punts off code that just consumes the rest of the process' output and waits for it to die make sense? Is there a better way to express "I don't really care what happens to this process but I don't want to keep it hanging around?" Obviously this depends on what said process really does, but assume for now that once it stops getting input, it will at some point in the future cease producing output that's not interesting to the calling code. (To avoid bloating this question more, I'm not expressly concerned with handling the process returning a nonzero exit code or handling its stderr.)

Some of these might be better suited for SO proper than CR.SE, so let's say the general question I have is "am I doing async..await right here?"

\$\endgroup\$
  • \$\begingroup\$ I'd suggest starting here: blog.stephencleary.com/2012/07/dont-block-on-async-code.html which explains the use of ConfigureAwait(false) nicely. You're also conflating threads with async and await so I'd suggest this one too: blog.stephencleary.com/2013/11/there-is-no-thread.html - In fact, read Stephen's entire blog because it's excellent. \$\endgroup\$ – RobH Jul 27 '18 at 8:22
  • \$\begingroup\$ @RobH I do have that sort material on my reading stack, I just figured an illustration on code I've already reasoned about would be helpful. Thanks for the links though, I haven't seen those so I'll look into it \$\endgroup\$ – millimoose Jul 27 '18 at 8:35
  • \$\begingroup\$ @RobH re: the mention of threads, I do know that async/await is actually more about rearranging asynchronous code to run on a single event-based thread. That said, I figured that in this specific case, the use of Task.Run()brings threads back into the mix; and that in situations when that's the case, such as doing background work from the UI thread, the mechanism that underlies the feature can take care so the continuation of an await is executed on a specific desired thread. (I.e. the UI thread again.) And that ConfigureAwait() has something to do with whether this happens or not. \$\endgroup\$ – millimoose Jul 27 '18 at 8:42
  • 1
    \$\begingroup\$ Having written that I'm now actually realizing that merely starting up a process might not actually cause a significant delay in the calling process, and that the Task.Run() is completely pointless here if it does not. \$\endgroup\$ – millimoose Jul 27 '18 at 8:43

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.