C# Language
Przykłady asynchronizacji / oczekiwania, pracy w tle, zadań i wątków
Szukaj…
Uwagi
Aby uruchomić dowolny z tych przykładów, po prostu wywołaj je w ten sposób:
static void Main()
{
new Program().ProcessDataAsync();
Console.ReadLine();
}
ASP.NET Konfiguruj oczekiwanie
Gdy ASP.NET obsługuje żądanie, wątek jest przypisywany z puli wątków i tworzony jest kontekst żądania . Kontekst żądania zawiera informacje o bieżącym żądaniu, do którego można uzyskać dostęp za pośrednictwem statycznej właściwości HttpContext.Current
. Kontekst żądania jest następnie przypisywany do wątku obsługującego żądanie.
Dany kontekst żądania może być aktywny tylko dla jednego wątku na raz .
Gdy wykonanie osiągnie await
, wątek obsługujący żądanie jest zwracany do puli wątków, podczas gdy działa metoda asynchroniczna, a kontekst żądania jest dostępny dla innego wątku.
public async Task<ActionResult> Index()
{
// Execution on the initially assigned thread
var products = await dbContext.Products.ToListAsync();
// Execution resumes on a "random" thread from the pool
// Execution continues using the original request context.
return View(products);
}
Po zakończeniu zadania pula wątków przypisuje kolejny wątek, aby kontynuować wykonywanie żądania. Kontekst żądania jest następnie przypisywany do tego wątku. To może być lub nie oryginalny wątek.
Bloking
Podczas oczekiwania na wywołanie metody async
mogą wystąpić synchroniczne zakleszczenia. Na przykład następujący kod spowoduje zakleszczenie po IndexSync()
:
public async Task<ActionResult> Index()
{
// Execution on the initially assigned thread
List<Product> products = await dbContext.Products.ToListAsync();
// Execution resumes on a "random" thread from the pool
return View(products);
}
public ActionResult IndexSync()
{
Task<ActionResult> task = Index();
// Block waiting for the result synchronously
ActionResult result = Task.Result;
return result;
}
Wynika to z tego, że domyślnie oczekiwane zadanie w tym przypadku db.Products.ToListAsync()
przechwytuje kontekst (w przypadku ASP.NET kontekst żądania) i próbuje go użyć po zakończeniu.
Gdy cały stos wywołań jest asynchroniczny, nie ma problemu, ponieważ po osiągnięciu await
oryginalny wątek zostaje zwolniony, uwalniając kontekst żądania.
Gdy Task.Result
synchronicznie za pomocą Task.Result
lub Task.Wait()
(lub innych metod blokowania) oryginalny wątek jest nadal aktywny i zachowuje kontekst żądania. Oczekiwana metoda nadal działa asynchronicznie i po próbie uruchomienia wywołania zwrotnego, tj. Po zwróceniu oczekiwanego zadania, próbuje uzyskać kontekst żądania.
Dlatego pojawia się impas, ponieważ podczas gdy wątek blokujący z kontekstem żądania czeka na zakończenie operacji asynchronicznej, operacja asynchroniczna próbuje uzyskać kontekst żądania w celu jej zakończenia.
ConfigureAwait
Domyślnie wywołania oczekiwanego zadania przechwytują bieżący kontekst i po zakończeniu podejmą próbę wznowienia wykonania w tym kontekście.
Używając ConfigureAwait(false)
można temu zapobiec i uniknąć zakleszczeń.
public async Task<ActionResult> Index()
{
// Execution on the initially assigned thread
List<Product> products = await dbContext.Products.ToListAsync().ConfigureAwait(false);
// Execution resumes on a "random" thread from the pool without the original request context
return View(products);
}
public ActionResult IndexSync()
{
Task<ActionResult> task = Index();
// Block waiting for the result synchronously
ActionResult result = Task.Result;
return result;
}
Pozwala to uniknąć zakleszczeń, gdy konieczne jest blokowanie kodu asynchronicznego, jednak wiąże się to z kosztem utraty kontekstu w kontynuacji (kod po wywołaniu, aby czekać).
W ASP.NET oznacza to, że jeśli twój kod po wywołaniu await someTask.ConfigureAwait(false);
próbuje uzyskać dostęp do informacji z kontekstu, na przykład HttpContext.Current.User
a następnie informacje zostały utracone. W tym przypadku HttpContext.Current
ma wartość NULL. Na przykład:
public async Task<ActionResult> Index()
{
// Contains information about the user sending the request
var user = System.Web.HttpContext.Current.User;
using (var client = new HttpClient())
{
await client.GetAsync("http://google.com").ConfigureAwait(false);
}
// Null Reference Exception, Current is null
var user2 = System.Web.HttpContext.Current.User;
return View();
}
Jeśli zostanie użyta opcja ConfigureAwait(true)
(odpowiednik braku opcji ConfigureAwait), zarówno user
jak i user2
zostaną zapełnione tymi samymi danymi.
Z tego powodu często zaleca się użycie ConfigureAwait(false)
w kodzie biblioteki, gdy kontekst nie jest już używany.
Asynchronizuj / czekaj
Poniżej znajduje się prosty przykład użycia funkcji asynchronicznej / oczekiwania na wykonanie pewnych pracochłonnych czynności w tle, przy jednoczesnym zachowaniu opcji wykonywania innych czynności, które nie muszą czekać na wykonanie pracochłonnych prac.
Jeśli jednak będziesz musiał później pracować z wynikiem metody czasochłonnej, możesz to zrobić, czekając na wykonanie.
public async Task ProcessDataAsync()
{
// Start the time intensive method
Task<int> task = TimeintensiveMethod(@"PATH_TO_SOME_FILE");
// Control returns here before TimeintensiveMethod returns
Console.WriteLine("You can read this while TimeintensiveMethod is still running.");
// Wait for TimeintensiveMethod to complete and get its result
int x = await task;
Console.WriteLine("Count: " + x);
}
private async Task<int> TimeintensiveMethod(object file)
{
Console.WriteLine("Start TimeintensiveMethod.");
// Do some time intensive calculations...
using (StreamReader reader = new StreamReader(file.ToString()))
{
string s = await reader.ReadToEndAsync();
for (int i = 0; i < 10000; i++)
s.GetHashCode();
}
Console.WriteLine("End TimeintensiveMethod.");
// return something as a "result"
return new Random().Next(100);
}
BackgroundWorker
Poniżej znajduje się prosty przykład użycia obiektu BackgroundWorker
do wykonywania czasochłonnych operacji w wątku w tle.
Musisz:
- Zdefiniuj metodę roboczą, która wykonuje czasochłonną pracę, i wywołaj ją z modułu obsługi zdarzeń dla zdarzenia
DoWork
wBackgroundWorker
. - Rozpocznij wykonywanie za pomocą
RunWorkerAsync
. Dowolny argument wymagany przez metodęDoWorkEventArgs
dołączoną doDoWork
można przekazać za pomocą parametruRunWorkerAsync
doRunWorkerAsync
.
Oprócz zdarzenia DoWork
klasa BackgroundWorker
definiuje również dwa zdarzenia, które powinny być używane do interakcji z interfejsem użytkownika. Te są opcjonalne.
- Zdarzenie
RunWorkerCompleted
jest wyzwalane po zakończeniu procedur obsługiDoWork
. - Zdarzenie
ProgressChanged
jest wyzwalane, gdyReportProgress
jest metodaReportProgress
.
public void ProcessDataAsync()
{
// Start the time intensive method
BackgroundWorker bw = new BackgroundWorker();
bw.DoWork += BwDoWork;
bw.RunWorkerCompleted += BwRunWorkerCompleted;
bw.RunWorkerAsync(@"PATH_TO_SOME_FILE");
// Control returns here before TimeintensiveMethod returns
Console.WriteLine("You can read this while TimeintensiveMethod is still running.");
}
// Method that will be called after BwDoWork exits
private void BwRunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
// we can access possible return values of our Method via the Parameter e
Console.WriteLine("Count: " + e.Result);
}
// execution of our time intensive Method
private void BwDoWork(object sender, DoWorkEventArgs e)
{
e.Result = TimeintensiveMethod(e.Argument);
}
private int TimeintensiveMethod(object file)
{
Console.WriteLine("Start TimeintensiveMethod.");
// Do some time intensive calculations...
using (StreamReader reader = new StreamReader(file.ToString()))
{
string s = reader.ReadToEnd();
for (int i = 0; i < 10000; i++)
s.GetHashCode();
}
Console.WriteLine("End TimeintensiveMethod.");
// return something as a "result"
return new Random().Next(100);
}
Zadanie
Poniżej znajduje się prosty przykład użycia Task
do wykonywania pracochłonnych czynności w tle.
Wszystko, co musisz zrobić, to owinąć swoją czasochłonną metodę w Task.Run()
.
public void ProcessDataAsync()
{
// Start the time intensive method
Task<int> t = Task.Run(() => TimeintensiveMethod(@"PATH_TO_SOME_FILE"));
// Control returns here before TimeintensiveMethod returns
Console.WriteLine("You can read this while TimeintensiveMethod is still running.");
Console.WriteLine("Count: " + t.Result);
}
private int TimeintensiveMethod(object file)
{
Console.WriteLine("Start TimeintensiveMethod.");
// Do some time intensive calculations...
using (StreamReader reader = new StreamReader(file.ToString()))
{
string s = reader.ReadToEnd();
for (int i = 0; i < 10000; i++)
s.GetHashCode();
}
Console.WriteLine("End TimeintensiveMethod.");
// return something as a "result"
return new Random().Next(100);
}
Wątek
Poniżej znajduje się prosty przykład użycia Thread
do wykonywania pracochłonnych czynności w tle.
public async void ProcessDataAsync()
{
// Start the time intensive method
Thread t = new Thread(TimeintensiveMethod);
// Control returns here before TimeintensiveMethod returns
Console.WriteLine("You can read this while TimeintensiveMethod is still running.");
}
private void TimeintensiveMethod()
{
Console.WriteLine("Start TimeintensiveMethod.");
// Do some time intensive calculations...
using (StreamReader reader = new StreamReader(@"PATH_TO_SOME_FILE"))
{
string v = reader.ReadToEnd();
for (int i = 0; i < 10000; i++)
v.GetHashCode();
}
Console.WriteLine("End TimeintensiveMethod.");
}
Jak widać, nie możemy zwrócić wartości z naszego TimeIntensiveMethod
ponieważ Thread
oczekuje parametru void jako parametru.
Aby uzyskać wartość zwrotną z Thread
użyj zdarzenia lub następujących czynności:
int ret;
Thread t= new Thread(() =>
{
Console.WriteLine("Start TimeintensiveMethod.");
// Do some time intensive calculations...
using (StreamReader reader = new StreamReader(file))
{
string s = reader.ReadToEnd();
for (int i = 0; i < 10000; i++)
s.GetHashCode();
}
Console.WriteLine("End TimeintensiveMethod.");
// return something to demonstrate the coolness of await-async
ret = new Random().Next(100);
});
t.Start();
t.Join(1000);
Console.Writeline("Count: " + ret);
Zadanie „uruchom i zapomnij” rozszerzenie
W niektórych przypadkach (np. Rejestrowanie) przydatne może być uruchomienie zadania i nie czekanie na wynik. Następujące rozszerzenie pozwala uruchomić zadanie i kontynuować wykonywanie kodu resztowego:
public static class TaskExtensions
{
public static async void RunAndForget(
this Task task, Action<Exception> onException = null)
{
try
{
await task;
}
catch (Exception ex)
{
onException?.Invoke(ex);
}
}
}
Oczekiwany jest wynik tylko w metodzie rozszerzenia. Ponieważ używane jest async
/ await
, możliwe jest wychwycenie wyjątku i wywołanie opcjonalnej metody jego obsługi.
Przykład użycia rozszerzenia:
var task = Task.FromResult(0); // Or any other task from e.g. external lib.
task.RunAndForget(
e =>
{
// Something went wrong, handle it.
});