0

Suppose I have multiple (say, two) processes that I want to run sequentially but asynchronously, how do I go about doing this? See snippet below:

public virtual Task<bool> ExecuteAsync()
{
    var tcs = new TaskCompletionSource<bool>();
    string exe1 = Spec.GetExecutablePath1();
    string exe2 = Spec.GetExecutablePath2();
    string args1 = string.Format("--input1={0} --input2={1}", Input1, Input2);
    string args2 = string.Format("--input1={0} --input2={1}", Input1, Input2);

    try
    {
        var process1 = new Process
        {
            EnableRaisingEvents = true,
            StartInfo =
            {
                UseShellExecute = false,
                FileName = exe1,
                Arguments = args1,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                WorkingDir = CaseDir
            }
        };
        var process2 = new Process
        {
            EnableRaisingEvents = true,
            StartInfo =
            {
                UseShellExecute = false,
                FileName = exe2,
                Arguments = args2,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                WorkingDir = CaseDir
            }
        };
        process1.Exited += (sender, arguments) =>
        {
            if (process1.ExitCode != 0)
            {
                string errorMessage = process1.StandardError.ReadToEndAsync();
                tcs.SetResult(false);
                tcs.SetException(new InvalidOperationException("The process1 did not exit correctly. Error message: " + errorMessage));
            }
            else
            {
                File.WriteAllText(LogFile, process1.StandardOutput.ReadToEnd());
                tcs.SetResult(true);
            }
            process1.Dispose();
        };
        process1.Start();

        process2.Exited += (sender, arguments) =>
        {
            if (process2.ExitCode != 0)
            {
                string errorMessage = process2.StandardError.ReadToEndAsync();
                tcs.SetResult(false);
                tcs.SetException(new InvalidOperationException("The process2 did not exit correctly. Error message: " + errorMessage));
            }
            else
            {
                File.WriteAllText(LogFile, process2.StandardOutput.ReadToEnd());
                tcs.SetResult(true);
            }
            process2.Dispose();
        };
        process2.Start();
    }
    catch (Exception e)
    {
        Logger.InfoOutputWindow(e.Message);
        tcs.SetResult(false);
        return tcs.Task;
    }

    return tcs.Task;
}

}

Thanks in advance for your ideas.

EDIT #1:

When I run the code as shown above it fails with the error below after successfully completing the first process:

"System.InvalidOperationException; An attempt was made to transition a task to a final state when it had already completed".

When I run the code with just the first process (delete all code pertaining to the other), it runs OK.

squashed.bugaboo
  • 1,338
  • 2
  • 20
  • 36
  • "*run sequentially but asynchronously*" is meaningless. what is the problem? – Amit Jun 16 '16 at 21:45
  • sequentially and asynchronously are opposites...you mean sequential (and synchronous) to each other, but asynchronous from the original calling thread? What happens when you run the code above, and how is it different from what you expect? – Tim Copenhaver Jun 16 '16 at 21:47
  • @TimCopenhaver: It is as you said. I'll test and report back shortly. Thanks – squashed.bugaboo Jun 16 '16 at 21:55

1 Answers1

2

The error you're seeing is unrelated to the two processes. It's caused by setting the SetResult method of the TaskCompletionSource multiple times. You can't give the same task two different results.

There are a few important changes:

  1. Don't set the task completion to true just because the first process succeeds. You want it to wait until the second process is done to decide if it's really a success. Remove the call to 'tsc.SetResult(true)' in process1.Exited.
  2. You should only start process2 after process1 is complete (you said you wanted them to be synchronous. To do that, start process2 in process1's Exited handler. Also, I assume you don't want to start process2 if process1 fails
  3. Move the process2.Exited handler to up above where you call process1.Start(). This just avoids a race condition when process1 and process2 complete really quickly before the Exited handler for process2 is set up.

This is untested, but your code should end up looking something like this:

public virtual Task<bool> ExecuteAsync()
{
    var tcs = new TaskCompletionSource<bool>();
    string exe1 = Spec.GetExecutablePath1();
    string exe2 = Spec.GetExecutablePath2();
    string args1 = string.Format("--input1={0} --input2={1}", Input1, Input2);
    string args2 = string.Format("--input1={0} --input2={1}", Input1, Input2);

    try
    {
        var process1 = new Process
        {
            EnableRaisingEvents = true,
            StartInfo =
            {
                UseShellExecute = false,
                FileName = exe1,
                Arguments = args1,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                WorkingDir = CaseDir
            }
        };
        var process2 = new Process
        {
            EnableRaisingEvents = true,
            StartInfo =
            {
                UseShellExecute = false,
                FileName = exe2,
                Arguments = args2,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                WorkingDir = CaseDir
            }
        };
        process1.Exited += (sender, arguments) =>
        {
            if (process1.ExitCode != 0)
            {
                string errorMessage = process1.StandardError.ReadToEndAsync();
                tcs.SetResult(false);
                tcs.SetException(new InvalidOperationException("The process1 did not exit correctly. Error message: " + errorMessage));
            }
            else
            {
                File.WriteAllText(LogFile, process1.StandardOutput.ReadToEnd());
                process2.Start();
            }
            process1.Dispose();
        };

        process2.Exited += (sender, arguments) =>
        {
            if (process2.ExitCode != 0)
            {
                string errorMessage = process2.StandardError.ReadToEndAsync();
                tcs.SetResult(false);
                tcs.SetException(new InvalidOperationException("The process2 did not exit correctly. Error message: " + errorMessage));
            }
            else
            {
                File.WriteAllText(LogFile, process2.StandardOutput.ReadToEnd());
                tcs.SetResult(true);
            }
            process2.Dispose();
        };

        process1.Start();
    }
    catch (Exception e)
    {
        Logger.InfoOutputWindow(e.Message);
        tcs.SetResult(false);
        return tcs.Task;
    }

    return tcs.Task;
}
Tim Copenhaver
  • 3,282
  • 13
  • 18
  • Tested - it works. Also, your explanation makes complete sense. Thanks! – squashed.bugaboo Jun 17 '16 at 04:26
  • I would like a follow-up clarification if possible. Can you confirm that the two processes get executed in their own dedicated thread, and at all times (until both complete successfully) asynchronous from the calling thread, correct? In other words, in between (e.g. at the end of the process1) it still stays asynchronous from calling thread? – squashed.bugaboo Jun 17 '16 at 14:34
  • Correct, the Exited handler is called asynchronously and waiting for process2 is also handled asynchronously. It's important to pick the correct terms though - asynchronous doesn't necessarily mean on a different thread. – Tim Copenhaver Jun 17 '16 at 20:30
  • OK, but in this particular example, the call to new TaskCompletionSource() results in the processes being executed on a separate thread from the calling thread, correct? If not, how to distinguish when it runs on a different thread from when it doesn't? – squashed.bugaboo Jun 17 '16 at 21:06
  • Not exactly. A new *task* doesn't necessarily correlate to a new *thread*. Typically you don't care if you're really on a new thread or not, you just care that it's asynchronous. See [this answer](http://stackoverflow.com/questions/600795/asynchronous-vs-multithreading-is-there-a-difference) for some discussion on that - it can be a confusing topic on its own. – Tim Copenhaver Jun 17 '16 at 21:54