Laden...

Coderedundanz bei Threadpooltask und Cancelation (async/await)

Erstellt von BlackMatrix vor 9 Jahren Letzter Beitrag vor 9 Jahren 3.266 Views
B
BlackMatrix Themenstarter:in
218 Beiträge seit 2012
vor 9 Jahren
Coderedundanz bei Threadpooltask und Cancelation (async/await)

Frohes Neues,

ich stelle meinen Code gerade auf async/await um und habe Tasks, deren Aufbau in etwa so ausschaut:


public async Task StartAsync(CancellationToken cancellationToken)
{
            var request = // ...

            var configuration = await _client.GetConfigurationAsync(request, cancellationToken).ConfigureAwait(false);

            // Fun with configuration

            var response = await _client.SetConfigurationAsync(configuration, cancellationToken).ConfigureAwait(false);

            // Fun with response

            // ... cancellationToken).ConfigureAwait(false);
            // ... cancellationToken).ConfigureAwait(false);
}

Ich gebe also jedes Mal das CancellationToken mit, jedes Mal konfiguriere ich den Awaiter dahingehend, dass er im ThreadPool laufen darf. Das ganz bläht den Code ganz schön auf und ich frage mich, kann man das nicht irgendwie andes bewerkstelligen? Zumal mich ja sowieso nur interessiert, ob Start vollständig durchgelaufen ist oder vom Benutzer explizit abgebrochen wurde.

16.807 Beiträge seit 2008
vor 9 Jahren

In Deinem Beispiel-Code macht das asynchrone Verhalten irgendwie kein Sinn.
Das eine ist ja stehts vom anderen abhängig, sodass hier gar nichts parallel laufen kann.

async/await ist auch kein Allheilmittel. Es gibt immer noch Fälle und Situationen, wo sequentieller oder Code über TPL durchaus hübscher und leistungsfähiger ist, als mit async/await.

T
2.219 Beiträge seit 2008
vor 9 Jahren

Ohne den Code und den eigentlichen Ablauf des Ganzen zu sehen, muss ich Abt recht geben.
Dein aktueller Code muss so oder so darauf warten, dass der erste Teil durchläuft damit die andere Verarbeitung greifen kann.
Entsprechend macht ein paralleler Ablauf kaum Sinn.

T-Virus

Developer, Developer, Developer, Developer....

99 little bugs in the code, 99 little bugs. Take one down, patch it around, 117 little bugs in the code.

B
BlackMatrix Themenstarter:in
218 Beiträge seit 2012
vor 9 Jahren

Nuja, die API bietet nur die asynchronen Methoden an und das ist meiner Meinung nach auch richtig, denn diese benutzt intern die HttpClient des .NET-Frameworks, der auch nur die asynchronen mit Cancellationsupport anbietet.

16.807 Beiträge seit 2008
vor 9 Jahren

Nur weil eine Methode asynchron ist heisst das noch lange nicht, dass dahinter async/await steckt.
Asynchrone Methoden gab's schon lange vor .NET 4.5..

742 Beiträge seit 2005
vor 9 Jahren

Wieso wird jetzt Parallelität ins Spiel gebracht? Async macht hier absolut Sinn, wenn man z.B. über die ganze Anwendung optimiert. Wieso einen Thread 100ms oder so für GetConfiguration blockieren, wenn dieser in der gleichen Zeit auch was anderes machen kann?

Zurück zum Thema: Mir fallen nur zwei Möglichkeiten ein:

  1. Delegates:

public async Task<T> GetOrCreateConfigAsync<T>(Func<Request> requestCreator, Action<Configuration> configHandler, Func<Response, Task<T>> responseHandler, CancellationToken cancellationToken)
{
     var request = requestCreator();

     var configuration = await _client.GetConfigurationAsync(request, cancellationToken).ConfigureAwait(false);
     
     var confiHandler(configuration);

     var response = await _client.SetConfigurationAsync(configuration, cancellationToken).ConfigureAwait(false);

     await responseHandler(response);
}

Ein bisschen unschön, weil zu komplex.

  1. Vererbung

