问题
I have a follow-up question to this post. In my version, I have the following I want to make asynchronous. Here is what I have:
public virtual Task<bool> ExecuteAsync()
{
var tcs = new TaskCompletionSource<bool>();
string exe = Spec.GetExecutablePath();
string args = string.Format("--input1={0} --input2={1}", Input1, Input2);
try
{
var process = new Process
{
EnableRaisingEvents = true,
StartInfo =
{
UseShellExecute = false,
FileName = exe,
Arguments = args,
RedirectStandardOutput = true,
RedirectStandardError = true,
WorkingDir = CaseDir
}
};
process.Exited += (sender, arguments) =>
{
if (process.ExitCode != 0)
{
string errorMessage = process.StandardError.ReadToEndAsync();
tcs.SetResult(false);
tcs.SetException(new InvalidOperationException("The process did not exit correctly. Error message: " + errorMessage));
}
else
{
File.WriteAllText(LogFile, process.StandardOutput.ReadToEnd());
tcs.SetResult(true);
}
process.Dispose();
};
process.Start();
}
catch (Exception e)
{
Logger.InfoOutputWindow(e.Message);
tcs.SetResult(false);
return tcs.Task;
}
return tcs.Task;
}
}
Here Spec, Input1, Input2, CaseDir, LogFile are all members of the class of which the ExecuteAsync is a method. Is that OK to use them like that? The parts I am struggling with are:
- I can't seem to use the async keyword at the method definition (
public virtual async Task<bool> ExecuteAsync()) without a warning that I need anawaitkeyword, whereas I do have one in the lambda expression for the process. Do I even need the async keyword at the method definition? I've seen supposedly asynchronous examples where they don't use it, e.g. this one. If I take it out it compiles, but can I use this asynchronously? - Is my usage of the async keyword in the lambda expression and the corresponding
await process.StandardError.ReadToEndAsync()OK inside the process lambda expression? In this example, they don't useasync awaitat the corresponding line, so I wonder how they got away with it? Wouldn't leaving it out make it blocking, since I was told that the methodReadToEndis blocking? - Is my call to
File.WriteAllText(LogFile, process.StandardOutput.ReadToEnd())going to render the whole method blocking? If so, how can I avoid that, if at all possible? - Does the exception handling make sense? Should I be aware of any details of the application logger method
Logger.InfoOutputWindowwhich I've used in thecatchblock? - Finally, why does the
process.Exitedevent always appear before theprocess.Start()in all the examples I've come across? Can I put theprocess.Start()before theprocess.Exitedevent?
Appreciate any ideas and thanks in advance for your interest & attention.
EDIT #1:
For #3 above, I had an idea, based in part on comment from @René Vogt below, so I made a change to move the File.WriteAllText(...) call inside the else {} block of the process.Exited event. Perhaps this addresses #3.
EDIT #2:
I made the initial list of changes (code snippet is changed now), basically removed both the async keyword at function definition and the await keyword in the process.Exited event handler based on original comments from @René Vogt. Haven't yet tried his most recent changes below. When I run, I get an exception:
A plugin has triggered error: System.InvalidOperationException; An attempt was made to transition a task to a final state when it had already completed.
The application log has call stack as follows:
UNHANDLED EXCEPTION:
Exception Type: CLR Exception (v4)
Exception Details: No message (.net exception object not captured)
Exception Handler: Unhandled exception filter
Exception Thread: Unnamed thread (id 29560)
Report Number: 0
Report ID: {d80f5824-ab11-4626-930a-7bb57ab22a87}
Native stack:
KERNELBASE.dll+0x1A06D RaiseException+0x3D
clr.dll+0x155294
clr.dll+0x15508E
<unknown/managed> (0x000007FE99B92E24)
<unknown/managed> (0x000000001AC86B00)
Managed stack:
at System.Threading.Tasks.TaskCompletionSource`1.SetException(Exception exception)
at <namespace>.<MyClass>.<>c__DisplayClass3.<ExecuteAsync>b__2(Object sender, EventArgs arguments)
at System.Diagnostics.Process.RaiseOnExited()
at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
at System.Threading._ThreadPoolWaitOrTimerCallback.PerformWaitOrTimerCallback(Object state, Boolean timedOut)
回答1:
You don't need the
asyncon your method signature, because you don't useawait. It's enough to return aTask. The caller mayawaitthatTask- or not, it has nothing to do with your method.Don't use the
asynckeyword on that lambda and don't use the asynchronousReadToEndinside that lambda. It's hard to predict what happens if you return from that event handler before it's really finished. And anyway you want to finish that method. It's called when the process exited, there is no need to make thisasync.Here it's the same as in (2). I think it's ok to do this "synchronously" inside this event handler. It will only block this handler, but the handler is called after the process has exited, so I guess it's ok for you.
Your exception handling looks OK, but I would add another
try/catchblock inside theExitedevent handler. But that's not based on knowledge, rather on experience that everywhere something can go wrong :)
For a better way to get the standard and error output, I suggest to subscribe to the ErrorDataReceived and OutputDataReceived events and fill StringBuilders with the received data.
In your method, declare two StringBuilders:
StringBuilder outputBuilder = new StringBuilder();
StringBuilder errorBuilder = new StringBuilder();
And subscribe to the events right after you instantiated process:
process.OutputDataReceived += (sender, e) => outputBuilder.AppendLine(e.Data);
process.ErrorDataReceived += (sender, e) => errorBuilder.AppendLine(e.Data);
Then you only need to call these two methods right after you called process.Start() (it won't work before, because the stdout and stderr are not yet opened):
process.Start();
process.BeginErrorReadLine();
process.BeginOutputReadLine();
In your Exited event handler you can then call outputBuilder.ToString() (or errorBuilder.ToString() respectively) instead of ReadToEnd and everything should work fine.
Unfortunatly there is a drawback, too: if the process is very very fast, your Exited handler may theoritically be called before those Begin*ReadLine calls. Don't know how to handle that, but it's not very likely to happen.
来源:https://stackoverflow.com/questions/37349973/asynchronous-method-in-a-c-sharp-class-that-executes-a-process