작업 기반 비동기 프로그래밍

TPL(작업 병렬 라이브러리)은 작업이란 개념을 기반으로 하며 비동기 작업을 나타냅니다. 몇 가지 점에서 작업은 스레드 또는 ThreadPool 작업 항목과 비슷하지만 추상화 수준은 더 높습니다. 작업 병렬 처리는 동시에 실행되는 하나 이상의 독립적인 작업을 의미합니다. 작업을 사용할 때의 주된 이점 두 가지는 다음과 같습니다.

  • 시스템 리소스를 더 효율적이고 확장 가능한 방식으로 사용할 수 있습니다.

    뒤에서 작업은 스레드 수를 결정하고 조정하는 알고리즘으로 향상된 ThreadPool의 큐에 추가됩니다. 이러한 알고리즘은 처리량을 최대화하기 위한 부하 분산을 제공합니다. 이 프로세스를 통해 작업이 비교적 단순해지며, 여러 개의 작업을 만들어 세부적인 병렬 처리를 사용할 수 있습니다.

  • 프로그래밍 방식 제어 수준이 스레드 또는 작업 항목을 사용할 때보다 높아집니다.

    작업과 작업을 기반으로 만들어진 프레임워크는 대기, 취소, 연속, 강력한 예외 처리, 세부 상태, 사용자 지정 예약 등을 지원하는 강력한 API 집합을 제공합니다.

두 가지 이유 모두에서 TPL은 .NET에서 다중 스레드, 비동기 및 병렬 코드를 작성하는 데 기본 설정되는 API입니다.

암시적으로 작업 만들기 및 실행

Parallel.Invoke 메서드를 사용하면 개수에 관계없이 여러 개의 임의 문을 간편하게 동시에 실행할 수 있습니다. 작업의 각 항목에 대한 Action 대리자를 전달하기만 하면 됩니다. 이러한 대리자를 만드는 가장 쉬운 방법은 람다 식을 사용하는 것입니다. 람다 식은 명명된 메서드를 호출하거나 코드를 인라인으로 제공합니다. 다음 예제에서는 동시에 실행되는 두 개의 작업을 만들고 시작하는 기본적인 Invoke 호출을 보여 줍니다.call that creates and starts two tasks that run concurrently. 이름이 DoSomeWork인 메서드를 호출하는 람다 식에서 첫 번째 작업을 표시하며 이름이 DoSomeOtherWork인 메서드를 호출하는 람다 식에서 두 번째 작업을 표시합니다.

참고 항목

이 문서에서는 람다 식을 사용하여 TPL에 대리자를 정의합니다. C# 또는 Visual Basic의 람다 식을 잘 모르는 경우 PLINQ 및 TPL의 람다 식을 참조하세요.

Parallel.Invoke(() => DoSomeWork(), () => DoSomeOtherWork());
Parallel.Invoke(Sub() DoSomeWork(), Sub() DoSomeOtherWork())

참고 항목

Task에 의해 자동으로 만들어지는 Invoke 인스턴스의 수는 제공되는 대리자의 수와 같지 않을 수도 있습니다. TPL은 특히 대리자 수가 많은 경우 다양한 최적화를 사용할 수 있습니다.

자세한 내용은 방법: Parallel.Invoke를 사용하여 병렬 작업 실행을 참조하세요.

작업 실행을 더 구체적으로 제어하거나 작업을 통해 값을 반환하려면 Task 개체를 더 명시적으로 사용해야 합니다.

명시적으로 작업 만들기 및 실행

값을 반환하지 않는 작업은 System.Threading.Tasks.Task 클래스로 표시됩니다. 값을 반환하는 작업은 System.Threading.Tasks.Task<TResult>에서 상속하는 Task 클래스로 표현됩니다. 작업 개체는 인프라 세부 사항을 처리하고, 작업의 수명 내내 호출 스레드에서 액세스할 수 있는 메서드 및 속성을 제공합니다. 예를 들어 언제든지 작업의 Status 속성에 액세스하여 작업이 실행되기 시작했는지, 이미 실행되어 완료되었는지, 취소되었는지, 또는 예외를 throw했는지를 확인할 수 있습니다. 이러한 상태는 TaskStatus 열거형으로 표현됩니다.

작업을 만들 때는 해당 작업에서 실행할 코드를 캡슐화하는 사용자 대리자를 지정합니다. 대리자는 명명된 대리자, 익명 메서드 또는 람다 식으로 표현할 수 있습니다. 람다 식에는 다음 예제에서처럼 명명된 메서드에 대한 호출을 포함할 수 있습니다. 예제에는 콘솔 모드 애플리케이션이 종료되기 전에 작업 실행이 완료되도록 Task.Wait 메서드에 대한 호출이 포함됩니다.

using System;
using System.Threading;
using System.Threading.Tasks;

public class Lambda
{
   public static void Main()
   {
      Thread.CurrentThread.Name = "Main";

      // Create a task and supply a user delegate by using a lambda expression.
      Task taskA = new Task( () => Console.WriteLine("Hello from taskA."));
      // Start the task.
      taskA.Start();

      // Output a message from the calling thread.
      Console.WriteLine("Hello from thread '{0}'.",
                        Thread.CurrentThread.Name);
      taskA.Wait();
   }
}
// The example displays output as follows:
//       Hello from thread 'Main'.
//       Hello from taskA.
// or
//       Hello from taskA.
//       Hello from thread 'Main'.
Imports System.Threading

Namespace Lambda
    Module Example
        Public Sub Main()
            Thread.CurrentThread.Name = "Main"

            ' Create a task and supply a user delegate by using a lambda expression. 
            Dim taskA = New Task(Sub() Console.WriteLine("Hello from taskA."))
            ' Start the task.
            taskA.Start()

            ' Output a message from the calling thread.
            Console.WriteLine("Hello from thread '{0}'.",
                            Thread.CurrentThread.Name)
            taskA.Wait()
        End Sub
    End Module
    ' The example displays output like the following:
    '    Hello from thread 'Main'.
    '    Hello from taskA.
End Namespace

Task.Run 메서드를 사용하여 한 번에 작업을 만들고 시작할 수도 있습니다. 작업을 관리하기 위해 Run 메서드는 기본 작업 스케줄러를 사용합니다. 이 경우 작업 스케줄러가 현재 스레드와 연결되었는지 여부는 관계없습니다. Run 메서드는 작업을 만들고 작업 일정을 예약할 때 더 자세히 제어할 필요가 없는 경우 작업을 만들고 시작하는 데 가장 많이 사용되는 방법입니다.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Run;

public class Example
{
   public static void Main()
   {
      Thread.CurrentThread.Name = "Main";

      // Define and run the task.
      Task taskA = Task.Run( () => Console.WriteLine("Hello from taskA."));

      // Output a message from the calling thread.
      Console.WriteLine("Hello from thread '{0}'.",
                          Thread.CurrentThread.Name);
      taskA.Wait();
   }
}
// The example displays output as follows:
//       Hello from thread 'Main'.
//       Hello from taskA.
// or
//       Hello from taskA.
//       Hello from thread 'Main'.
Imports System.Threading

