Program Club

작업을 취소하면 예외가 발생합니다.

proclub 2021. 1. 9. 10:23
반응형

작업을 취소하면 예외가 발생합니다.


Tasks에 대해 읽은 내용에서 다음 코드는 예외를 발생시키지 않고 현재 실행중인 작업을 취소해야합니다. 작업 취소의 요점은 스레드를 중단하지 않고 작업을 중지하도록 정중하게 "요청"하는 것이라는 인상을 받았습니다.

다음 프로그램의 출력은 다음과 같습니다.

예외 덤프

[OperationCanceledException]

마지막으로 계산 된 소수를 취소하고 반환합니다.

취소 할 때 예외를 피하기 위해 노력하고 있습니다. 어떻게하면 되나요?

void Main()
{
    var cancellationToken = new CancellationTokenSource();

    var task = new Task<int>(() => {
        return CalculatePrime(cancellationToken.Token, 10000);
    }, cancellationToken.Token);

    try
    {
        task.Start();
        Thread.Sleep(100);
        cancellationToken.Cancel();
        task.Wait(cancellationToken.Token);         
    }
    catch (Exception e)
    {
        Console.WriteLine("Dumping exception");
        e.Dump();
    }
}

int CalculatePrime(CancellationToken cancelToken, object digits)
{  
    int factor; 
    int lastPrime = 0;

    int c = (int)digits;

    for (int num = 2; num < c; num++)
    { 
        bool isprime = true;
        factor = 0; 

        if (cancelToken.IsCancellationRequested)
        {
            Console.WriteLine ("Cancelling and returning last calculated prime.");
            //cancelToken.ThrowIfCancellationRequested();
            return lastPrime;
        }

        // see if num is evenly divisible 
        for (int i = 2; i <= num/2; i++)
        { 
            if ((num % i) == 0)
            {             
                // num is evenly divisible -- not prime 
                isprime = false; 
                factor = i; 
            }
        } 

        if (isprime)
        {
            lastPrime = num;
        }
    }

    return lastPrime;
}

이 줄에서 명시 적으로 예외를 던지고 있습니다.

cancelToken.ThrowIfCancellationRequested();

작업을 정상적으로 종료하려면 해당 줄을 제거하면됩니다.

일반적으로 사람들은이를 제어 메커니즘으로 사용하여 추가 코드를 실행하지 않고도 현재 처리가 중단되도록합니다. 또한 다음과 ThrowIfCancellationRequested()기능적으로 동일하므로 호출 할 때 취소를 확인할 필요가 없습니다 .

if (token.IsCancellationRequested) 
    throw new OperationCanceledException(token);

ThrowIfCancellationRequested()Task를 사용할 때 다음과 같이 보일 수 있습니다.

int CalculatePrime(CancellationToken cancelToken, object digits) {
    try{
        while(true){
            cancelToken.ThrowIfCancellationRequested();

            //Long operation here...
        }
    }
    finally{
        //Do some cleanup
    }
}

또한 Task.Wait(CancellationToken)토큰이 취소 된 경우 예외가 발생합니다. 이 방법을 사용하려면 Wait 호출을 Try...Catch블록 으로 래핑해야합니다 .

MSDN : 작업 취소 방법


취소 할 때 예외를 피하기 위해 노력하고 있습니다.

그렇게해서는 안됩니다.

Throwing OperationCanceledException은 "당신이 호출 한 메서드가 취소되었습니다"를 TPL로 표현하는 관용적 방법입니다. 그것에 맞서 싸우지 마십시오. 기대하십시오.

It's a good thing, because it means that when you've got multiple operations using the same cancellation token, you don't need to pepper your code at every level with checks to see whether or not the method you've just called has actually completed normally or whether it's returned due to cancellation. You could use CancellationToken.IsCancellationRequested everywhere, but it'll make your code a lot less elegant in the long run.

Note that there are two pieces of code in your example which are throwing an exception - one within the task itself:

cancelToken.ThrowIfCancellationRequested()

and one where you wait for the task to complete:

task.Wait(cancellationToken.Token);

I don't think you really want to be passing the cancellation token into the task.Wait call, to be honest... that allows other code to cancel your waiting. Given that you know you've just cancelled that token, it's pointless - it's bound to throw an exception, whether the task has actually noticed the cancellation yet or not. Options:

  • Use a different cancellation token (so that other code can cancel your wait independently)
  • Use a time-out
  • Just wait for as long as it takes

Some of the above answers read as if ThrowIfCancellationRequested() would be an option. It is not in this case, because you won't get your resulting last prime. The idiomatic way that "the method you called was cancelled" is defined for cases when canceling means throwing away any (intermediate) results. If your definition of cancelling is "stop computation and return the last intermediate result" you already left that way.

Discussing the benefits especially in terms of runtime is also quite misleading: The implemented algorithm sucks at runtime. Even a highly optimized cancellation will not do any good.

The easiest optimization would be to unroll this loop and skip some unneccessary cycles:

for(i=2; i <= num/2; i++) { 
  if((num % i) == 0) { 
    // num is evenly divisible -- not prime 
    isprime = false; 
    factor = i; 
  }
} 

You can

  • save (num/2)-1 cycles for every even number, which is slightly less than 50% overall (unrolling),
  • save (num/2)-square_root_of(num) cycles for every prime (choose bound according to math of smallest prime factor),
  • save at least that much for every non-prime, expect much more savings, e.g. num = 999 finishes with 1 cycle instead of 499 (break, if answer is found) and
  • save another 50% of cycles, which is of course 25% overall (choose step according to math of primes, unrolling handles the special case 2).

That accounts to saving a guaranteed minimum of 75% (rough estimation: 90%) of cycles in the inner loop, just by replacing it with:

if ((num % 2) == 0) {
  isprime = false; 
  factor = 2;
} else {
  for(i=3; i <= (int)Math.sqrt(num); i+=2) { 
    if((num % i) == 0) { 
      // num is evenly divisible -- not prime 
      isprime = false; 
      factor = i;
      break;
    }
  }
} 

There are much faster algorithms (which I won't discuss because I'm far enough off-topic) but this optimization is quite easy and still proves my point: Don't worry about micro-optimizing runtime when your algorithm is this far from optimal.


Another note about the benefit of using ThrowIfCancellationRequested rather than IsCancellationRequested: I've found that when needing to use ContinueWith with a continuation option of TaskContinuationOptions.OnlyOnCanceled, IsCancellationRequested will not cause the conditioned ContinueWith to fire. ThrowIfCancellationRequested, however, will set the Canceled condition of the task, causing the ContinueWith to fire.

Note: This is only true when the task is already running and not when the task is starting. This is why I added a Thread.Sleep() between the start and cancellation.

CancellationTokenSource cts = new CancellationTokenSource();

Task task1 = new Task(() => {
    while(true){
        if(cts.Token.IsCancellationRequested)
            break;
    }
}, cts.Token);
task1.ContinueWith((ant) => {
    // Perform task1 post-cancellation logic.
    // This will NOT fire when calling cst.Cancel().
}

Task task2 = new Task(() => {
    while(true){
        cts.Token.ThrowIfCancellationRequested();
    }
}, cts.Token);
task2.ContinueWith((ant) => {
    // Perform task2 post-cancellation logic.
    // This will fire when calling cst.Cancel().
}

task1.Start();
task2.Start();
Thread.Sleep(3000);
cts.Cancel();

You have two things listening to the token, the calculate prime method and also the Task instance named task. The calculate prime method should return gracefully, but task gets cancelled while it is still running so it throws. When you construct task don't bother giving it the token.

ReferenceURL : https://stackoverflow.com/questions/7343211/cancelling-a-task-is-throwing-an-exception

반응형