public abstract class ConfigReader<T>
{
      private ConfigClient client;
      
      protected ConfigReader(ConfigClient client)
      {
            this.client = client;
      }

      public async Task<T> GetOrCreateConfigAsync<T>(CancellationToken cancellationToken)
      {
              var request = CreateRequest();

              var configuration = await _client.GetConfigurationAsync(request, cancellationToken).ConfigureAwait(false);
     
              var HandleConfig(configuration);
 
              var response = await _client.SetConfigurationAsync(configuration, cancellationToken).ConfigureAwait(false);

              await HandleResponseAsync(response);
      }

      protected abstract Request CreateRequest();
      
      protected abstract void HandleConfig(Configuration config);

      protected abstract Task<T> HandleResponseAsync(Response response);
}

Du kannst das noch aufblähen mit allen möglichen Pattern wie Strategy / Mediator, aber ich denke der letzte Ansatz könnte reichen.

16.807 Beiträge seit 2008
vor 9 Jahren

Wieso wird jetzt Parallelität ins Spiel gebracht? Async macht hier absolut Sinn, wenn man z.B. über die ganze Anwendung optimiert. Wieso einen Thread 100ms oder so für GetConfiguration blockieren, wenn dieser in der gleichen Zeit auch was anderes machen kann?

Der Sinn hinter Async/Await ist das vereinfachte Auslagern von Aufgaben in Tasks. Tasks wiederum sind nichts anderes als abstrakte Container um Parallelität umzusetzen.
Man kann komplett auf die Schlüsselwörter verzichten, um die identische Ausführung umzusetzen. Und es gibt durchaus Situationen, wo das Verzichten auf diese Schlüsselwörter den Code schlanker und übersichtlicher machen.

Du kannst A und B nur dann zeitgleich laufen lassen, wenn sie keine Abhängigkeiten haben.
Benötigt B das Resultat von A, was in diesem Beispiel der Falll ist, dann funktioniert das nicht.

Vielleicht sieht der echte Code anders aus, aber wieso muss in so einem Fall async/await verwendet werden?
Das ist eigentlich das Paradebeispiel, dass es durchaus auch sinnvolle Anwendungsfälle für das alte Task-Schema / Task-Run gibt und man auf die Schlüsselwörter verzichten kann.

In Deinem Beispiel, malignate, hast Du wenigstens noch gezeigt, dass man mit dem Resultat an für sich parallel arbeiten kann


// Resultat holen
var configuration = await _client.GetConfigurationAsync(request, cancellationToken).ConfigureAwait(false);

// Und zeitgleich damit arbeiten
 var HandleConfig(configuration);

var response = await _client.SetConfigurationAsync(configuration, cancellationToken).ConfigureAwait(false);

Ich bin mir aber nicht wirklich so sicher, dass der Echtcode dies verfolgt.
Es kann gut sein, dass dies das derzeit leider typische, zwanghafte Umsetzen von async/await ist, obwohl es durchaus einfacher gehen könnte; ich will das gewiss nicht negativ unterstellen -aber wir sollten uns vielleicht die echte Umsetzung anhören, bevor es nachher gar nicht passt.

B
BlackMatrix Themenstarter:in
218 Beiträge seit 2012
vor 9 Jahren

@Abt:

Habe ich etwas verpasst oder warum sollte HandleConfig gleichzeitig die Konfiguration behandeln? Das geschieht doch erst, nachdem die configuration geholt wurde.

Mir geht es halt darum, dass ich an jede asynchrone Methode den Token mitgebe und den Awaiter konfugiere, immer gleich und sehr häufig in den Methoden.