Namespace Run
    Module Example
        Public Sub Main()
            Thread.CurrentThread.Name = "Main"

            Dim taskA As Task = Task.Run(Sub() Console.WriteLine("Hello from taskA."))

            ' Output a message from the calling thread.
            Console.WriteLine("Hello from thread '{0}'.",
                            Thread.CurrentThread.Name)
            taskA.Wait()
        End Sub
    End Module
    ' The example displays output like the following:
    '    Hello from thread 'Main'.
    '    Hello from taskA.
End Namespace

TaskFactory.StartNew 메서드를 사용하여 한 번에 작업을 만들고 시작할 수도 있습니다. 다음 예에 표시된 대로 다음과 같은 경우에 이 메서드를 사용할 수 있습니다.

  • 만들기와 예약을 분리할 필요는 없으며 추가 작업 만들기 옵션이나 특정 스케줄러의 사용이 필요합니다.

  • Task.AsyncState 속성을 통해 검색할 수 있는 작업에 추가 상태를 전달해야 합니다.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskIntro;

class CustomData
{
    public long CreationTime;
    public int Name;
    public int ThreadNum;
}

public class AsyncState
{
    public static void Main()
    {
        Task[] taskArray = new Task[10];
        for (int i = 0; i < taskArray.Length; i++)
        {
            taskArray[i] = Task.Factory.StartNew((Object obj) =>
            {
                CustomData data = obj as CustomData;
                if (data == null) return;

                data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
            },
            new CustomData() { Name = i, CreationTime = DateTime.Now.Ticks });
        }
        Task.WaitAll(taskArray);
        foreach (var task in taskArray)
        {
            var data = task.AsyncState as CustomData;
            if (data != null)
                Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
                                  data.Name, data.CreationTime, data.ThreadNum);
        }
    }
}
// The example displays output like the following:
//     Task #0 created at 635116412924597583, ran on thread #3.
//     Task #1 created at 635116412924607584, ran on thread #4.
//     Task #2 created at 635116412924607584, ran on thread #4.
//     Task #3 created at 635116412924607584, ran on thread #4.
//     Task #4 created at 635116412924607584, ran on thread #3.
//     Task #5 created at 635116412924607584, ran on thread #3.
//     Task #6 created at 635116412924607584, ran on thread #4.
//     Task #7 created at 635116412924607584, ran on thread #4.
//     Task #8 created at 635116412924607584, ran on thread #3.
//     Task #9 created at 635116412924607584, ran on thread #4.
Imports System.Threading

Namespace AsyncState
    Class CustomData
        Public CreationTime As Long
        Public Name As Integer
        Public ThreadNum As Integer
    End Class

    Module Example
        Public Sub Main()
            Dim taskArray(9) As Task
            For i As Integer = 0 To taskArray.Length - 1
                taskArray(i) = Task.Factory.StartNew(Sub(obj As Object)
                                                         Dim data As CustomData = TryCast(obj, CustomData)
                                                         If data Is Nothing Then Return

                                                         data.ThreadNum = Environment.CurrentManagedThreadId
                                                     End Sub,
                New CustomData With {.Name = i, .CreationTime = Date.Now.Ticks})
            Next
            Task.WaitAll(taskArray)

            For Each task In taskArray
                Dim data = TryCast(task.AsyncState, CustomData)
                If data IsNot Nothing Then
                    Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
                                    data.Name, data.CreationTime, data.ThreadNum)
                End If
            Next
        End Sub
    End Module
    ' The example displays output like the following:
    '     Task #0 created at 635116412924597583, ran on thread #3.
    '     Task #1 created at 635116412924607584, ran on thread #4.
    '     Task #2 created at 635116412924607584, ran on thread #4.
    '     Task #3 created at 635116412924607584, ran on thread #4.
    '     Task #4 created at 635116412924607584, ran on thread #3.
    '     Task #5 created at 635116412924607584, ran on thread #3.
    '     Task #6 created at 635116412924607584, ran on thread #4.
    '     Task #7 created at 635116412924607584, ran on thread #4.
    '     Task #8 created at 635116412924607584, ran on thread #3.
    '     Task #9 created at 635116412924607584, ran on thread #4.
End Namespace

TaskTask<TResult>는 각각 Factory의 기본 인스턴스를 반환하는 정적 TaskFactory 속성을 노출하므로 메서드를 Task.Factory.StartNew()로 호출할 수 있습니다. 또한 다음 예제에서 작업은 System.Threading.Tasks.Task<TResult> 형식이므로 계산 결과가 들어 있는 공용 Task<TResult>.Result 속성이 각 작업에 포함됩니다. 작업은 비동기식으로 실행되며 어떤 순서로든 완료될 수 있습니다. 계산이 완료되기 전에 Result 속성에 액세스할 경우 이 속성이 값을 사용할 수 있을 때까지 호출 스레드를 차단합니다.

using System;
using System.Threading.Tasks;

public class Result
{
   public static void Main()
   {
        Task<Double>[] taskArray = { Task<Double>.Factory.StartNew(() => DoComputation(1.0)),
                                     Task<Double>.Factory.StartNew(() => DoComputation(100.0)),
                                     Task<Double>.Factory.StartNew(() => DoComputation(1000.0)) };

        var results = new Double[taskArray.Length];
        Double sum = 0;

        for (int i = 0; i < taskArray.Length; i++) {
            results[i] = taskArray[i].Result;
            Console.Write("{0:N1} {1}", results[i],
                              i == taskArray.Length - 1 ? "= " : "+ ");
            sum += results[i];
        }
        Console.WriteLine("{0:N1}", sum);
   }

   private static Double DoComputation(Double start)
   {
      Double sum = 0;
      for (var value = start; value <= start + 10; value += .1)
         sum += value;

      return sum;
   }
}
// The example displays the following output:
//        606.0 + 10,605.0 + 100,495.0 = 111,706.0

Namespace Result
    Module Example
        Public Sub Main()
            Dim taskArray() = {Task(Of Double).Factory.StartNew(Function() DoComputation(1.0)),
                Task(Of Double).Factory.StartNew(Function() DoComputation(100.0)),
                Task(Of Double).Factory.StartNew(Function() DoComputation(1000.0))}

            Dim results(taskArray.Length - 1) As Double
            Dim sum As Double

            For i As Integer = 0 To taskArray.Length - 1
                results(i) = taskArray(i).Result
                Console.Write("{0:N1} {1}", results(i),
                    If(i = taskArray.Length - 1, "= ", "+ "))
                sum += results(i)
            Next
            Console.WriteLine("{0:N1}", sum)
        End Sub

        Private Function DoComputation(start As Double) As Double
            Dim sum As Double
            For value As Double = start To start + 10 Step .1
                sum += value
            Next
            Return sum
        End Function
    End Module
    ' The example displays the following output:
    '       606.0 + 10,605.0 + 100,495.0 = 111,706.0
End Namespace

자세한 내용은 방법: 작업에서 값 반환을 참조하세요.

람다 식을 사용하여 대리자를 만드는 경우 소스 코드의 해당 지점에서 표시되는 모든 변수에 액세스할 수 있습니다. 그러나 특히 루프 내에서 람다가 기대한 대로 변수를 캡처하지 않는 경우가 있습니다. 각 반복 후에 변경되므로 값이 아닌 변수의 참조만 캡처합니다. 다음 예제에서 이 문제를 보여줍니다. CustomData 개체를 인스턴스화하고 루프 카운터를 개체 식별자로 사용하는 람다 식에 루프 카운터를 전달합니다. 이 예제의 출력에서 표시되는 것처럼 각 CustomData 개체에는 동일한 식별자가 있습니다.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace Example.Iterations;

