C# Language
비동기 대기 중
수색…
소개
C #에서 async
선언 된 메서드는 I / O 기반 작업 (예 : 웹 액세스, 파일 작업 ...)을 사용하는 경우 동기식 프로세스 내에서 차단되지 않습니다. 그러한 비동기 표시 메소드의 결과는 await
키워드를 사용하여 await
.
비고
async
메서드는 void
, Task
또는 Task<T>
반환 할 수 있습니다.
반환 유형 Task
는 메소드가 완료 될 때까지 대기하며 결과는 void
됩니다. Task<T>
는 메소드가 완료된 후에 T
유형의 값을 리턴합니다.
async
메서드는 거의 모든 상황에서 void
아니라 Task
또는 Task<T>
반환해야합니다. async void
메서드는 ed를 await
수 없기 때문에 다양한 문제가 발생합니다. async
가 void
를 리턴해야하는 유일한 시나리오는 이벤트 핸들러의 경우입니다.
async
/ await
는 async
메서드를 상태 시스템으로 변환하여 작동합니다. 현재 상태와 모든 컨텍스트 (예 : 로컬 변수)를 저장하는 장면 뒤에 구조를 작성하여 대기 상태가 완료 될 때마다 MoveNext()
메서드를 사용하여 상태를 전진시키고 연관된 코드를 실행합니다.
간단한 연속 통화
public async Task<JobResult> GetDataFromWebAsync()
{
var nextJob = await _database.GetNextJobAsync();
var response = await _httpClient.GetAsync(nextJob.Uri);
var pageContents = await response.Content.ReadAsStringAsync();
return await _database.SaveJobResultAsync(pageContents);
}
여기에서 주목해야 할 중요한 점은 await
모든 메서드가 비동기 적으로 호출되는 동안 해당 컨트롤에 대한 컨트롤이 시스템에 반환된다는 것입니다. 메서드 내부의 흐름은 직선적이며 특별한 처리가 필요 없습니다. 비동기. 실패한 메소드 중 하나라도 실패하면 예외가 "예상대로"처리됩니다.이 경우 메소드 실행이 중단되고 예외가 스택으로 올라갈 것입니다.
시도 / 캐치 / 마침내
C # 6.0부터 catch
키워드와 finally
블록 내에서 await
키워드를 사용할 수 있습니다.
try {
var client = new AsyncClient();
await client.DoSomething();
} catch (MyException ex) {
await client.LogExceptionAsync();
throw;
} finally {
await client.CloseAsync();
}
C # 6.0 이전 버전에서는 다음과 같은 작업을 수행해야합니다. 6.0에서는 Null 전달 연산자를 사용하여 Null 검사도 정리했습니다.
AsynClient client;
MyException caughtException;
try {
client = new AsyncClient();
await client.DoSomething();
} catch (MyException ex) {
caughtException = ex;
}
if (client != null) {
if (caughtException != null) {
await client.LogExceptionAsync();
}
await client.CloseAsync();
if (caughtException != null) throw caughtException;
}
async
(예 : Task.Run
의해 생성 된 작업)로 생성되지 않은 작업을 기다리는 경우 일부 디버거는 주변 try / catch에 의해 겉으로보기에도 처리되는 경우에도 작업에 의해 throw 된 예외에서 중단 될 수 있습니다. 이것은 디버거가 사용자 코드와 관련하여 처리되지 않은 것으로 간주하기 때문에 발생합니다. Visual Studio에는 이러한 상황에서 디버거가 손상되지 않도록 "내 코드" 라는 옵션이 비활성화되어 있습니다.
올바른 비동기 동작을 위해 4.5를 대상으로하는 Web.config 설치.
web.config system.web.httpRuntime은 async 메서드를 다시 시작하기 전에 스레드가 요청 컨텍스트를 리터칭하도록 4.5를 대상으로해야합니다.
<httpRuntime targetFramework="4.5" />
Async 및 Await에는 4.5 이전의 ASP.NET에서 정의되지 않은 동작이 있습니다. 비동기 / 대기는 요청 컨텍스트가없는 임의의 스레드에서 재개됩니다. 대기중인 응용 프로그램은 기다렸다가 HttpContext에 액세스하는 null 참조 예외로 인해 무작위로 실패합니다. WebApi에서 HttpContext.Current를 사용하면 비동기로 인해 위험합니다.
동시 통화
대기중인 작업을 먼저 호출 한 다음 대기하기 위해 동시에 여러 개의 호출을 기다리는 것이 가능합니다.
public async Task RunConcurrentTasks()
{
var firstTask = DoSomethingAsync();
var secondTask = DoSomethingElseAsync();
await firstTask;
await secondTask;
}
또는 Task.WhenAll
을 사용하여 여러 작업을 단일 Task
으로 그룹화 할 수 있습니다.이 작업은 모든 전달 된 작업이 완료 될 때 완료됩니다.
public async Task RunConcurrentTasks()
{
var firstTask = DoSomethingAsync();
var secondTask = DoSomethingElseAsync();
await Task.WhenAll(firstTask, secondTask);
}
다음과 같이 루프 내에서이 작업을 수행 할 수도 있습니다.
List<Task> tasks = new List<Task>();
while (something) {
// do stuff
Task someAsyncTask = someAsyncMethod();
tasks.Add(someAsyncTask);
}
await Task.WhenAll(tasks);
Task.WhenAll을 사용하여 여러 작업을 기다린 후 작업에서 결과를 얻으려면 작업을 다시 기다려야합니다. 작업이 이미 완료되었으므로 결과가 반환됩니다.
var task1 = SomeOpAsync();
var task2 = SomeOtherOpAsync();
await Task.WhenAll(task1, task2);
var result = await task2;
또한 Task.WhenAny
등, 동시에 여러 작업을 실행하는 데 사용될 수 Task.WhenAll
공급 작업 중 하나가 완료 될 때,이 방법은 완료 차이와 위에서.
public async Task RunConcurrentTasksWhenAny()
{
var firstTask = TaskOperation("#firstTask executed");
var secondTask = TaskOperation("#secondTask executed");
var thirdTask = TaskOperation("#thirdTask executed");
await Task.WhenAny(firstTask, secondTask, thirdTask);
}
Task
에 의해 반환 RunConcurrentTasksWhenAny
어떤 경우 완료 firstTask
, secondTask
, 또는 thirdTask
완료됩니다.
연산자와 비동기 키워드 기다림
연산자와 async
키워드가 함께 오기를 await
async
.
await 가 사용되는 비동기 메소드는 async 키워드로 수정해야합니다.
반대의 경우는 항상 사실이 아닙니다. 메서드를 본문에서 async
않고 async
로 표시 할 수 await
.
무엇을 await
실제로하는 일은 기다리는 작업이 완료 될 때까지 코드의 실행을 중지하는 것입니다; 모든 작업을 기다릴 수 있습니다.
참고 : 아무것도 반환하지 않는 비동기 메서드 (void)를 기다릴 수 없습니다.
실제로, 'suspends'라는 단어는 약간의 오해의 소지가 있습니다. 그 이유는 실행이 멈출뿐만 아니라 스레드가 다른 작업을 수행 할 때 자유 롭기 때문입니다. 후드 아래에서, 약간의 컴파일러 마술에 의해 구현이 await
. 그것은 메소드를 두 부분으로 나눕니다. 앞과 뒤를 await
. 후자 부분은 대기중인 작업이 완료 될 때 실행됩니다.
우리가 몇 가지 중요한 세부 사항을 무시한다면, 컴파일러는 대략 당신을 위해 이것을합니다 :
public async Task<TResult> DoIt()
{
// do something and acquire someTask of type Task<TSomeResult>
var awaitedResult = await someTask;
// ... do something more and produce result of type TResult
return result;
}
다음과 같이됩니다.
public Task<TResult> DoIt()
{
// ...
return someTask.ContinueWith(task => {
var result = ((Task<TSomeResult>)task).Result;
return DoIt_Continuation(result);
});
}
private TResult DoIt_Continuation(TSomeResult awaitedResult)
{
// ...
}
일반적인 방법은 다음과 같은 방법으로 비동기로 변환 할 수 있습니다.
await Task.Run(() => YourSyncMethod());
UI를 고정하지 않고 UI 스레드에서 장기 실행 메서드를 실행해야 할 때 유용 할 수 있습니다.
그러나 여기에 매우 중요한 발언이 있습니다. 비동기가 항상 병행 (병렬 또는 멀티 스레드)을 의미하지는 않습니다. 단일 스레드에서도 async
- await
여전히 비동기 코드를 허용합니다. 예를 들어이 사용자 지정 작업 스케줄러를 참조하십시오. 이러한 '미친'작업 스케줄러는 작업을 메시지 루프 처리 내에서 호출되는 기능으로 간단히 전환 할 수 있습니다.
우리는 우리 자신에게 물어야합니다 : 어떤 스레드가 우리의 메소드 DoIt_Continuation
의 연속을 실행합니까?
기본적으로 await
운영자는 현재 동기화 컨텍스트 로 연속 실행을 예약합니다. 즉, 기본적으로 UI 스레드에서 WinForms 및 WPF 연속 실행을 의미합니다. 어떤 이유로이 동작을 변경해야하는 경우 Task.ConfigureAwait()
메서드를 사용합니다.
await Task.Run(() => YourSyncMethod()).ConfigureAwait(continueOnCapturedContext: false);
기다리지 않고 작업 반환
비동기 작업을 수행하는 메소드는 다음과 같은 경우 await
를 사용할 필요가 없습니다.
- 메서드 내에 비동기 호출이 하나만 있습니다.
- 비동기 호출은 메소드의 끝에 있습니다.
- 작업 내에서 발생할 수있는 catch / handling 예외는 필요하지 않습니다.
Task
을 반환하는이 메서드를 고려하십시오.
public async Task<User> GetUserAsync(int id)
{
var lookupKey = "Users" + id;
return await dataStore.GetByKeyAsync(lookupKey);
}
GetByKeyAsync
가 GetUserAsync
와 동일한 서명 ( Task<User>
반환)을 갖는 경우 메서드를 단순화 할 수 있습니다.
public Task<User> GetUserAsync(int id)
{
var lookupKey = "Users" + id;
return dataStore.GetByKeyAsync(lookupKey);
}
이 경우 비동기 작업을 수행하는 경우에도 메서드를 async
로 표시 할 필요가 없습니다. GetByKeyAsync
에 의해 반환 된 Task는 호출 된 메서드에 직접 전달되며, 호출 된 메서드는 ed를 await
립니다.
중요 : Task
를 기다리는 대신 Task
를 리턴하면 Task
를 시작하는 메소드 내에서 예외를 던지지 않고 기다리고있는 메소드에서 예외를 throw하지 않으므로 메소드의 예외 동작이 변경됩니다.
public Task SaveAsync()
{
try {
return dataStore.SaveChangesAsync();
}
catch(Exception ex)
{
// this will never be called
logger.LogException(ex);
}
}
// Some other code calling SaveAsync()
// If exception happens, it will be thrown here, not inside SaveAsync()
await SaveAsync();
이렇게하면 컴파일러가 여분의 비동기 상태 시스템을 생성하지 않으므로 성능이 향상됩니다.
비동기 코드를 차단하면 교착 상태가 발생할 수 있습니다.
동기화 컨텍스트가있는 환경에서 교착 상태가 발생할 수 있으므로 비동기 호출을 차단하는 것은 좋지 않은 방법입니다. 가장 좋은 방법은 async를 사용하거나 "모든 방법을 사용"하는 것입니다. 예를 들어 다음 Windows Forms 코드는 교착 상태가 발생합니다.
private async Task<bool> TryThis()
{
Trace.TraceInformation("Starting TryThis");
await Task.Run(() =>
{
Trace.TraceInformation("In TryThis task");
for (int i = 0; i < 100; i++)
{
// This runs successfully - the loop runs to completion
Trace.TraceInformation("For loop " + i);
System.Threading.Thread.Sleep(10);
}
});
// This never happens due to the deadlock
Trace.TraceInformation("About to return");
return true;
}
// Button click event handler
private void button1_Click(object sender, EventArgs e)
{
// .Result causes this to block on the asynchronous call
bool result = TryThis().Result;
// Never actually gets here
Trace.TraceInformation("Done with result");
}
기본적으로 비동기 호출이 완료되면 동기화 컨텍스트가 사용 가능할 때까지 대기합니다. 그러나 이벤트 처리기는 TryThis()
메서드가 완료 될 때까지 기다리는 동안 동기화 컨텍스트에 "유지"되므로 순환 대기가 발생합니다.
이 문제를 해결하려면 코드를 다음과 같이 수정해야합니다.
private async void button1_Click(object sender, EventArgs e)
{
bool result = await TryThis();
Trace.TraceInformation("Done with result");
}
참고 : async void
메서드를 기다릴 수 없으므로 async void
사용해야하는 곳은 이벤트 핸들러뿐입니다.
비동기 / 대기는 시스템이 추가 작업을 수행 할 수있는 경우에만 성능을 향상시킵니다.
다음 코드를 살펴보십시오.
public async Task MethodA()
{
await MethodB();
// Do other work
}
public async Task MethodB()
{
await MethodC();
// Do other work
}
public async Task MethodC()
{
// Or await some other async work
await Task.Delay(100);
}
이 기능은
public void MethodA()
{
MethodB();
// Do other work
}
public void MethodB()
{
MethodC();
// Do other work
}
public void MethodC()
{
Thread.Sleep(100);
}
async / await의 주된 목적은 컴퓨터가 추가 작업을 수행 할 수 있도록하는 것입니다. 예를 들어 호출 스레드가 일부 I / O 작업의 결과를 기다리는 동안 다른 작업을 수행 할 수 있도록하는 것이 좋습니다. 이 경우, 호출 스레드는 달리 수행 할 수 있었던 것보다 많은 작업을 수행 할 수 없기 때문에 단순히 MethodA()
, MethodB()
및 MethodC()
동 기적으로 호출하는 것 이상의 성능 향상은 없습니다.