2016-05-31 9 views
5

È 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?

+1

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. –

+1

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

+0

@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

risposta

2

Si consiglia di inviare OperationCancelledException in caso di cancellazione. Quando i consumatori del tuo codice ottengono delle eccezioni, non avranno la minima idea se si trattasse effettivamente di una cancellazione o qualcos'altro.

Detto questo, se non si è in grado di lanciare OperationCancelledException a causa di registrazione vicino delegato, si potrebbe provare l'approccio fornito here dove potrete creare un'attività per la chiusura del tcpClient o ruscello e verificare quale compito ha completato prima e agire di conseguenza.

+0

Non sono sicuro di cosa intendi con il tuo secondo paragrafo.La risposta che hai collegato parla della modifica di un 'OperationCancelledException' a un 'TimeoutException' quando uno usa un secondo' CancellationTokenSource' per lo scopo del timeout, e ciò è possibile confrontando il campo 'CancellationToken' dell'eccezione (come mostrato nella risposta accettata). La domanda qui è di cambiare qualche eccezione X in un 'OperationCancelledException', ma ciò non sembra giustificare l'uso di' .WithCancellation() 'quando si può semplicemente' catch (Exception) {ct.ThrowIfCancellationRequested; gettare; } 'come mostrato sopra. – antak

Problemi correlati