class CustomData
{
   public long CreationTime;
   public int Name;
   public int ThreadNum;
}

public class IterationTwo
{
   public static void Main()
   {
      // Create the task object by using an Action(Of Object) to pass in the loop
      // counter. This produces an unexpected result.
      Task[] taskArray = new Task[10];
      for (int i = 0; i < taskArray.Length; i++) {
         taskArray[i] = Task.Factory.StartNew( (Object obj) => {
                                                 var data = new CustomData() {Name = i, CreationTime = DateTime.Now.Ticks};
                                                 data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
                                                 Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
                                                                   data.Name, data.CreationTime, data.ThreadNum);
                                               },
                                              i );
      }
      Task.WaitAll(taskArray);
   }
}
// The example displays output like the following:
//       Task #10 created at 635116418427727841 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427737842 on thread #4.
//       Task #10 created at 635116418427727841 on thread #3.
//       Task #10 created at 635116418427747843 on thread #3.
//       Task #10 created at 635116418427747843 on thread #3.
//       Task #10 created at 635116418427737842 on thread #4.
Imports System.Threading

Namespace IterationsTwo
    Class CustomData
        Public CreationTime As Long
        Public Name As Integer
        Public ThreadNum As Integer
    End Class

    Module Example
        Public Sub Main()
            ' Create the task object by using an Action(Of Object) to pass in the loop
            ' counter. This produces an unexpected result.
            Dim taskArray(9) As Task
            For i As Integer = 0 To taskArray.Length - 1
                taskArray(i) = Task.Factory.StartNew(Sub(obj As Object)
                                                         Dim data As New CustomData With {.Name = i, .CreationTime = Date.Now.Ticks}
                                                         data.ThreadNum = Environment.CurrentManagedThreadId
                                                         Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
                                                                         data.Name, data.CreationTime, data.ThreadNum)
                                                     End Sub,
                    i)
            Next
            Task.WaitAll(taskArray)
        End Sub
    End Module
    ' The example displays output like the following:
    '       Task #10 created at 635116418427727841 on thread #4.
    '       Task #10 created at 635116418427737842 on thread #4.
    '       Task #10 created at 635116418427737842 on thread #4.
    '       Task #10 created at 635116418427737842 on thread #4.
    '       Task #10 created at 635116418427737842 on thread #4.
    '       Task #10 created at 635116418427737842 on thread #4.
    '       Task #10 created at 635116418427727841 on thread #3.
    '       Task #10 created at 635116418427747843 on thread #3.
    '       Task #10 created at 635116418427747843 on thread #3.
    '       Task #10 created at 635116418427737842 on thread #4.
End Namespace

생성자를 통해 작업에 상태 개체를 제공하여 각 반복의 값에 액세스할 수 있습니다. 다음 예제에서는 CustomData 개체를 만들 때 루프 카운터를 사용하여 이전 예제를 수정하여 람다 식에 전달되도록 합니다. 예제 출력에서 보듯이 각 CustomData 개체는 이제 개체를 인스턴스화한 시간에 루프 카운터의 값을 기반으로 한 고유 식별자가 있습니다.

using System;
using System.Threading;
using System.Threading.Tasks;

class CustomData
{
   public long CreationTime;
   public int Name;
   public int ThreadNum;
}

public class IterationOne
{
   public static void Main()
   {
      // Create the task object by using an Action(Of Object) to pass in custom data
      // to the Task constructor. This is useful when you need to capture outer variables
      // from within a loop.
      Task[] taskArray = new Task[10];
      for (int i = 0; i < taskArray.Length; i++) {
         taskArray[i] = Task.Factory.StartNew( (Object obj ) => {
                                                  CustomData data = obj as CustomData;
                                                  if (data == null)
                                                     return;

                                                  data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
                                                  Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
                                                                   data.Name, data.CreationTime, data.ThreadNum);
                                               },
                                               new CustomData() {Name = i, CreationTime = DateTime.Now.Ticks} );
      }
      Task.WaitAll(taskArray);
   }
}
// The example displays output like the following:
//       Task #0 created at 635116412924597583 on thread #3.
//       Task #1 created at 635116412924607584 on thread #4.
//       Task #3 created at 635116412924607584 on thread #4.
//       Task #4 created at 635116412924607584 on thread #4.
//       Task #2 created at 635116412924607584 on thread #3.
//       Task #6 created at 635116412924607584 on thread #3.
//       Task #5 created at 635116412924607584 on thread #4.
//       Task #8 created at 635116412924607584 on thread #4.
//       Task #7 created at 635116412924607584 on thread #3.
//       Task #9 created at 635116412924607584 on thread #4.
Imports System.Threading

Namespace IterationsOne
    Class CustomData
        Public CreationTime As Long
        Public Name As Integer
        Public ThreadNum As Integer
    End Class

    Module Example
        Public Sub Main()
            ' Create the task object by using an Action(Of Object) to pass in custom data
            ' to the Task constructor. This is useful when you need to capture outer variables
            ' from within a loop. 
            Dim taskArray(9) As Task
            For i As Integer = 0 To taskArray.Length - 1
                taskArray(i) = Task.Factory.StartNew(Sub(obj As Object)
                                                         Dim data As CustomData = TryCast(obj, CustomData)
                                                         If data Is Nothing Then Return

                                                         data.ThreadNum = Environment.CurrentManagedThreadId
                                                         Console.WriteLine("Task #{0} created at {1} on thread #{2}.",
                                                                         data.Name, data.CreationTime, data.ThreadNum)
                                                     End Sub,
                    New CustomData With {.Name = i, .CreationTime = Date.Now.Ticks})
            Next
            Task.WaitAll(taskArray)
        End Sub
    End Module
    ' The example displays output like the following:
    '       Task #0 created at 635116412924597583 on thread #3.
    '       Task #1 created at 635116412924607584 on thread #4.
    '       Task #3 created at 635116412924607584 on thread #4.
    '       Task #4 created at 635116412924607584 on thread #4.
    '       Task #2 created at 635116412924607584 on thread #3.
    '       Task #6 created at 635116412924607584 on thread #3.
    '       Task #5 created at 635116412924607584 on thread #4.
    '       Task #8 created at 635116412924607584 on thread #4.
    '       Task #7 created at 635116412924607584 on thread #3.
    '       Task #9 created at 635116412924607584 on thread #4.
End Namespace

이 상태는 작업 대리자에 인수로 전달되며 작업 개체에서 Task.AsyncState 속성을 사용하여 액세스할 수 있습니다. 다음 예제는 이전 예제를 약간 변형한 예제입니다. AsyncState 속성을 사용하여 람다 식에 전달된 CustomData 개체에 대한 정보를 표시합니다.

using System;
using System.Threading;
using System.Threading.Tasks;

namespace TaskIntro;

class CustomData
{
    public long CreationTime;
    public int Name;
    public int ThreadNum;
}

