c # .net 4.5 asynchrone / multithread?

J’écris une application console C # qui extrait des données de pages Web.

Cette application utilisera environ 8 000 pages Web et récupérera des données (le même format de données sur chaque page).

Je l’ai en ce moment sans aucune méthode asynchrone ni multithreading.

Cependant, j’ai besoin que cela soit plus rapide. Il utilise seulement environ 3% à 6% de la CPU, je pense parce qu’il passe du temps à attendre pour télécharger le code HTML.

C’est le stream de base de mon programme

DataSet alldata; foreach(var url in the8000urls) { // ScrapeData downloads the html from the url with WebClient.DownloadSsortingng // and scrapes the data into several datatables which it returns as a dataset. DataSet dataForOnePage = ScrapeData(url); //merge each table in dataForOnePage into allData } // PushAllDataToSql(alldata); 

J’ai essayé de multi-thread cela, mais je ne suis pas sûr de savoir comment commencer correctement. J’utilise .net 4.5 et ma compréhension est asynchrone et attendre dans 4.5 est faite pour rendre cela beaucoup plus facile à programmer mais je suis toujours un peu perdu.

Mon idée était de continuer à créer de nouveaux threads asynchrones pour cette ligne

 DataSet dataForOnePage = ScrapeData(url); 

et puis comme chacun termine, exécutez

 //merge each table in dataForOnePage into allData 

Quelqu’un peut-il m’indiquer comment rendre cette ligne asynchrone dans .net 4.5 c # et ensuite exécuter ma méthode de fusion sur complete?

Je vous remercie.

Edit: Voici ma méthode ScrapeData:

 public static DataSet GetProperyData(CookieAwareWebClient webClient, ssortingng pageid) { var dsPageData = new DataSet(); // DOWNLOAD HTML FOR THE REO PAGE AND LOAD IT INTO AN HTMLDOCUMENT ssortingng url = @"https://domain.com?&id=" + pageid + @"restofurl"; ssortingng html = webClient.DownloadSsortingng(url); var doc = new HtmlDocument(); doc.LoadHtml(html ); // A BUNCH OF PARSING WITH HTMLAGILITY AND STORING IN dsPageData return dsPageData ; } 

Si vous souhaitez utiliser les mots-clés async et en await (bien que vous n’ayez pas à le faire, mais que cela facilite les choses dans .NET 4.5), vous voudrez tout d’abord modifier votre méthode ScrapeData pour renvoyer une instance Task à l’aide de la commande mot clé async , comme ceci:

 async Task ScrapeDataAsync(Uri url) { // Create the HttpClientHandler which will handle cookies. var handler = new HttpClientHandler(); // Set cookies on handler. // Await on an async call to fetch here, convert to a data // set and return. var client = new HttpClient(handler); // Wait for the HttpResponseMessage. HttpResponseMessage response = await client.GetAsync(url); // Get the content, await on the ssortingng content. ssortingng content = await response.Content.ReadAsSsortingngAsync(); // Process content variable here into a data set and return. DataSet ds = ...; // Return the DataSet, it will return Task. return ds; } 

Notez que vous souhaiterez probablement vous éloigner de la classe WebClient , car elle ne prend pas en charge la Task inhérente dans ses opérations asynchrones. Un meilleur choix dans .NET 4.5 est la classe HttpClient . J’ai choisi d’utiliser HttpClient ci-dessus. Consultez également la classe HttpClientHandler , en particulier la propriété CookieContainer que vous utiliserez pour envoyer des cookies à chaque demande.

Toutefois, cela signifie que vous devrez probablement utiliser le mot-clé wait pour attendre une autre opération asynchrone, qui dans ce cas serait plus que probablement le téléchargement de la page. Vous devrez personnaliser vos appels de téléchargement de données pour utiliser les versions asynchrones et les await .

Une fois cette opération terminée, vous appelez normalement await , mais vous ne pouvez pas le faire dans ce scénario car vous await une variable. Dans ce scénario, vous exécutez une boucle afin que la variable soit réinitialisée à chaque itération. Dans ce cas, il est préférable de simplement stocker la Task dans un tableau comme suit:

 DataSet alldata = ...; var tasks = new List>(); foreach(var url in the8000urls) { // ScrapeData downloads the html from the url with // WebClient.DownloadSsortingng // and scrapes the data into several datatables which // it returns as a dataset. tasks.Add(ScrapeDataAsync(url)); } 

Il s’agit de fusionner les données dans allData . À cette fin, vous souhaitez appeler la méthode ContinueWith sur l’instance de Task renvoyée et effectuer la tâche consistant à append les données à allData :

 DataSet alldata = ...; var tasks = new List>(); foreach(var url in the8000urls) { // ScrapeData downloads the html from the url with // WebClient.DownloadSsortingng // and scrapes the data into several datatables which // it returns as a dataset. tasks.Add(ScrapeDataAsync(url).ContinueWith(t => { // Lock access to the data set, since this is // async now. lock (allData) { // Add the data. } }); } 

Ensuite, vous pouvez attendre toutes les tâches en utilisant la méthode WhenAll sur la classe Task et await que:

 // After your loop. await Task.WhenAll(tasks); // Process allData 

Toutefois, notez que vous avez une foreach et WhenAll prend une implémentation IEnumerable . Ceci est un bon indicateur que ceci est approprié pour utiliser LINQ, ce qui est:

 DataSet alldata; var tasks = from url in the8000Urls select ScrapeDataAsync(url).ContinueWith(t => { // Lock access to the data set, since this is // async now. lock (allData) { // Add the data. } }); await Task.WhenAll(tasks); // Process allData 

Vous pouvez également choisir de ne pas utiliser la syntaxe de requête si vous le souhaitez, peu importe dans ce cas.

Notez que si la méthode contenant n’est pas marquée comme async (car vous êtes dans une application console et devez attendre les résultats avant la fin de l’application), vous pouvez simplement appeler la méthode Wait sur la Task retournée lorsque vous appelez WhenAll :

 // This will block, waiting for all tasks to complete, all // tasks will run asynchronously and when all are done, then the // code will continue to execute. Task.WhenAll(tasks).Wait(); // Process allData. 

En fait, vous souhaitez collecter vos instances de Task dans une séquence, puis attendre toute la séquence avant de traiter allData .

Cependant, je suggérerais d’essayer de traiter les données avant de les fusionner dans allData si vous le pouvez; À moins que le traitement des données ne requière l’ intégralité du DataSet , vous gagnerez encore plus en performances en traitant autant de données que vous récupérez lorsque vous les récupérerez, au lieu d’attendre que tout cela soit récupéré.

Vous pouvez également utiliser TPL Dataflow , qui convient parfaitement à ce type de problème.

Dans ce cas, vous créez un “maillage de stream de données”, puis vos données le traversent.

Celui-ci ressemble en réalité davantage à un pipeline qu’à un “maillage”. Je mets en trois étapes: Télécharger les données (chaîne) de l’URL; Analyser les données (chaîne) en HTML, puis dans un DataSet ; et fusionner le DataSet dans le DataSet maître.

Tout d’abord, nous créons les blocs qui iront dans le maillage:

 DataSet allData; var downloadData = new TransformBlock( async pageid => { System.Net.WebClient webClient = null; var url = "https://domain.com?&id=" + pageid + "restofurl"; return await webClient.DownloadSsortingngTaskAsync(url); }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded, }); var parseHtml = new TransformBlock( html => { var dsPageData = new DataSet(); var doc = new HtmlDocument(); doc.LoadHtml(html); // HTML Agility parsing return dsPageData; }, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded, }); var merge = new ActionBlock( dataForOnePage => { // merge dataForOnePage into allData }); 

Ensuite, nous lions les trois blocs ensemble pour créer le maillage:

 downloadData.LinkTo(parseHtml); parseHtml.LinkTo(merge); 

Ensuite, nous commençons à injecter des données dans le maillage:

 foreach (var pageid in the8000urls) downloadData.Post(pageid); 

Et enfin, nous attendons que chaque étape du maillage soit terminée (cela propagera proprement toutes les erreurs):

 downloadData.Complete(); await downloadData.Completion; parseHtml.Complete(); await parseHtml.Completion; merge.Complete(); await merge.Completion; 

La bonne chose à propos de TPL Dataflow est que vous pouvez facilement contrôler le parallèle de chaque partie. Pour l’instant, j’ai défini les blocs de téléchargement et d’parsing sur Unbounded , mais vous souhaiterez peut-être les restreindre. Le bloc de fusion utilise le parallélisme maximal par défaut de 1, aucun verrou n’est donc nécessaire lors de la fusion.

Je recommande de lire mon introduction assez complète à async / await .

Tout d’abord, rendre tout asynchrone, en commençant par le niveau inférieur:

 public static async Task ScrapeDataAsync(ssortingng pageid) { CookieAwareWebClient webClient = ...; var dsPageData = new DataSet(); // DOWNLOAD HTML FOR THE REO PAGE AND LOAD IT INTO AN HTMLDOCUMENT ssortingng url = @"https://domain.com?&id=" + pageid + @"restofurl"; ssortingng html = await webClient.DownloadSsortingngTaskAsync(url).ConfigureAwait(false); var doc = new HtmlDocument(); doc.LoadHtml(html); // A BUNCH OF PARSING WITH HTMLAGILITY AND STORING IN dsPageData return dsPageData; } 

Ensuite, vous pouvez le consumr comme suit (en utilisant async avec LINQ):

 DataSet alldata; var tasks = the8000urls.Select(async url => { var dataForOnePage = await ScrapeDataAsync(url); //merge each table in dataForOnePage into allData }); await Task.WhenAll(tasks); PushAllDataToSql(alldata); 

Et utilisez AsyncContext depuis ma bibliothèque AsyncEx puisqu’il s’agit d’ une application console :

 class Program { static int Main(ssortingng[] args) { try { return AsyncContext.Run(() => MainAsync(args)); } catch (Exception ex) { Console.Error.WriteLine(ex); return -1; } } static async Task MainAsync(ssortingng[] args) { ... } } 

C’est tout. Pas besoin de locking ou de continuations ou de tout ça.

Je crois que vous n’avez pas besoin d’ async et await choses ici. Ils peuvent vous aider dans les applications de bureau où vous devez déplacer votre travail vers un thread non graphique. À mon avis, il vaudra mieux utiliser la méthode Parallel.ForEach dans votre cas. Quelque chose comme ça:

  DataSet alldata; var bag = new ConcurrentBag(); Parallel.ForEach(the8000urls, url => { // ScrapeData downloads the html from the url with WebClient.DownloadSsortingng // and scrapes the data into several datatables which it returns as a dataset. DataSet dataForOnePage = ScrapeData(url); // Add data for one page to temp bag bag.Add(dataForOnePage); }); //merge each table in dataForOnePage into allData from bag PushAllDataToSql(alldata);