5

Vorrei chiudere la seguente operazione di socket datagramma con TPL per ripulire l'API in modo che funzioni correttamente con async e await, proprio come fa la classe StreamSocket.Come si adatta DatagramSocket.MessageReceived per l'uso con async/await?

public static async Task<bool> TestAsync(HostName hostName, string serviceName, byte[] data) 
{ 
    var tcs = new TaskCompletionSource<bool>(); 
    var socket = new DatagramSocket(); 
    socket.MessageReceived += (sender, e) => 
    { 
     var status = false; // Status value somehow derived from e etc. 
     tcs.SetResult(status); 
    }; 
    await socket.ConnectAsync(hostName, serviceName); 
    var stream = await socket.GetOutputStreamAsync(); 
    var writer = new DataWriter(stream); 
    writer.WriteBytes(data); 
    await writer.StoreAsync(); 
    return tcs.Task; 
} 

Il punto critico è l'evento MessageReceived che trasforma la classe DatagramSocket in uno strano miscuglio di modello asincrono evento e la nuova async modello. Ad ogni modo, TaskCompletionSource<T> mi consente di adattare il gestore per conformarsi a quest'ultimo, quindi non è troppo spaventoso.

Questo sembra funzionare piuttosto bene a meno che l'endpoint non restituisca mai alcun dato. L'attività associata al gestore MessageReceived non viene mai completata e pertanto l'attività restituita da TestAsync non viene mai completata.

Qual è il modo corretto di avvolgere questa operazione per incorporare un timeout e una cancellazione? Vorrei estendere questa funzione per prendere un argomento CancellationToken per quest'ultimo, ma cosa devo fare con esso? L'unica cosa che mi è venuta in mente è quello di creare un compito aggiuntivo "monitoraggio" utilizzando Task.Delay a cui passare un valore di timeout e il token cancellazione per sostenere questi due comportamenti secondo le seguenti linee:

public static async Task<bool> CancellableTimeoutableTestAsync(HostName hostName, string serviceName, byte[] data, CancellationToken userToken, int timeout) 
{ 
    var tcs = new TaskCompletionSource<bool>(); 
    var socket = new DatagramSocket(); 
    socket.MessageReceived += (sender, e) => 
    { 
     var status = false; // Status value somehow derived from e etc. 
     tcs.SetResult(status); 
    }; 
    await socket.ConnectAsync(hostName, serviceName); 
    var stream = await socket.GetOutputStreamAsync(); 
    var writer = new DataWriter(stream); 
    writer.WriteBytes(data); 
    await writer.StoreAsync(); 

    var delayTask = Task.Delay(timeout, userToken); 
    var t1 = delayTask.ContinueWith(t => { /* Do something to tcs to indicate timeout */ }, TaskContinuationOptions.OnlyOnRanToCompletion); 
    var t2 = delayTask.ContinueWith(t => { tcs.SetCanceled(); }, TaskContinuationOptions.OnlyOnCanceled); 

    return tcs.Task; 
} 

Tuttavia, questo ha tutti i tipi di problemi tra cui potenziali condizioni di gara tra l'attività di ritardo e il gestore MessageReceived. Non sono mai stato in grado di far funzionare questo approccio in modo affidabile, inoltre sembra ridicolmente complicato e un uso inefficiente del pool di thread. È laborioso, soggetto a errori e fa male alla testa.

Nota a margine: sono l'unica persona confusa dall'API DatagramSocket in generale? Non solo sembra essere un brutto conglomerato del modello IAsyncAction WinRT e TPL con qualche EAP complicato, non mi sento molto a mio agio con un'API che intende rappresentare un protocollo fondamentalmente senza connessione come i metodi UDP che contengono il nome ConnectAsync in loro. Questa sembra essere una contraddizione in termini per me.

+0

Invece del secondo metodo, creare un nuovo metodo che combina l'attività dal primo con Task.Delay e utilizzi Task.WhenAny. Una volta restituito, l'operazione o il timeout si sono completati. –

risposta

2

In primo luogo, penso che l'interfaccia di DatagramSocket abbia senso proprio a causa della natura di UDP. Se si dispone di un flusso di datagrammi, quindi un evento è un modo appropriato per rappresentarlo. WinRT IAsyncAction (o .Net Task) può rappresentare solo il modello di pull, in cui si richiede esplicitamente ogni parte di dati (ad esempio, potrebbe esistere un metodo ReadNextDatagramAsync()). Questo ha senso per TCP, perché ha il controllo del flusso, quindi se leggi i dati lentamente, il mittente li invierà lentamente. Ma per UDP, il modello push (rappresentato da un evento in WinRT e .Net) ha molto più senso.

E sono d'accordo che il nome Connect non ha il 100% senso, ma penso che la maggior parte fa ha un senso, in particolare per renderlo più coerente con StreamSocket. E hai bisogno di un metodo come questo, in modo che il sistema possa risolvere il nome di dominio e assegnare una porta al tuo socket.

Per il tuo metodo, sono d'accordo con @usr che dovresti creare un metodo separato per ricevere il datagramma. E se vuoi convertire un modello asincrono in un altro, mentre aggiungere funzionalità che il modello originale non supporta in modo nativo, sarà piuttosto complicato, penso che non ci sia nulla che tu possa fare al riguardo.

Inoltre, non sarà inefficiente, se si implementa in modo corretto: è necessario assicurarsi che dopo la Task ha completato, l'evento è MessageReceived sottoscritte, il timer associato con Delay() è disposto (nota bene che annullando il token sei passato a Delay()) e il delegato che è stato registrato con il passato CancellationToken non è registrato (penso che dovresti usare Register() direttamente invece di (ab) usando Delay() per quello).

Per quanto riguarda le condizioni di gara, ovviamente è necessario pensarci. Ma esiste un modo relativamente semplice per gestirlo qui: utilizzare i metodi Try di TaskCompletionSource (ad esempio TrySetResult()).

0

Timeout: avviare un timer e utilizzare tcs.TrySetCancelled() per completare l'attività. Per la cancellazione utilizzare cancellationToken.Register per registrare una richiamata in cui si è impostato anche su annullato. Fare attenzione a smaltire sempre il timer.

Suggerisco di spostare la logica del timer in un metodo di supporto riutilizzabile. Ciò evita che il codice assomigli agli spaghetti con un sacco di roba non correlata.