c#.net 4.5 async / multithread?
J'écris une application console C# qui racle les données des pages web.
Cette application ira à environ 8000 pages web et gratter des données(même format de données sur chaque page).
Je l'ai fonctionne en ce moment sans méthodes asynchrones et sans multithreading.
Cependant, j'ai besoin que ce soit plus rapide. Il utilise seulement environ 3%-6% du CPU, je pense parce qu'il passe le temps à attendre pour télécharger le html.(WebClient.Télécharger (url))
C'est le flux de base de mon programme
DataSet alldata;
foreach(var url in the8000urls)
{
// ScrapeData downloads the html from the url with WebClient.DownloadString
// 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 mais je ne sais pas comment démarrer correctement. J'utilise. net 4.5 et ma compréhension est asynchrone et attend dans 4.5 sont faits pour rendre cela beaucoup plus facile à programmer mais je suis encore un peu perdu.
Mon idée était de continuer à créer de nouveaux threads asynchrones pour cette ligne
DataSet dataForOnePage = ScrapeData(url);
Et alors que chacun se termine, exécutez
//merge each table in dataForOnePage into allData
Quelqu'un peut-il me diriger dans la bonne direction sur la façon de rendre cette ligne asynchrone dans. Net 4.5 C# et ensuite, ma méthode de fusion est-elle terminée?
Je vous Remercie.
Edit: voici ma méthode ScrapeData:
public static DataSet GetProperyData(CookieAwareWebClient webClient, string pageid)
{
var dsPageData = new DataSet();
// DOWNLOAD HTML FOR THE REO PAGE AND LOAD IT INTO AN HTMLDOCUMENT
string url = @"https://domain.com?&id=" + pageid + @"restofurl";
string html = webClient.DownloadString(url);
var doc = new HtmlDocument();
doc.LoadHtml(html );
// A BUNCH OF PARSING WITH HTMLAGILITY AND STORING IN dsPageData
return dsPageData ;
}
4 réponses
Si vous souhaitez utiliser le async
et await
mots-clés (bien que vous n'ayez pas à le faire, mais ils facilitent les choses dans. Net 4.5), vous devez d'abord changer votre méthode ScrapeData
pour retourner un Task<T>
instance en utilisant le mot clé async
, comme ceci:
async Task<DataSet> 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 string content.
string content = await response.Content.ReadAsStringAsync();
// Process content variable here into a data set and return.
DataSet ds = ...;
// Return the DataSet, it will return Task<DataSet>.
return ds;
}
Notez que vous voudrez probablement vous éloigner de la classe WebClient
, car elle ne supporte pas Task<T>
intrinsèquement dans ses opérations asynchrones. Un meilleur choix dans. Net 4.5 est le HttpClient
classe. J'ai choisi pour utiliser HttpClient
ci-dessus. Aussi, jetez un oeil à la HttpClientHandler
de catégorie, plus précisément le CookieContainer
la propriété que vous utiliserez pour envoyer des cookies à chaque requête.
Cependant, cela signifie que vous devrez plus que probablement utiliser le mot clé await
pour attendre une autre opération asynchrone, qui dans ce cas, serait plus que probablement le téléchargement de la page. Vous devrez adapter vos appels qui téléchargent des données pour utiliser les versions asynchrones et await
sur de ceux-ci.
Une fois cela terminé, vous appelez normalement await
sur cela, mais vous ne pouvez pas le faire dans ce scénario parce que vous le feriez await
sur une variable. Dans ce scénario, vous exécutez une boucle, de sorte que la variable serait réinitialisée à chaque itération. Dans ce cas, il est préférable de simplement stocker le Task<T>
dans un tableau comme ceci:
DataSet alldata = ...;
var tasks = new List<Task<DataSet>>();
foreach(var url in the8000urls)
{
// ScrapeData downloads the html from the url with
// WebClient.DownloadString
// 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 en allData
. À cette fin, vous voulez appeler le ContinueWith
méthode sur Task<T>
instance retournée et effectuer la tâche d'ajouter les données à allData
:
DataSet alldata = ...;
var tasks = new List<Task<DataSet>>();
foreach(var url in the8000urls)
{
// ScrapeData downloads the html from the url with
// WebClient.DownloadString
// 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 sur toutes les tâches en utilisant le WhenAll
méthode sur le Task
classe et await
sur cela:
// After your loop.
await Task.WhenAll(tasks);
// Process allData
Notez, Cependant, que vous avez un foreach
, et WhenAll
prend un IEnumerable<T>
la mise en œuvre. C'est un bon indicateur que cela convient 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, cela n'a pas d'importance cas.
Notez que si le contenant méthode n'est pas marqué comme async
(parce que vous êtes dans une application console et attendre les résultats avant que l'application se termine), alors vous pouvez simplement appeler l'Wait
méthode sur Task
retourné 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.
À savoir, le fait est que vous voulez collecter vos instances Task
dans une séquence, puis attendre toute la séquence avant de traiter allData
.
Cependant, je suggère d'essayer de traiter le données avant de les fusionner en allData
si vous le pouvez; à moins que le traitement des données ne nécessite l'ensemble DataSet
, vous obtiendrez encore plus de gains de performance en traitant la plus grande partie des données que vous récupérez lorsque vous les récupérez, plutôt que d'attendre que all revienne.
Vous pouvez également utiliser TPL Dataflow , ce qui convient bien à ce genre de problème.
Dans ce cas, vous créez un "maillage de flux de données", puis vos données le traversent.
Celui-ci ressemble plus à un pipeline qu'à un "maillage". Je mets en trois étapes: Télécharger les données (string) de l'URL; analyser les données (string) en HTML, puis dans un DataSet
; et fusionner le DataSet
dans le maître DataSet
.
Tout d'abord, nous créons les blocs qui iront dans le maillage:
DataSet allData;
var downloadData = new TransformBlock<string, string>(
async pageid =>
{
System.Net.WebClient webClient = null;
var url = "https://domain.com?&id=" + pageid + "restofurl";
return await webClient.DownloadStringTaskAsync(url);
},
new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = DataflowBlockOptions.Unbounded,
});
var parseHtml = new TransformBlock<string, DataSet>(
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<DataSet>(
dataForOnePage =>
{
// merge dataForOnePage into allData
});
Ensuite, nous relions les trois blocs ensemble pour créer le maillage:
downloadData.LinkTo(parseHtml);
parseHtml.LinkTo(merge);
Ensuite, nous commençons à pomper 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 également proprement les erreurs):
downloadData.Complete();
await downloadData.Completion;
parseHtml.Complete();
await parseHtml.Completion;
merge.Complete();
await merge.Completion;
La bonne chose à propos de flux de données TPL est que vous pouvez facilement contrôler comment parallèle {[26] } chaque partie est. Pour l'instant, j'ai défini les blocs de téléchargement et d'analyse sur Unbounded
, mais vous pouvez les restreindre. Le bloc de fusion utilise le parallélisme maximum par défaut de 1, donc aucun verrou n'est nécessaire lors de la fusion.
Je recommande de lire mon Introduction raisonnablement complète à async
/await
.
Tout d'abord, rendez tout asynchrone, en commençant par les éléments de niveau inférieur:
public static async Task<DataSet> ScrapeDataAsync(string pageid)
{
CookieAwareWebClient webClient = ...;
var dsPageData = new DataSet();
// DOWNLOAD HTML FOR THE REO PAGE AND LOAD IT INTO AN HTMLDOCUMENT
string url = @"https://domain.com?&id=" + pageid + @"restofurl";
string html = await webClient.DownloadStringTaskAsync(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 consommer 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 l'utilisation AsyncContext
de mon AsyncEx bibliothèque étant donné que c'est une application console:
class Program
{
static int Main(string[] args)
{
try
{
return AsyncContext.Run(() => MainAsync(args));
}
catch (Exception ex)
{
Console.Error.WriteLine(ex);
return -1;
}
}
static async Task<int> MainAsync(string[] args)
{
...
}
}
C'est ça. Pas besoin de verrouillage ou de poursuites ou de tout cela.
Je crois que vous n'avez pas besoin de async
et await
trucs ici. Ils peuvent aider dans l'application de bureau où vous devez déplacer votre travail vers un thread non-GUI. À mon avis, il vaudra mieux utiliser la méthode Parallel.ForEach
dans votre cas. Quelque chose comme ceci:
DataSet alldata;
var bag = new ConcurrentBag<DataSet>();
Parallel.ForEach(the8000urls, url =>
{
// ScrapeData downloads the html from the url with WebClient.DownloadString
// 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);