public class AsyncState
{
    public static void Main()
    {
        Task[] taskArray = new Task[10];
        for (int i = 0; i < taskArray.Length; i++)
        {
            taskArray[i] = Task.Factory.StartNew((Object obj) =>
            {
                CustomData data = obj as CustomData;
                if (data == null) return;

                data.ThreadNum = Thread.CurrentThread.ManagedThreadId;
            },
            new CustomData() { Name = i, CreationTime = DateTime.Now.Ticks });
        }
        Task.WaitAll(taskArray);
        foreach (var task in taskArray)
        {
            var data = task.AsyncState as CustomData;
            if (data != null)
                Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
                                  data.Name, data.CreationTime, data.ThreadNum);
        }
    }
}
// The example displays output like the following:
//     Task #0 created at 635116412924597583, ran on thread #3.
//     Task #1 created at 635116412924607584, ran on thread #4.
//     Task #2 created at 635116412924607584, ran on thread #4.
//     Task #3 created at 635116412924607584, ran on thread #4.
//     Task #4 created at 635116412924607584, ran on thread #3.
//     Task #5 created at 635116412924607584, ran on thread #3.
//     Task #6 created at 635116412924607584, ran on thread #4.
//     Task #7 created at 635116412924607584, ran on thread #4.
//     Task #8 created at 635116412924607584, ran on thread #3.
//     Task #9 created at 635116412924607584, ran on thread #4.
Imports System.Threading

Namespace AsyncState
    Class CustomData
        Public CreationTime As Long
        Public Name As Integer
        Public ThreadNum As Integer
    End Class

    Module Example
        Public Sub Main()
            Dim taskArray(9) As Task
            For i As Integer = 0 To taskArray.Length - 1
                taskArray(i) = Task.Factory.StartNew(Sub(obj As Object)
                                                         Dim data As CustomData = TryCast(obj, CustomData)
                                                         If data Is Nothing Then Return

                                                         data.ThreadNum = Environment.CurrentManagedThreadId
                                                     End Sub,
                New CustomData With {.Name = i, .CreationTime = Date.Now.Ticks})
            Next
            Task.WaitAll(taskArray)

            For Each task In taskArray
                Dim data = TryCast(task.AsyncState, CustomData)
                If data IsNot Nothing Then
                    Console.WriteLine("Task #{0} created at {1}, ran on thread #{2}.",
                                    data.Name, data.CreationTime, data.ThreadNum)
                End If
            Next
        End Sub
    End Module
    ' The example displays output like the following:
    '     Task #0 created at 635116412924597583, ran on thread #3.
    '     Task #1 created at 635116412924607584, ran on thread #4.
    '     Task #2 created at 635116412924607584, ran on thread #4.
    '     Task #3 created at 635116412924607584, ran on thread #4.
    '     Task #4 created at 635116412924607584, ran on thread #3.
    '     Task #5 created at 635116412924607584, ran on thread #3.
    '     Task #6 created at 635116412924607584, ran on thread #4.
    '     Task #7 created at 635116412924607584, ran on thread #4.
    '     Task #8 created at 635116412924607584, ran on thread #3.
    '     Task #9 created at 635116412924607584, ran on thread #4.
End Namespace

작업 ID

모든 작업은 애플리케이션 도메인에서 작업을 고유하게 식별하는 정수 ID를 받으며, 이 ID는 Task.Id 속성을 사용하여 액세스할 수 있습니다. ID는 Visual Studio 디버거의 병렬 스택작업 창에서 작업 정보를 보는 데 유용합니다. ID는 느리게 만들어집니다. 즉, 요청될 때까지 만들어지지 않습니다. 따라서 프로그램이 실행될 때마다 작업의 ID가 다를 수 있습니다. 디버거에서 작업 ID를 보는 방법에 대한 자세한 내용은 작업 창 사용병렬 스택 창 사용을 참조하세요.

작업 생성 옵션

작업을 만드는 대부분의 API는 TaskCreationOptions 매개 변수를 사용하는 오버로드를 제공합니다. 관련 옵션 중 하나 이상을 지정하여 스레드 풀에서 작업을 예약하는 방법을 작업 스케줄러에 지시할 수 있습니다. 비트별 OR 연산을 사용하여 옵션을 결합할 수 있습니다.

다음 예제에서는 LongRunningPreferFairness 옵션이 있는 작업을 보여 줍니다.

var task3 = new Task(() => MyLongRunningMethod(),
                    TaskCreationOptions.LongRunning | TaskCreationOptions.PreferFairness);
task3.Start();

Dim task3 = New Task(Sub() MyLongRunningMethod(),
                        TaskCreationOptions.LongRunning Or TaskCreationOptions.PreferFairness)
task3.Start()

작업, 스레드 및 문화권

각 스레드에는 각각 Thread.CurrentCultureThread.CurrentUICulture 속성으로 정의된 관련 문화권 및 UI 문화권이 있습니다. 스레드의 문화권은 서식 지정, 구문 분석, 정렬 및 문자열 비교 작업과 같은 작업에 사용됩니다. 스레드의 UI 문화권은 리소스 조회에서 사용됩니다.

시스템 문화권은 스레드의 기본 문화권과 UI 문화권을 정의합니다. 그러나 CultureInfo.DefaultThreadCurrentCultureCultureInfo.DefaultThreadCurrentUICulture 속성을 사용하여 애플리케이션 도메인의 모든 스레드에 대한 기본 문화권을 지정할 수 있습니다. 스레드의 문화권을 명시적으로 설정하고 새 스레드를 시작하는 경우 새 스레드는 호출 스레드의 문화권을 상속하지 않습니다. 대신, 해당 문화권은 기본 시스템 문화권입니다. 그러나 작업 기반 프로그래밍에서 작업은 다른 스레드에서 비동기적으로 실행되는 경우에도 호출 스레드의 문화권을 사용합니다.

다음 예제에서는 간단한 설명을 제공합니다. 앱의 현재 문화권을 프랑스어(프랑스)로 변경합니다. 프랑스어(프랑스)가 이미 현재 문화권인 경우 영어(미국)로 변경됩니다. 그런 다음 새 문화권의 통화 값으로 형식이 지정된 일부 숫자를 반환하는 formatDelegate라는 대리자를 호출합니다. 대리자가 작업에서 동기적으로 또는 비동기적으로 호출되는지와 관계없이 작업은 호출 스레드의 문화권을 사용합니다.

using System;
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;