Der Originalcode sieht in etwa so aus. Wie gesagt, es interessiert mich eigentlich nur, ob der Code komplett durchgelaufen ist oder er abgebrochen wurde. Vor der Umstellung auf async/await war das Ganze nur in einen Task.Run mit synchronem WebRequest/WebResponse gekapselt und mittels Abort() abgebrochen. Nun wird jedes Mal das Await konfiguriert und das CancellationToken mitgegeben, was zu vielen Codedopplungen führt.


	class Program
	{
		private static readonly CancellationTokenSource _cts = new CancellationTokenSource();
		static void Main(string[] args)
		{
			try
			{
				StartAsync().Wait();
			}
			catch (TaskCanceledException)
			{
				// ...
			}
			catch (Exception)
			{
				// ...
			}
		}


		private static async Task StartAsync()
		{
			var client = new HttpClient();
			var response = await client.GetAsync("http://www.example.net/",_cts.Token).ConfigureAwait(false); // First access

			if (response.StatusCode == HttpStatusCode.OK)
			{
				// ...
			}
			else if (response.StatusCode == HttpStatusCode.ExpectationFailed)
			{
				// ...
			}

			var message = await client.PostAsync("http://www.example.net/", new StringContent("example=true"), _cts.Token).ConfigureAwait(false);

			var parser = new JsonParser();
			var json = parser.Parse(await message.Content.ReadAsStringAsync());

			if (!json.Successful)
			{
				throw new Exception();
			}

			response = await client.GetAsync("http://www.example.net/" + json.PathAndQuery, _cts.Token).ConfigureAwait(false);;

			// ...
		}
	}
16.807 Beiträge seit 2008
vor 9 Jahren

Man kann bei Async nicht davon ausgehen, dass etwas parallel verarbeitet wird - man kann aber - per default - auch nicht davon ausgehen, dass es _nicht _parallel läuft.
Das kannst Du eigentlich nur, wenn Du Elemente in verschiedenen Async-Operationen zeitgleich verwendest.

Insgesamt sehe ich bei den Code immer noch nicht den Sinn von async/await.
Das wäre für mich ein Fall, dass ich eine Methode ganz normal ohne Tasks umsetze und eine zweite Methode erstelle, die die erste Methode async-mäßig aufruft.

public void Start ()
{
   // ...
}

public Task StartAsync( )
{
 return AsyncAll.ExecuteAsync( () => Start( ))
}

Siehe dazu AsyncAll

Beim Cancel kommt es darauf an, wie Du es umsetzen willst. Das geht sowohl über Deine Tokens, wie aber auch über ein Flag, sodass es auch via Start() verfügbar wäre.

Ich hoffe

try
            {
                StartAsync().Wait();
            }
            catch (TaskCanceledException)
            {
                // ...
            }
            catch (Exception)
            {
                // ...
            }

ist nur ein Test, ansonsten kann man sich wirklich das async-Gedöhns sparen...

// Edit: mein Text war viel zu kompliziert ausgedrückt, ich mach ihn neu..

B
BlackMatrix Themenstarter:in
218 Beiträge seit 2012
vor 9 Jahren

Puh, das haut mich ja jetzt um. Ich bin bisher immer davon ausgegangen, dass der Code nach dem await immer erst dann ausgeführt wird, wenn der Task abgeschlossen ist.

Das heißt:


            var client = new HttpClient();

            var response = await client.GetAsync("http://www.example.net/",_cts.Token).ConfigureAwait(false); // First access

            if (response.StatusCode == HttpStatusCode.OK)
            {
               var message = await client.PostAsync("http://www.example.net/", new StringContent("example=true"), _cts.Token).ConfigureAwait(false);
            }

würde definitiv nacheinander ausgeführt werden, aber bei


            var client = new HttpClient();

            var response = await client.GetAsync("http://www.example.net/",_cts.Token).ConfigureAwait(false); // First access

            if (response.StatusCode == HttpStatusCode.OK)
            {
            			//...
            }

            var message = await client.PostAsync("http://www.example.net/", new StringContent("example=true"), _cts.Token).ConfigureAwait(false);

muss das nicht der Fall sein, selbst dann nicht, wenn mir der erste Request Cookies liefert, die ich beispielsweise für den 2. Request brauche bzw. den Request erst möglich machen?

Also

var client = new HttpClient();

durch

var client = new HttpClient(new HttpClientHandler{CookieContainer = new CookieContainer()});

ersetze?

16.807 Beiträge seit 2008
vor 9 Jahren

Das await blockt i.d.R. schon an dieser Stelle; aber async alleine ist kein Garant dafür.

