2017-09-13 8 views
2

확인을 클릭하십시오. 데이터베이스에서 행을 쿼리하는 응용 프로그램의 일부가 있습니다. 사용자가 검색 상자에 텍스트를 입력하거나 다른 필터 설정을 변경할 때 쿼리를 수행합니다.비동기 작업이 해당 작업을 다시 시작하기 전에 완전히 취소되었는지 확인하십시오.

데이터베이스에서 반환되는 데이터는 DataGrid에 바인딩 된 ObservableCollection으로 이동합니다. UI 응답 성을 의식하고 있기 때문에 배경에 ObservableCollection을 채우기 위해 Async-Await (시도)를 사용하고 있습니다.

내 생각에 사용자가 무언가를 입력하거나 필터 설정을 변경할 때마다 진행중인 작업을 취소하고 싶습니다. 취소가 완료 될 때까지 기다렸다가을 확인한 다음 새로운 작업)을 새 설정으로 변경하십시오.

하지만 수집이 취소되지 않고 두 번 채워지고 CancellationTokenSource를 처분 할 때와 같이 (내가 느린 데이터베이스 액세스를 시뮬레이트하는 작업을 느리게 할 때 특히) 모든 종류의 이상한 결과가 발생합니다. 좋은 생각) 때로는 내가 Cancel()라고 부르는 지점에 도착했을 때 그 시간까지 처리되어 예외가 생겼습니다.

필자는이 문제는 필자가 여기서 사용하고자하는 패턴을 이해하는데있어 근본적인 차이에서 기인한다고 생각합니다. 따라서 어떤 스타일/패턴 포인터도 실제 기술 솔루션처럼 환영받을 수 있습니다.

코드는 기본적으로 다음과 같이 진행됩니다

ObservableCollection<Thing> _thingCollection; 
Task _thingUpdaterTask; 
CancellationTokenSource _thingUpdaterCancellationSource; 

// initialisation etc. here 

async void PopulateThings(ThingFilterSettings settings) 
{ 
    // try to cancel any ongoing task 
    if(_thingUpdaterTask?.IsCompleted ?? false){ 
     _thingUpdaterCancellationSource.Cancel(); 
     await _thingUpdaterTask; 
    } 

    // I'm hoping that any ongoing task is now done with, 
    // but in reality that isn't happening. I'm guessing 
    // that's because Tasks are getting dereferenced and 
    // orphaned in concurrent calls to this method? 

    _thingCollection.Clear(); 
    _thingUpdaterCancellationSource = new CancellationTokenSource(); 
    var cancellationToken = _thingUpdaterCancellationSource.Token; 
    var progressHandler = new Progress<Thing>(x => _thingCollection.add(x)); 
    var progress = (IProgress<Thing>)progressHandler; 

    try{ 
     _thingUpdaterTask = Task.Factory.StartNew(
      () => GetThings(settings, progress, cancellationToken)); 
     await _thingUpdaterTask; 
    }catch(AggregateException e){ 
     //handle stuff etc. 
    }finally{ 
     // should I be disposing the Token Source here? 
    } 
} 

void GetThings(ThingFilterSettings settings, 
       IProgress<Thing> progress, 
       CancellationToken ctok){ 
    foreach(var thingy in SomeGetThingsMethod(settings)){ 
     if(ctok.IsCancellationRequested){ 
      break; 
     } 
     progress.Report(thingy); 
    } 
} 
+2

'await'은 예외를 언랩하고, AggregateException을 잡을 필요가 없습니다. – xxbbcc

+0

@dymanoid - 예를 들어 코드를 일반화하려고 시도 할 때 오타가났습니다. 고정되어 있고 잘하면 지금 감각을 만들고있어! – LexyStardust

+2

@LexyStardust는 이벤트 핸들러를 제외하고는'async void '를 사용하지 않습니다. 그 메소드를 업데이트하여'Task'를 리턴해야합니다. – Nkosi

답변

0

당신은 새 작업을 시작하기 전에 (완료 또는 취소함으로써 중) 정지 이전 작업 실행을 위해 대기하는 래퍼 클래스를 추가 할 수 있습니다.

public class ChainableTask 
{ 
    private readonly Task _task; 
    private readonly CancellationTokenSource _cts = new CancellationTokenSource(); 

    public ChainableTask(Func<CancellationToken, Task> asyncAction, 
         ChainableTask previous = null) 
    { 
     _task = Execute(asyncAction, previous); 
    } 

    private async Task CancelAsync() 
    { 
     try 
     { 
      _cts.Cancel(); 
      await _task; 
     } 
     catch (OperationCanceledException) 
     { } 
    } 

    private async Task Execute(Func<CancellationToken, Task> asyncAction, ChainableTask previous) 
    { 
     if (previous != null) 
      await previous.CancelAsync(); 

     if (_cts.IsCancellationRequested) 
      return; 

     await asyncAction(_cts.Token); 
    } 
} 

이전 프로젝트에서 위의 클래스를 사용하는 경우. 이 클래스는 람다 (asyncAction)를 사용하여 다음 작업을 만듭니다. 작업은 이전 작업이 완료된 후에 만 ​​만들어집니다.

각 작업에 CancellationToken을 전달하여 완료 전에 작업을 중지 할 수 있습니다. 다음 작업을 시작하기 전에 이전 토큰이 취소되고 이전 작업이 대기됩니다. 이것은 CancelAsync에서 발생합니다.

이전 Cancel이 대기 한 후에 만 ​​다음 태스크를 생성하기 위해 람다를 호출합니다.

사용 현황 예 : ChainableTask 두 번째 호출이 두 번째를 호출하기 전에 첫 번째 Task.Delay(1000) 완료 될 때까지 대기 할 수 있도록이 예에서

var firstAction = new ChainableTask(async tcs => await Task.Delay(1000)); 
var secondAction = new ChainableTask(async tcs => await Task.Delay(1000), firstAction); // pass the previous action 

은 생성 된 작업은 취소를 지원하지 않습니다.