public class Example
{
   public static void Main()
   {
       decimal[] values = { 163025412.32m, 18905365.59m };
       string formatString = "C2";
       Func<String> formatDelegate = () => { string output = String.Format("Formatting using the {0} culture on thread {1}.\n",
                                                                           CultureInfo.CurrentCulture.Name,
                                                                           Thread.CurrentThread.ManagedThreadId);
                                             foreach (var value in values)
                                                output += String.Format("{0}   ", value.ToString(formatString));

                                             output += Environment.NewLine;
                                             return output;
                                           };

       Console.WriteLine("The example is running on thread {0}",
                         Thread.CurrentThread.ManagedThreadId);
       // Make the current culture different from the system culture.
       Console.WriteLine("The current culture is {0}",
                         CultureInfo.CurrentCulture.Name);
       if (CultureInfo.CurrentCulture.Name == "fr-FR")
          Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
       else
          Thread.CurrentThread.CurrentCulture = new CultureInfo("fr-FR");

       Console.WriteLine("Changed the current culture to {0}.\n",
                         CultureInfo.CurrentCulture.Name);

       // Execute the delegate synchronously.
       Console.WriteLine("Executing the delegate synchronously:");
       Console.WriteLine(formatDelegate());

       // Call an async delegate to format the values using one format string.
       Console.WriteLine("Executing a task asynchronously:");
       var t1 = Task.Run(formatDelegate);
       Console.WriteLine(t1.Result);

       Console.WriteLine("Executing a task synchronously:");
       var t2 = new Task<String>(formatDelegate);
       t2.RunSynchronously();
       Console.WriteLine(t2.Result);
   }
}
// The example displays the following output:
//         The example is running on thread 1
//         The current culture is en-US
//         Changed the current culture to fr-FR.
//
//         Executing the delegate synchronously:
//         Formatting using the fr-FR culture on thread 1.
//         163 025 412,32 €   18 905 365,59 €
//
//         Executing a task asynchronously:
//         Formatting using the fr-FR culture on thread 3.
//         163 025 412,32 €   18 905 365,59 €
//
//         Executing a task synchronously:
//         Formatting using the fr-FR culture on thread 1.
//         163 025 412,32 €   18 905 365,59 €
Imports System.Globalization
Imports System.Threading

Module Example
    Public Sub Main()
        Dim values() As Decimal = {163025412.32D, 18905365.59D}
        Dim formatString As String = "C2"
        Dim formatDelegate As Func(Of String) = Function()
                                                    Dim output As String = String.Format("Formatting using the {0} culture on thread {1}.",
                                                                                         CultureInfo.CurrentCulture.Name,
                                                                                         Thread.CurrentThread.ManagedThreadId)
                                                    output += Environment.NewLine
                                                    For Each value In values
                                                        output += String.Format("{0}   ", value.ToString(formatString))
                                                    Next
                                                    output += Environment.NewLine
                                                    Return output
                                                End Function

        Console.WriteLine("The example is running on thread {0}",
                          Thread.CurrentThread.ManagedThreadId)
        ' Make the current culture different from the system culture.
        Console.WriteLine("The current culture is {0}",
                          CultureInfo.CurrentCulture.Name)
        If CultureInfo.CurrentCulture.Name = "fr-FR" Then
            Thread.CurrentThread.CurrentCulture = New CultureInfo("en-US")
        Else
            Thread.CurrentThread.CurrentCulture = New CultureInfo("fr-FR")
        End If
        Console.WriteLine("Changed the current culture to {0}.",
                          CultureInfo.CurrentCulture.Name)
        Console.WriteLine()

        ' Execute the delegate synchronously.
        Console.WriteLine("Executing the delegate synchronously:")
        Console.WriteLine(formatDelegate())

        ' Call an async delegate to format the values using one format string.
        Console.WriteLine("Executing a task asynchronously:")
        Dim t1 = Task.Run(formatDelegate)
        Console.WriteLine(t1.Result)

        Console.WriteLine("Executing a task synchronously:")
        Dim t2 = New Task(Of String)(formatDelegate)
        t2.RunSynchronously()
        Console.WriteLine(t2.Result)
    End Sub
End Module

' The example displays the following output:
'
'          The example is running on thread 1
'          The current culture is en-US
'          Changed the current culture to fr-FR.
'
'          Executing the delegate synchronously:
'          Formatting Imports the fr-FR culture on thread 1.
'          163 025 412,32 €   18 905 365,59 €
'
'          Executing a task asynchronously:
'          Formatting Imports the fr-FR culture on thread 3.
'          163 025 412,32 €   18 905 365,59 €
'
'          Executing a task synchronously:
'          Formatting Imports the fr-FR culture on thread 1.
'          163 025 412,32 €   18 905 365,59 €

참고 항목

.NET Framework 4.6 이전의 .NET Framework 버전에서 작업의 문화권은 ‘호출 스레드’의 문화권이 아니라 ‘실행’하는 스레드의 문화권에 의해 결정됩니다. 비동기 작업의 경우 작업에서 사용하는 문화권은 호출 스레드의 문화권과 다를 수 있습니다.

비동기 작업 및 문화권에 대한 자세한 내용은 CultureInfo 문서의 "문화권 및 비동기 작업 기반 작업" 섹션을 참조하세요.

작업 연속 만들기

Task.ContinueWithTask<TResult>.ContinueWith 메서드를 사용하면 ‘선행 작업’이 완료될 때 시작할 작업을 지정할 수 있습니다. 연속 작업의 대리자는 선행 작업의 상태를 검사할 수 있도록 선행 작업에 대한 참조를 전달합니다. 그리고 Task<TResult>.Result 속성의 값을 검색하여 선행 항목의 출력을 연속에 대한 입력으로 사용할 수 있습니다.

다음 예제에서는 getData 작업이 TaskFactory.StartNew<TResult>(Func<TResult>) 메서드에 대한 호출로 시작됩니다. processData작업은 getData가 완료되면 자동으로 시작되고 displayDataprocessData가 완료되면 시작됩니다. getDataprocessData 작업의 getData 속성을 통해 Task<TResult>.Result 작업에 액세스할 수 있는 정수 배열을 생성합니다. processData 작업은 해당 배열을 처리하고 Task<TResult>.ContinueWith<TNewResult>(Func<Task<TResult>,TNewResult>) 메서드로 전달된 람다 식의 반환 형식에서 유추한 형식을 갖는 결과를 반환합니다. displayData 작업은 processData가 완료되면 자동으로 실행되며 Tuple<T1,T2,T3> 람다 식에서 반환한 processData 개체는 displayData 작업의 processData 속성을 통해 Task<TResult>.Result 작업에 액세스할 수 있습니다. displayData 작업은 processData 작업의 결과를 사용합니다. 유사한 방식으로 형식이 유추되고 Result 속성에서 프로그램에 사용할 수 있는 결과를 생성합니다.

using System;
using System.Threading.Tasks;

public class ContinuationOne
{
   public static void Main()
   {
      var getData = Task.Factory.StartNew(() => {
                                             Random rnd = new Random();
                                             int[] values = new int[100];
                                             for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
                                                values[ctr] = rnd.Next();

                                             return values;
                                          } );
      var processData = getData.ContinueWith((x) => {
                                                int n = x.Result.Length;
                                                long sum = 0;
                                                double mean;

                                                for (int ctr = 0; ctr <= x.Result.GetUpperBound(0); ctr++)
                                                   sum += x.Result[ctr];

                                                mean = sum / (double) n;
                                                return Tuple.Create(n, sum, mean);
                                             } );
      var displayData = processData.ContinueWith((x) => {
                                                    return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
                                                                         x.Result.Item1, x.Result.Item2,
                                                                         x.Result.Item3);
                                                 } );
      Console.WriteLine(displayData.Result);
   }
}
// The example displays output similar to the following:
//    N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82