Ich hab deswegen das CodeBeispiel entfernt, weil das wahrscheinlich doch eher verwirrt als hilft.

742 Beiträge seit 2005
vor 9 Jahren

Abt, ich verstehe überhaupt nicht worauf du hinauswillst. Async-await ist eine Art besseres Delegate-Prinzip. Eine externe Resource (ein Task, ein Thread, das Betriebssystem, eine Festplatte) wird aufgerufen, damit irgendwas durchgeführt wird und diese Resource meldet dann, wann sie fertig ist. Das ist doch ein gutes Beispiel für Async-await. Natürlich ist es vollkommen unerheblich, ob man jetzt in einer Konsolenanwendung ContinueWith verwendet oder normale Delegates oder async-await, aber warum sollte man es nicht verwenden???

Also ich verstehe die ganze Diskussion hier nicht.

R
228 Beiträge seit 2013
vor 9 Jahren

Hallo,

ich hoffe es ist ok wenn ich hier nochmal einhaken.

Ist es wirklich ein "no-go" solche Funktionen zu schreiben, bzw. Async-Await zu verwenden, obwohl nichts parallel abläuft?

Zum verdeutlichen ein sinnfreies Beispiel:


async Task<int> AccessTheWebAsync()
{ 
    HttpClient client = new HttpClient();

    string bla =  await client.GetStringAsync("http://msdn.microsoft.com");

    return bla.Length;
}

Sinn wäre jetzt einfach nur die GUI nicht zu blockieren, weil ich die Methode mit await aufrufen könnte.

Besser gefragt: Wie Programmiert ihr Methoden die die Gui nicht blockieren sollen und Async Methoden bereitstellen? Synchron und dann einen Task beim Methodenaufruf drum rum, oder eine Methode mit dem Namen AccessTheWebAsync wo die eigentliche Methode AccessTheWeb per return Task.Run(() => AccessTheWeb()); aufgerufen wird?

Vielen Dank

T
415 Beiträge seit 2007
vor 9 Jahren

@Rioma

Doch es ist durchaus möglich async / await dafür zu verwenden, um die GUI nicht blockieren zu lassen. Siehe dazu gfoidl's Beitrag unter [FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke)

Dort findest du auch alle anderen Möglichkeiten, um eine GUI nicht blockieren zu lassen.

49.485 Beiträge seit 2005
vor 9 Jahren

Hallo Rioma,

Wie Programmiert ihr Methoden die die Gui nicht blockieren sollen und Async Methoden bereitstellen?

für beide deiner Vorschläge gibt es gute Gründe. Ist beides möglich, zielführend und ok. Und es gibt noch viele weitere Möglichkeiten. Aber grundsätzlich ist das Nicht-Blockieren des GUIs anderes Thema, das in [FAQ] Warum blockiert mein GUI? und [FAQ] Controls von Thread aktualisieren lassen (Control.Invoke/Dispatcher.Invoke) ausführlich besprochen wurde (auch für den async-await-Fall) und nicht hierher gehört.

Hallo zusammen,

die Diskussion der Sinnhaftigkeit kann ich auch nicht ganz nachvollziehen. Wenn mir eine Bibliothek lauter Async-Methoden anbietet, die ich aber aus irgendwelchen Gründen im konkreten Anwendungsfall nacheinander aufrufen will oder muss, dann ist es natürlich ok, in einer Methode M zu schreiben, die alle dieses Aufrufe tätigt und dabei jedes Mal await benutzt. Ich sehe es also so wie malignate. Natürlich könnte man auch die jeweils nächste Methode explizit als Continuation der vorherigen ausführen. Aber durch die Compiler-Magic von async-await passiert ja bei der Benutzung der beschriebenen Methode M letztlich genau das. Ich sehe also wirklich keinen Grund, der dagegen spricht.

Dies im Sinn, sollten wir versuchen, zum eigentlichen Thema, nämlich der Vermeidung der Coderedundanzen zurückzukommen (sofern dieser Punkt nicht ohnehin schon beantwortet ist).

herbivore