Begininvoke Paired With Endinvoke
ID |
csharp.begininvoke_paired_with_endinvoke |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
low |
Remediation Effort |
medium |
Resource |
Code Smell |
Language |
CSharp |
Tags |
async, code_smell, delegate, resource-leak |
Description
Reports a delegate BeginInvoke that no EndInvoke completes, in the two cases that can be decided at
the call site: the callback argument is null and no EndInvoke appears in the enclosing method, or the
callback is written inline as a one-parameter lambda or anonymous method whose body never calls
EndInvoke.
A callback passed as a method group names a body this rule cannot see, and is not reported. Neither is
the unrelated BeginInvoke that marshals a call onto a UI thread, whose argument list does not have the
callback, state tail of a delegate.
Rationale
BeginInvoke starts the call on a pool thread and returns an IAsyncResult that owns real resources — a
wait handle among them. The asynchronous call is not finished until EndInvoke is called on that result.
EndInvoke is what releases those resources, what returns the value along with any out and ref
arguments, and what rethrows an exception raised inside the delegate. Skip it and the exception is
swallowed with no trace anywhere, the result is never collected, and the handle is held until
finalization. The contract for this pattern is that each BeginInvoke is matched by exactly one
EndInvoke, with no exception for a call whose result nobody wants.
using System;
public delegate int Compute(int value);
public class Runner
{
private Compute compute;
public void FireAndForget()
{
compute.BeginInvoke(1, null, null); // FLAW, nothing ever ends the call
}
public void WithCallback()
{
compute.BeginInvoke(1, ar => Log("done"), null); // FLAW, the callback does not end it
}
public void Blocking()
{
IAsyncResult result = compute.BeginInvoke(1, null, null); // OK, ended below
int value = compute.EndInvoke(result);
Log(value.ToString());
}
public void Completed()
{
compute.BeginInvoke(1, ar => compute.EndInvoke(ar), null); // OK, ended in the callback
}
private void Log(string message) { }
}
Remediation
Call EndInvoke exactly once for every BeginInvoke, on the IAsyncResult that BeginInvoke returned:
inside the callback when there is one, otherwise at the point where the result is needed.
Prefer not to write the pattern at all in new code. A task-returning method awaited by the caller expresses the same intent, propagates exceptions where they can be caught, and has nothing to pair up.