Namespace ContinuationsOne
    Module Example
        Public Sub Main()
            Dim getData = Task.Factory.StartNew(Function()
                                                    Dim rnd As New Random()
                                                    Dim values(99) As Integer
                                                    For ctr = 0 To values.GetUpperBound(0)
                                                        values(ctr) = rnd.Next()
                                                    Next
                                                    Return values
                                                End Function)
            Dim processData = getData.ContinueWith(Function(x)
                                                       Dim n As Integer = x.Result.Length
                                                       Dim sum As Long
                                                       Dim mean As Double

                                                       For ctr = 0 To x.Result.GetUpperBound(0)
                                                           sum += x.Result(ctr)
                                                       Next
                                                       mean = sum / n
                                                       Return Tuple.Create(n, sum, mean)
                                                   End Function)
            Dim displayData = processData.ContinueWith(Function(x)
                                                           Return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
                                                                                   x.Result.Item1, x.Result.Item2,
                                                                                   x.Result.Item3)
                                                       End Function)
            Console.WriteLine(displayData.Result)
        End Sub
    End Module
    ' The example displays output like the following:
    '   N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82
End Namespace

Task.ContinueWith는 인스턴스 메서드이므로 각 선행 작업에 대해 Task<TResult> 개체를 인스턴스화하는 대신 메서드를 연속 호출할 수 있습니다. 다음 예제는 Task.ContinueWith 메서드에 대한 호출도 함께 연결된다는 점을 제외하고 이전 예제와 기능적으로 동일합니다. 메서드 호출 체인에 의해 반환된 Task<TResult> 개체는 최종 연속 작업입니다.

using System;
using System.Threading.Tasks;

public class ContinuationTwo
{
   public static void Main()
   {
      var displayData = Task.Factory.StartNew(() => {
                                                 Random rnd = new Random();
                                                 int[] values = new int[100];
                                                 for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
                                                    values[ctr] = rnd.Next();

                                                 return values;
                                              } ).
                        ContinueWith((x) => {
                                        int n = x.Result.Length;
                                        long sum = 0;
                                        double mean;

                                        for (int ctr = 0; ctr <= x.Result.GetUpperBound(0); ctr++)
                                           sum += x.Result[ctr];

                                        mean = sum / (double) n;
                                        return Tuple.Create(n, sum, mean);
                                     } ).
                        ContinueWith((x) => {
                                        return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
                                                             x.Result.Item1, x.Result.Item2,
                                                             x.Result.Item3);
                                     } );
      Console.WriteLine(displayData.Result);
   }
}
// The example displays output similar to the following:
//    N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82

Namespace ContinuationsTwo
    Module Example
        Public Sub Main()
            Dim displayData = Task.Factory.StartNew(Function()
                                                        Dim rnd As New Random()
                                                        Dim values(99) As Integer
                                                        For ctr = 0 To values.GetUpperBound(0)
                                                            values(ctr) = rnd.Next()
                                                        Next
                                                        Return values
                                                    End Function). _
            ContinueWith(Function(x)
                             Dim n As Integer = x.Result.Length
                             Dim sum As Long
                             Dim mean As Double

                             For ctr = 0 To x.Result.GetUpperBound(0)
                                 sum += x.Result(ctr)
                             Next
                             mean = sum / n
                             Return Tuple.Create(n, sum, mean)
                         End Function). _
            ContinueWith(Function(x)
                             Return String.Format("N={0:N0}, Total = {1:N0}, Mean = {2:N2}",
                                                 x.Result.Item1, x.Result.Item2,
                                                 x.Result.Item3)
                         End Function)
            Console.WriteLine(displayData.Result)
        End Sub
    End Module
    ' The example displays output like the following:
    '   N=100, Total = 110,081,653,682, Mean = 1,100,816,536.82
End Namespace

ContinueWhenAllContinueWhenAny 메서드를 사용하면 여러 작업을 연속적으로 수행할 수 있습니다.

자세한 내용은 연속 작업을 사용하여 작업 연결을 참조하세요.

분리된 자식 작업 만들기

작업에서 실행 중인 사용자 코드가 새 작업을 만들고 AttachedToParent 옵션을 지정하지 않으면 새 작업은 특별한 방식으로 부모 작업과 동기화되지 않습니다. 이 유형의 동기화되지 않은 작업을 분리된 중첩 작업 또는 분리된 자식 작업이라고 부릅니다. 다음 예제에서는 분리된 상태의 자식 작업을 하나 만드는 작업을 보여 줍니다.

var outer = Task.Factory.StartNew(() =>
{
    Console.WriteLine("Outer task beginning.");

    var child = Task.Factory.StartNew(() =>
    {
        Thread.SpinWait(5000000);
        Console.WriteLine("Detached task completed.");
    });
});

outer.Wait();
Console.WriteLine("Outer task completed.");
// The example displays the following output:
//    Outer task beginning.
//    Outer task completed.
//    Detached task completed.
Dim outer = Task.Factory.StartNew(Sub()
                                      Console.WriteLine("Outer task beginning.")
                                      Dim child = Task.Factory.StartNew(Sub()
                                                                            Thread.SpinWait(5000000)
                                                                            Console.WriteLine("Detached task completed.")
                                                                        End Sub)
                                  End Sub)
outer.Wait()
Console.WriteLine("Outer task completed.")
' The example displays the following output:
'     Outer task beginning.
'     Outer task completed.
'    Detached child completed.

참고 항목

부모 작업은 분리된 자식 작업이 완료될 때까지 기다리지 않습니다.

자식 작업 만들기

작업에서 실행되는 사용자 코드에 AttachedToParent 옵션을 사용하여 작업을 만드는 경우 새 작업을 부모 작업의 연결된 자식 작업이라고 합니다. AttachedToParent 옵션을 사용하면 부모 작업은 암시적으로 모든 연결된 자식 작업이 완료될 때까지 대기하게 되므로 이 옵션을 사용하여 구조적 작업 병렬 처리를 표현할 수 있습니다. 다음 예에서는 10개의 연결된 자식 작업을 만드는 부모 작업을 보여 줍니다. 이 예에서는 Task.Wait 메서드를 호출하여 부모 작업이 완료될 때까지 기다립니다. 연결된 자식 작업이 완료될 때까지 명시적으로 기다릴 필요는 없습니다.

using System;
using System.Threading;
using System.Threading.Tasks;

public class Child
{
   public static void Main()
   {
      var parent = Task.Factory.StartNew(() => {
                      Console.WriteLine("Parent task beginning.");
                      for (int ctr = 0; ctr < 10; ctr++) {
                         int taskNo = ctr;
                         Task.Factory.StartNew((x) => {
                                                  Thread.SpinWait(5000000);
                                                  Console.WriteLine("Attached child #{0} completed.",
                                                                    x);
                                               },
                                               taskNo, TaskCreationOptions.AttachedToParent);
                      }
                   });

      parent.Wait();
      Console.WriteLine("Parent task completed.");
   }
}
// The example displays output like the following:
//       Parent task beginning.
//       Attached child #9 completed.
//       Attached child #0 completed.
//       Attached child #8 completed.
//       Attached child #1 completed.
//       Attached child #7 completed.
//       Attached child #2 completed.
//       Attached child #6 completed.
//       Attached child #3 completed.
//       Attached child #5 completed.
//       Attached child #4 completed.
//       Parent task completed.
Imports System.Threading

