Toto je velmi ošemetný problém související s Knihovnou úloh. Stručně řečeno, je vytvořeno a naplánováno příliš mnoho úloh, takže jedna z úloh, na kterou ovladač MongoDB čeká, nebude moci být dokončena. Trvalo mi velmi dlouho, než jsem si uvědomil, že to není uváznutí, i když to tak vypadá.
Zde je krok k reprodukci:
- Stáhněte si zdrojový kód ovladač CSharp MongoDB .
- Otevřete toto řešení a vytvořte v něm projekt konzoly s odkazem na projekt ovladače.
- Ve funkci Main vytvořte System.Threading.Timer, který bude volat TestTask včas. Nastavte časovač tak, aby se jednou spustil okamžitě. Na konec přidejte Console.Read().
- V testu TestTask použijte cyklus for k vytvoření 300 úkolů voláním Task.Factory.StartNew(DoOneThing). Přidejte všechny tyto úkoly do seznamu a pomocí Task.WaitAll počkejte, než budou všechny dokončeny.
- Ve funkci DoOneThing vytvořte MongoClient a zadejte jednoduchý dotaz.
- Teď to spusťte.
Toto selže na stejném místě, které jste zmínili:MongoDB.Driver.Core.Clusters.Cluster.WaitForDescriptionChangedHelper.HandleCompletedTask(Task completedTask)
Pokud vložíte nějaké body přerušení, budete vědět, že WaitForDescriptionChangedHelper vytvořil úlohu časového limitu. Poté čeká na dokončení libovolné úlohy DescriptionUpdate nebo úlohy časového limitu. K aktualizaci popisu však nikdy nedojde, ale proč?
Nyní, zpět k mému příkladu, je zde jedna zajímavá část:Spustil jsem časovač. Pokud zavoláte TestTask přímo, poběží bez problémů. Když je porovnáte s oknem Úkoly sady Visual Studio, zjistíte, že verze s časovačem vytvoří mnohem více úkolů než verze bez časovače. Dovolte mi vysvětlit tuto část o něco později. Je tu ještě jeden důležitý rozdíl. Do Cluster.cs
musíte přidat ladicí řádky :
protected void UpdateClusterDescription(ClusterDescription newClusterDescription)
{
ClusterDescription oldClusterDescription = null;
TaskCompletionSource<bool> oldDescriptionChangedTaskCompletionSource = null;
Console.WriteLine($"Before UpdateClusterDescription {_descriptionChangedTaskCompletionSource?.Task.Id}, {_descriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
lock (_descriptionLock)
{
oldClusterDescription = _description;
_description = newClusterDescription;
oldDescriptionChangedTaskCompletionSource = _descriptionChangedTaskCompletionSource;
_descriptionChangedTaskCompletionSource = new TaskCompletionSource<bool>();
}
OnDescriptionChanged(oldClusterDescription, newClusterDescription);
Console.WriteLine($"Setting UpdateClusterDescription {oldDescriptionChangedTaskCompletionSource?.Task.Id}, {oldDescriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
oldDescriptionChangedTaskCompletionSource.TrySetResult(true);
Console.WriteLine($"Set UpdateClusterDescription {oldDescriptionChangedTaskCompletionSource?.Task.Id}, {oldDescriptionChangedTaskCompletionSource?.Task?.GetHashCode().ToString("F8")}");
}
private void WaitForDescriptionChanged(IServerSelector selector, ClusterDescription description, Task descriptionChangedTask, TimeSpan timeout, CancellationToken cancellationToken)
{
using (var helper = new WaitForDescriptionChangedHelper(this, selector, description, descriptionChangedTask, timeout, cancellationToken))
{
Console.WriteLine($"Waiting {descriptionChangedTask?.Id}, {descriptionChangedTask?.GetHashCode().ToString("F8")}");
var index = Task.WaitAny(helper.Tasks);
helper.HandleCompletedTask(helper.Tasks[index]);
}
}
Přidáním těchto řádků také zjistíte, že verze bez časovače se aktualizuje dvakrát, ale verze s časovačem se aktualizuje pouze jednou. A druhý pochází z "MonitorServerAsync" v ServerMonitor.cs. Ukázalo se, že ve verzi s časovačem byl MontiorServerAsync proveden, ale poté, co prošel celou cestou přes ServerMonitor.HeartbeatAsync, BinaryConnection.OpenAsync, BinaryConnection.OpenHelperAsync a TcpStreamFactory.CreateStreamAsync, konečně dosáhl TEnc Tady se stane ta špatná věc:Dns.GetHostAddressesAsync
. Tenhle nikdy nebude popraven. Pokud mírně upravíte kód a změníte jej na:
var task = Dns.GetHostAddressesAsync(dnsInitial.Host).ConfigureAwait(false);
return (await task)
.Select(x => new IPEndPoint(x, dnsInitial.Port))
.OrderBy(x => x, new PreferredAddressFamilyComparer(preferred))
.ToArray();
Budete moci najít ID úkolu. Při pohledu do okna Tasks ve Visual Studiu je zcela zřejmé, že před ním je asi 300 úkolů. Pouze několik z nich se provádí, ale je blokováno. Pokud do funkce DoOneThing přidáte Console.Writeline, uvidíte, že plánovač úloh spustí několik z nich téměř současně, ale pak se zpomalí na přibližně jednu za sekundu. To znamená, že musíte počkat asi 300 sekund, než se spustí úloha vyřešení DNS. Proto překračuje časový limit 30 sekund.
Nyní přichází rychlé řešení, pokud neděláte bláznivé věci:
Task.Factory.StartNew(DoOneThing, TaskCreationOptions.LongRunning);
To přinutí ThreadPoolScheduler, aby okamžitě spustil vlákno namísto čekání jednu sekundu před vytvořením nového.
To však nebude fungovat, pokud děláte opravdu bláznivé věci jako já. Změňme cyklus for z 300 na 30000, i toto řešení může selhat. Důvodem je, že vytváří příliš mnoho vláken. To je náročné na zdroje a čas. A může to začít proces GC. Dohromady nemusí být možné dokončit vytváření všech těchto vláken dříve, než vyprší čas.
Dokonalým způsobem je přestat vytvářet spoustu úloh a použít k jejich plánování výchozí plánovač. Můžete zkusit vytvořit pracovní položku a vložit ji do ConcurrentQueue a poté vytvořit několik vláken jako pracovníci, kteří budou položky používat.
Pokud však nechcete původní strukturu příliš měnit, můžete zkusit následující způsob:
Vytvořte ThrottledTaskScheduler odvozený od TaskScheduler.
- Tento ThrottledTaskScheduler přijímá TaskScheduler jako základní, který bude spouštět skutečnou úlohu.
- Uložte úlohy do základního plánovače, ale pokud překročí limit, umístěte je místo toho do fronty.
- Pokud některá z úloh byla dokončena, zkontrolujte frontu a pokuste se ji uložit do základního plánovače v rámci limitu.
- Použijte následující kód ke spuštění všech těch bláznivých nových úkolů:
·
var taskScheduler = new ThrottledTaskScheduler(
TaskScheduler.Default,
128,
TaskCreationOptions.LongRunning | TaskCreationOptions.HideScheduler,
logger
);
var taskFactory = new TaskFactory(taskScheduler);
for (var i = 0; i < 30000; i++)
{
tasks.Add(taskFactory.StartNew(DoOneThing))
}
Task.WaitAll(tasks.ToArray());
Jako referenci si můžete vzít System.Threading.Tasks.ConcurrentExclusiveSchedulerPair.ConcurrentExclusiveTaskScheduler. Je to trochu složitější než to, co potřebujeme. Je to pro nějaký jiný účel. Takže si nedělejte starosti s těmi částmi, které se pohybují tam a zpět s funkcí uvnitř třídy ConcurrentExclusiveSchedulerPair. Nemůžete jej však použít přímo, protože při vytváření úlohy balení neprojde TaskCreationOptions.LongRunning.
Pro mě to funguje. Hodně štěstí!
P.S.:Důvod, proč máte ve verzi s časovačem mnoho úkolů, pravděpodobně leží uvnitř TaskScheduler.TryExecuteTaskInline. Pokud je v hlavním vlákně, kde je vytvořen ThreadPool, bude moci provádět některé úlohy, aniž by je zařadil do fronty.