È una cattiva pratica che le mie librerie interrompano i metodi lanciando qualcosa di diverso da un OperationCancelledException
quando viene rilevato CancellationToken.IsCancelRequested
?È una cattiva pratica lanciare un'eccezione arbitraria quando si imposta CancellationToken?
Ad esempio:
async Task<TcpClient> ConnectAsync(string host, int port, CancellationToken ct)
{
var client = new TcpClient();
try
{
using (ct.Register(client.Close, true))
{
await client.ConnectAsync(host, port);
}
// Pick up strugglers here because ct.Register() may have hosed our client
ct.ThrowIfCancellationRequested();
}
catch (Exception)
{
client.Close();
throw;
}
return client;
}
All'annullamento, questo ha la possibilità di gettare ObjectDisposedException
o NullReferenceException
seconda dei tempi. (A causa TcpClient.ConnectAsync()
può lanciare uno dei due quando TcpClient.Close()
viene chiamato contemporaneamente.)
Ora, potrei risolvere questo modo:
async Task<TcpClient> ConnectAsync(string host, int port, CancellationToken ct)
{
var client = new TcpClient();
try
{
using (ct.Register(client.Close, true))
{
try
{
await client.ConnectAsync(host, port);
}
catch (Exception)
{
// These exceptions are likely because we closed the
// connection with ct.Register(). Convert them to
// OperationCancelledException if that's the case
ct.ThrowIfCancellationRequested();
throw;
}
}
// Pick up strugglers here because ct.Register() may have hosed our client
ct.ThrowIfCancellationRequested();
}
catch (Exception)
{
client.Close();
throw;
}
return client;
}
E allo stesso modo in ogni strato della gerarchia di richiamo, se del caso:
async Task<TcpClient> ConnectSslStreamAsync(string host, int port, CancellationToken ct)
{
var client = await ConnectAsync(host, port, ct);
try
{
ct.ThrowIfCancellationRequested();
var sslStream = new SslStream(client.getStream());
using (ct.Register(sslStream.Close))
{
try
{
await sslStream.AuthenticateAsClientAsync(...);
}
catch (Exception)
{
// These exceptions are likely because we closed the
// stream with ct.Register(). Convert them to
// OperationCancelledException if that's the case
ct.ThrowIfCancellationRequested();
throw;
}
}
// Pick up strugglers here because ct.Register() may have hosed our stream
ct.ThrowIfCancellationRequested();
}
catch (Exception)
{
client.Close();
throw;
}
return client;
}
Ma questo aggiunge questo codice aggiuntivo quando, in realtà, tutto ciò che è richiesto è un controllo al livello più esterno. (Alla fonte di cancellazione.)
È una cattiva pratica lasciare volare queste eccezioni arbitrarie? O li stanno ignorando al più esterno chiamante convenzionale?
Quando Microsoft aggiunge esplicitamente un metodo che genera un'eccezione, no, "cattiva pratica" non è la prima cosa che salta alla mente. Solo fingere che non sia stato gettato è una cattiva pratica. –
Non metterei nemmeno 'ct.ThrowIfCancellationRequested()' alla fine di 'ConnectAsync'. Cosa succede se il token viene cancellato subito dopo questa chiamata ma prima che 'ConnectAsync' ritorni? Preferisco lasciare che il cliente della biblioteca si occupi di varie eccezioni di corsa e documenterebbe solo i possibili effetti collaterali. – Noseratio
@Noseratio Quel 'ct.ThrowIfCancellationRequested()' è lì perché senza di esso, c'è una corsa sottile dove 'using (ct.Register (...))' può chiudere la connessione aperta anche se il nostro 'ConnectAsync()' continua riuscire. Potrebbe non fare la differenza nel nostro esempio (e il chiamante probabilmente eliminerà comunque la connessione), ma quella struttura è probabilmente la pena tenuta in atto come regola empirica, per preservare l'integrità (atomicità) nei casi in cui i nostri metodi vanno avanti cambiare stato. – antak