Namespace Child
    Module Example
        Public Sub Main()
            Dim parent = Task.Factory.StartNew(Sub()
                                                   Console.WriteLine("Parent task beginning.")
                                                   For ctr As Integer = 0 To 9
                                                       Dim taskNo As Integer = ctr
                                                       Task.Factory.StartNew(Sub(x)
                                                                                 Thread.SpinWait(5000000)
                                                                                 Console.WriteLine("Attached child #{0} completed.",
                                                                                                 x)
                                                                             End Sub,
                                                       taskNo, TaskCreationOptions.AttachedToParent)
                                                   Next
                                               End Sub)
            parent.Wait()
            Console.WriteLine("Parent task completed.")
        End Sub
    End Module
    ' The example displays output like the following:
    '       Parent task beginning.
    '       Attached child #9 completed.
    '       Attached child #0 completed.
    '       Attached child #8 completed.
    '       Attached child #1 completed.
    '       Attached child #7 completed.
    '       Attached child #2 completed.
    '       Attached child #6 completed.
    '       Attached child #3 completed.
    '       Attached child #5 completed.
    '       Attached child #4 completed.
    '       Parent task completed.
End Namespace

부모 작업은 TaskCreationOptions.DenyChildAttach 옵션을 사용하여 부모 작업에 다른 작업이 첨부되지 않도록 할 수 있습니다. 자세한 내용은 연결된 자식 작업과 분리된 자식 작업을 참조하세요.

작업 완료 시까지 대기

System.Threading.Tasks.TaskSystem.Threading.Tasks.Task<TResult> 형식은 작업이 완료될 때까지 대기할 수 있게 해주는 Task.Wait 메서드의 몇 가지 오버로드를 제공합니다. 또한 정적 Task.WaitAllTask.WaitAny 메서드의 오버로드를 사용하면 작업 배열의 일부 또는 모두가 완료될 때까지 대기할 수 있습니다.

일반적으로 다음과 같은 경우에 작업을 대기시킵니다.

  • 주 스레드에서 작업에 의해 계산된 최종 결과를 사용해야 하는 경우

  • 작업에서 throw될 수 있는 예외를 처리해야 하는 경우

  • 모든 작업이 실행을 완료하기 전에 애플리케이션이 종료될 수 있습니다. 예를 들어, 콘솔 애플리케이션은 Main(애플리케이션 진입점)의 모든 동기 코드가 실행된 후에 종료됩니다.

다음 예에서는 예외 처리를 포함하지 않는 기본 패턴을 보여 줍니다.

Task[] tasks = new Task[3]
{
    Task.Factory.StartNew(() => MethodA()),
    Task.Factory.StartNew(() => MethodB()),
    Task.Factory.StartNew(() => MethodC())
};

//Block until all tasks complete.
Task.WaitAll(tasks);

// Continue on this thread...
Dim tasks() =
{
    Task.Factory.StartNew(Sub() MethodA()),
    Task.Factory.StartNew(Sub() MethodB()),
    Task.Factory.StartNew(Sub() MethodC())
}

' Block until all tasks complete.
Task.WaitAll(tasks)

' Continue on this thread...

예외 처리를 보여 주는 예제는 예외 처리를 참조하세요.

일부 오버로드에서는 제한 시간을 지정할 수 있으며, 다른 오버로드에서는 프로그래밍 방식으로 또는 사용자 입력에 대한 응답으로 대기 자체를 취소할 수 있도록 추가 CancellationToken을 입력 매개 변수로 사용합니다.

작업을 대기할 때는 암시적으로 TaskCreationOptions.AttachedToParent 옵션의 사용으로 생성된 해당 작업의 모든 자식 작업을 대기합니다. Task.Wait는 작업이 이미 완료되었는지 여부를 즉시 반환합니다. 작업이 완료된 후 Task.Wait 메서드가 호출된 경우에도 Task.Wait 메서드는 작업에서 throw된 모든 예외를 throw합니다.

작업 작성

TaskTask<TResult> 클래스는 여러 작업을 구성하는 데 도움이 되는 여러 메서드를 제공합니다. 이러한 메서드는 일반적인 패턴을 구현하고 C#, Visual Basic 및 F#에서 제공하는 비동기 언어 기능을 더 효과적으로 활용합니다. 이 단원에서는 WhenAll, WhenAny, DelayFromResult 메서드에 대해 설명합니다.

Task.WhenAll

Task.WhenAll 메서드는 여러 Task 또는 Task<TResult> 개체가 완료될 때까지 비동기적으로 대기합니다. 균일하지 않은 작업 집합에 대해 대기할 수 있는 오버로드된 버전을 제공합니다. 예를 들어, 여러 TaskTask<TResult> 개체가 한 메서드 호출에서 완료되기를 기다릴 수 있습니다.

Task.WhenAny

Task.WhenAny 메서드는 여러 Task 또는 Task<TResult> 개체 중 하나가 완료될 때까지 비동기적으로 대기합니다. Task.WhenAll 메서드에서와 마찬가지로 이 메서드는 균일하지 않은 작업 집합에 대해 대기할 수 있는 오버로드된 버전을 제공합니다. WhenAny 메서드는 다음과 같은 시나리오에서 특히 유용합니다.

  • 중복 작업: 다양한 방법으로 수행할 수 있는 알고리즘이나 작업을 고려해 보세요. WhenAny 메서드를 사용하여 먼저 완료되는 작업을 선택한 다음 나머지 작업을 취소할 수 있습니다.

  • 인터리브 작업: 완료해야 하는 여러 작업을 시작하고 WhenAny 메서드를 사용하여 각 작업이 완료될 때 결과를 처리할 수 있습니다. 하나의 작업이 완료된 후 하나 이상의 작업을 시작할 수 있습니다.

  • 제한된 작업: WhenAny 메서드를 사용하면 동시 실행 작업 수를 제한하여 이전 시나리오를 확장할 수 있습니다.

  • 만료된 작업: WhenAny 메서드를 사용하여 하나 이상의 작업과 특정 시간 이후에 완료되는 작업(예: Delay 메서드에 의해 반환된 작업) 중에서 선택할 수 있습니다. Delay 메서드는 다음 단원에서 설명됩니다.

Task.Delay

Task.Delay 메서드는 지정된 시간 이후에 완료되는 Task 개체를 생성합니다. 이 메서드를 사용하여 데이터를 폴링하는 루프를 빌드하고, 시간 제한을 지정하고, 사용자 입력 처리를 지연시키는 등의 작업을 할 수 있습니다.

Task(T).FromResult

Task.FromResult 메서드를 사용하면 미리 계산된 결과를 갖는 Task<TResult> 개체를 만들 수 있습니다. 이 메서드는 Task<TResult> 개체가 반환되는 비동기 작업을 수행하고 해당 Task<TResult> 개체의 결과가 계산되어 있을 때 유용합니다. FromResult를 사용하여 캐시에 저장된 비동기 다운로드 작업 결과를 검색하는 예제는 방법: 미리 계산된 작업 만들기를 참조하세요.

작업의 예외 처리

작업이 하나 이상의 예외를 throw하면 해당 예외가 AggregateException 예외에 래핑됩니다. 해당 예외는 작업과 조인하는 스레드로 다시 전파됩니다. 일반적으로 작업이 완료되기를 기다리는 스레드이거나 Result 속성에 액세스하는 스레드입니다. 이 동작은 기본적으로 처리되지 않은 모든 예외가 프로세스를 종료해야 한다는 .NET Framework 정책을 적용합니다. 호출 코드는 다음 try/catch 블록을 사용하여 예외를 처리할 수 있습니다.

또한 조인하는 스레드에서는 작업이 가비지 수집되기 전에 Exception 속성에 액세스하여 예외를 처리할 수 있습니다. 이 속성에 액세스하면 처리되지 않은 예외로 인해 개체가 종료될 때 프로세스를 종료하는 예외 전파 동작이 트리거되는 것을 방지할 수 있습니다.

예외 및 작업에 대한 자세한 내용은 예외 처리를 참조하세요.

작업 취소

Task 클래스는 협조적 취소를 지원하며 .NET Framework 4에 소개된 System.Threading.CancellationTokenSourceSystem.Threading.CancellationToken 클래스와 완전히 통합됩니다. System.Threading.Tasks.Task 클래스의 많은 생성자는 CancellationToken 개체를 입력 매개 변수로 사용합니다. 대부분의 StartNewRun 오버로드는 CancellationToken 매개 변수를 포함합니다.

이 토큰을 만든 다음 나중에 CancellationTokenSource 클래스를 사용하여 취소 요청을 실행할 수 있습니다. 이 토큰을 Task에 인수로 전달하고, 취소 요청에 대한 응답 작업을 수행하는 사용자 대리자에서도 동일한 토큰을 참조합니다.

자세한 내용은 작업 취소방법: 작업 및 해당 자식 취소를 참조하세요.

TaskFactory 클래스

TaskFactory 클래스에서는 작업 및 연속 작업을 만들고 시작하기 위한 일반적인 패턴을 캡슐화하는 정적 메서드를 제공합니다.

기본 TaskFactoryTask 클래스 또는 Task<TResult> 클래스에서 정적 속성으로 액세스될 수 있습니다. TaskFactory를 직접 인스턴스화하고 CancellationToken, TaskCreationOptions 옵션, TaskContinuationOptions 옵션 또는 TaskScheduler를 포함한 다양한 옵션을 지정할 수도 있습니다. 작업 팩터리를 만들 때 어떤 옵션을 지정하든 작업 팩터리에서 만드는 모든 작업에 해당 옵션이 적용됩니다. 단, Task 열거형을 사용하여 TaskCreationOptions를 만드는 경우는 제외되며, 이때는 해당 작업의 옵션이 작업 팩터리의 옵션을 재정의합니다.

대리자가 없는 작업

경우에 따라 Task를 사용하여 사용자 대리자 대신 외부 구성 요소에 의해 수행되는 일부 비동기 작업을 캡슐화할 수도 있습니다. 이 작업이 비동기 프로그래밍 모델인 Begin/End 패턴을 기반으로 하는 경우에는 FromAsync 메서드를 사용할 수 있습니다. 그렇지 않은 경우에는 TaskCompletionSource<TResult> 개체를 사용하여 작업(operation)을 작업(task)으로 래핑하여 Task 프로그래밍 가능성의 일부 이점을 얻을 수 있습니다. 예를 들어, 예외 전파 및 지속을 지원합니다. 자세한 내용은 TaskCompletionSource<TResult>를 참조하세요.

사용자 지정 스케줄러

대부분의 애플리케이션 또는 라이브러리 개발자는 작업이 실행되는 프로세서, 작업과 다른 작업이 동기화되는 방식 또는 System.Threading.ThreadPool에서 작업이 예약되는 방식에는 크게 신경 쓰지 않고, 단지 작업이 호스트 컴퓨터에서 가능한 한 효율적으로 실행되기만을 바랍니다. 일정 세부 사항을 보다 세밀하게 제어해야 하는 경우 TPL을 사용하면 기본 작업 스케줄러에서 일부 설정을 구성할 수 있으며 사용자 지정 스케줄러를 제공할 수도 있습니다. 자세한 내용은 TaskScheduler를 참조하세요.

TPL에는 병렬 시나리오와 순차 시나리오에 유용한 새로운 공용 형식이 몇 가지 포함되어 있습니다. 여기에는 System.Collections.Concurrent 네임스페이스의 여러 스레드로부터 안전하고 빠르며 확장성 있는 컬렉션 클래스와 몇 가지 새로운 동기화 형식이 포함됩니다. 예를 들어, 특정 종류의 워크로드에 대해 이전 버전보다 더 효율적인 System.Threading.SemaphoreSystem.Threading.ManualResetEventSlim입니다. .NET Framework 4에 새로 추가된 System.Threading.BarrierSystem.Threading.SpinLock 등의 다른 형식은 이전 릴리스에서는 사용할 수 없었던 기능을 제공합니다. 자세한 내용은 병렬 프로그래밍의 데이터 구조를 참조하세요.

사용자 지정 작업 형식

System.Threading.Tasks.Task 또는 System.Threading.Tasks.Task<TResult>에서 상속하지 않는 것이 좋습니다. 대신 AsyncState 속성을 사용하여 추가 데이터 또는 상태를 Task 또는 Task<TResult> 개체에 연결하는 것이 좋습니다. 확장 메서드를 사용하여 TaskTask<TResult> 클래스의 기능을 확장할 수도 있습니다. 확장 메서드에 대한 자세한 내용은 확장 메서드확장 메서드를 참조하세요.

Task 또는 Task<TResult>에서 상속해야 하는 경우 Run 또는 System.Threading.Tasks.TaskFactory, System.Threading.Tasks.TaskFactory<TResult> 또는 System.Threading.Tasks.TaskCompletionSource<TResult> 클래스를 사용하여 사용자 지정 작업 종류의 인스턴스를 만들 수 없습니다. 이 클래스는 TaskTask<TResult> 개체만 만들므로 사용할 수 없습니다. 또한 Task, Task<TResult>, TaskFactoryTaskFactory<TResult>에서 제공하는 작업 연속 메커니즘을 사용하여 사용자 지정 작업 종류의 인스턴스를 만들 수 없습니다. 이러한 클래스도 TaskTask<TResult> 개체만 만들므로 사용할 수 없습니다.

제목 설명
연속 작업을 사용하여 작업 연결 연속 작업이 실행되는 방식을 설명합니다.
연결된 자식 작업 및 분리된 자식 작업 연결된 자식 작업과 분리된 자식 작업의 차이점을 설명합니다.
작업 취소 Task 개체에 기본 제공되는 취소 지원에 대해 설명합니다.
예외 처리 동시 스레드에 대한 예외를 처리하는 방법을 설명합니다.
방법: parallel_invoke를 사용하여 병렬 작업 실행 Invoke를 사용하는 방법을 설명합니다.
방법: 작업에서 값 반환 작업을 통해 값을 반환하는 방법을 설명합니다.
방법: 작업 및 해당 자식 취소 작업을 취소하는 방법을 설명합니다.
방법: 미리 계산된 작업 만들기 Task.FromResult 메서드를 사용하여 캐시에 저장된 비동기 다운로드 작업 결과를 검색하는 방법을 설명합니다.
방법: 병렬 작업을 사용하여 이진 트리 트래버스 작업을 사용하여 이진 트리를 따라 이동하는 방법을 설명합니다.
방법: 중첩된 작업 래핑 취소 Unwrap 확장 메서드를 사용하는 방법을 보여 줍니다.
데이터 병렬 처리 ForForEach를 사용하여 데이터에 대한 병렬 루프를 만드는 방법을 설명합니다.
병렬 프로그래밍 .NET Framework 병렬 프로그래밍의 최상위 노드입니다.

참고 항목