CS5L7-2——Task类

本章代码关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
System.Threading.Tasks            //Task类的命名空间
Task //Task类,任务类,基于线程池的对Thread类的封装
Task() //Task的构造函数,传入多线程要执行的方法
task.Start() //运行该对象的多线程,不过该多线程基于线程池
Task.Run() //运行多线程的静态方法,不过该多线程基于线程池,直接传入多线程要执行的方法即可
Task.Factory.StartNew() //运行多线程的静态方法,不过该多线程基于线程池,直接传入多线程要执行的方法即可
Task<>() //Task<>的构造函数,传入多线程要执行的方法,该方法需要返回值,返回值类型由传入的泛型参数决定
Task.Run<>() //运行多线程的静态方法,直接传入多线程要执行的方法即可,该方法需要返回值,返回值类型由传入的泛型参数决定
Task.Factory.StartNew<>() //运行多线程的静态方法,直接传入多线程要执行的方法即可,该方法需要返回值,返回值类型由传入的泛型参数决定
task.Result //获取task对象对应的多线程执行完毕的返回值,注意,使用该属性时如果没有返回值会卡死线程,直到多线程的方法执行return
task.RunSynchronously() //同步执行Task对象里的多线程方法
task.Wait() //使执行该方法的线程等待任务执行完毕,再执行后面的内容
Task.WaitAny() //传入任务中任意一个任务结束就继续执行
Task.WaitAll() //任务列表中所有任务执行结束就继续执行
Task.WhenAll().ContinueWith() //其实是两个方法,第一个方法是等待传入的所有Task执行完毕后返回一个Task对象,第二个方法是让返回的Task对象继续执行新的任务
Task.Factory.ContinueWhenAll() //将上面两个方法融合起来的方法,即当第一个参数传入的Task数组内所有Task都执行完毕时,执行第二个参数的新任务
Task.WhenAny().ContinueWith() //其实是两个方法,第一个方法是等待传入的Task有一个执行完毕后返回一个Task对象,第二个方法是让返回的Task对象继续执行新的任务
Task.Factory.ContinueWhenAny() //将上面两个方法融合起来的方法,即当第一个参数传入的Task数组内中其中一个Task执行完毕时,执行第二个参数的新任务
CancellationTokenSource //取消标识源类,相比单纯的用bool来标识多了更多的额外处理方法
cancellationTokenSource.IsCancellationRequested //是否取消请求的属性,使用该属性来表示是否已经取消,可以用于替代原本控制线程循环的bool值,默认为false
cancellationTokenSource.Cancel() //使是否取消请求的属性为true的方法
cancellationTokenSource.CancelAfter() //延迟使是否取消请求的属性为true的方法,传入延迟的时间,单位是毫秒
cancellationTokenSource.Token.Register() //当使是否取消请求的属性为true的方法被执行后要执行的回调函数

Task

  1. Task​ 类是基于Thread的封装
  2. Task​ 类可以有返回值,Thread没有返回值
  3. Task​ 类可以执行后续操作,Thread没有这个功能
  4. Task​ 可以更加方便的取消任务,Thread相对更加单一
  5. Task具备ThreadPool线程池的优点,更节约性能

命名空间:System.Threading.Tasks
类名:Task
Task​ 顾名思义就是任务的意思,Task​ 是在线程池基础上进行的改进,它拥有线程池的优点,同时解决了使用线程池不易控制的弊端
它是基于线程池的优点对线程的封装,可以让我们更方便高效的进行多线程开发

简单理解:
Task​ 的本质是对线程 Thread​ 的封装,它的创建遵循线程池的优点,并且可以更方便的让我们控制线程
一个 Task​ 对象就是一个线程

创建无返回值Task的三种方式

  1. 通过 new​ 一个 Task​ 对象传入委托函数并启动

    传入的这个委托函数就是多线程要执行的方法
    值得一提的是,虽然 Task​ 的启动和 Thread​ 很相似,这里的 Task​运 行是基于线程池的运行,因此它的性能会比直接开启线程更好
    以及, Task打开的多线程仍然需要自行关闭,否则会与编辑器共生

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    private bool isRunning = true;

    void Start()
    {
    Task t1 = new Task(() =>
    {
    int i = 0;
    while (isRunning)
    {
    print("方法一:" + i);
    i++;
    Thread.Sleep(1000);
    }
    });
    t1.Start();
    }

    private void OnDestroy()
    {
    isRunning = false;
    }

  2. 通过 Task​ 中的 Run​ 静态方法传入委托函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Task t2 = Task.Run(() =>
    {
    int i = 0;
    while (isRunning)
    {
    print("方法二:" + i);
    i++;
    Thread.Sleep(1000);
    }
    });
  3. 通过 Task.Factory​ 中的 StartNew​ 静态方法传入委托函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Task t3 = Task.Factory.StartNew(() =>
    {
    int i = 0;
    while (isRunning)
    {
    print("方法三:" + i);
    i++;
    Thread.Sleep(1000);
    }
    });

创建有返回值的Task

创建有返回值的Task,只需要使用泛型方法即可

  1. 通过 new​ 一个 Task<>​ 对象传入委托函数并启动

    传入的这个委托函数就是多线程要执行的方法
    泛型填入返回值的类型,在委托里加上返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Task<int> t1 = new Task<int>(() =>
    {
    int i = 0;
    while (isRunning)
    {
    print("方法一:" + i);
    i++;
    Thread.Sleep(1000);
    }
    return 1;
    });
    t1.Start();
  2. 通过 Task​ 中的 Run<>​ 静态泛型方法传入委托函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Task<string> t2 = Task.Run<string>(() =>
    {
    int i = 0;
    while (isRunning)
    {
    print("方法二:" + i);
    i++;
    Thread.Sleep(1000);
    }
    return "任务完成";
    });
  3. 通过 Task.Factory​ 中的 StartNew<>​ 静态泛型方法传入委托函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Task<float> t3 = Task.Factory.StartNew<float>(() =>
    {
    int i = 0;
    while (isRunning)
    {
    print("方法三:" + i);
    i++;
    Thread.Sleep(1000);
    }
    return 4.5f;
    });

获取传入Task方法的返回值

注意

task.Result​ 获取结果时会阻塞线程!即如果 task​ 没有执行完成,会等待 task​ 执行完成获取到 Result​ ,然后再执行后边的代码,
也就是说执行到这句代码时,由于我们的Task​中是死循环,所以执行 task.Result​ 的线程就会被卡死

这意味着,如果我们编写逻辑不当,造成多线程内执行的方法始终没有 return,可能导致整个主程序卡死,例如,Unity无法继续运行等问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private void Start()
{
Task<string> t1 = Task.Run<string>(() =>
{
int i = 0;
while (isRunning)
{
print("方法一:" + i);
i++;
Thread.Sleep(1000);
}
return "方法一任务完成";
});

Task t2 = Task.Run(() =>
{
for (int i = 0; i < 20; i++)
{
print("t2: " + i);
print(t1.Result); //在这里我们如果没有按下空格结束t1的死循环,t2将会被一直卡死在这里无法继续向下执行
}
print("方法二任务完成");
});
print("主线程执行");
}

private void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
isRunning = false;
}
}

同步执行Task

刚才我们举的例子都是通过多线程异步执行的
(举个例子,下面这段代码里,print("主线程执行");​ 会先于线程开启)

1
2
3
4
5
6
7
8
9
10
private void Start()
{
Task t = new Task(() =>
{
Thread.Sleep(1000);
print("线程开启");
});
t.Start();
print("主线程执行"); //线程的开启并不会影响主线程的执行,这就是异步执行
}

如果你希望 Task​ 能够同步执行,只需要调用 Task​ 对象中的 RunSynchronously​ 方法

注意:需要使用 new Task​ 对象的方式,因为 Run​ 和 StartNew​ 在创建时就会启动

1
2
3
4
5
6
7
8
9
10
private void Start()
{
Task t = new Task(() =>
{
Thread.Sleep(1000);
print("线程开启");
});
t.RunSynchronously();
print("主线程执行");
}

Task中线程阻塞的方式(任务阻塞)

  1. Wait()​ 方法:使执行该方法的线程等待任务执行完毕,再执行后面的内容

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    Task t1 = Task.Run(() =>
    {
    for (int i = 0; i < 5; i++)
    {
    print("t1: " + i);
    }
    });

    Task t2 = Task.Run(() =>
    {
    for (int i = 0; i < 20; i++)
    {
    print("t2: " + i);
    }
    });
    t1.Wait();
    print("主线程执行");
  2. WaitAny()​ 静态方法:传入任务中任意一个任务结束就继续执行

    1
    2
    Task.WaitAny(t1, t2);
    print("主线程执行");
  3. WaitAll()​ 静态方法:任务列表中所有任务执行结束就继续执行

    1
    2
    Task.WaitAll(t1, t2);
    print("主线程执行");

Task完成后继续其它Task(任务延续)

先声明并执行两个线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Task t1 = Task.Run(() =>
{
for (int i = 0; i < 5; i++)
{
print("t1:" + i);
}
});

Task t2 = Task.Run(() =>
{
for (int i = 0; i < 20; i++)
{
print("t2:" + i);
}
});
  1. WhenAll()​ 静态方法 + ContinueWith()​ 方法:传入的任务都完毕后再执行某任务,需要一个以task​参数的方法,传入新开始的task

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Task.WhenAll(t1, t2).ContinueWith((t) =>
    {
    print("一个新的任务开始了");
    int i = 0;
    while (isRunning)
    {
    print(i);
    i++;
    Thread.Sleep(1000);
    }
    print("任务结束");
    });
  2. Task.Factory.ContinueWhenAll()​静态方法:相当于上面两个方法的融合

    • 参数一:Task数组,要等待完成的所有Task任务
    • 参数二:参数一的所有任务完成后要执行的方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Task.Factory.ContinueWhenAll(new Task[] { t1, t2 }, (t) =>
    {
    print("一个新的任务开始了");
    int i = 0;
    while (isRunning)
    {
    print(i);
    i++;
    Thread.Sleep(1000);
    }
    print("任务结束");
    });
  3. WhenAny()​ 静态方法 + ContinueWith()​ 方法:传入任务只要有一个执行完毕后再执行某任务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Task.WhenAny(t1, t2).ContinueWith((t) =>
    {
    print("一个新的任务开始了");
    int i = 0;
    while (isRunning)
    {
    print(i);
    i++;
    Thread.Sleep(1000);
    }
    print("任务结束");
    });
  4. Task.Factory.ContinueWhenAny()​ 静态方法:相当于上面两个方法的融合
    参数一:Task​ 数组,要等待完成的所有 Task​ 任务
    参数二:参数一的只要有一个任务完成后要执行的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Task.Factory.ContinueWhenAny(new Task[] { t1, t2 }, (t) =>
    {
    print("一个新的任务开始了");
    int i = 0;
    while (isRunning)
    {
    print(i);
    i++;
    Thread.Sleep(1000);
    }
    print("任务结束");
    });

取消Task执行

  1. 通过加入 bool​ 标识 控制线程内死循环的结束

    上面已经有诸多的例子使用了这种方法,这里是其中一个例子:Task()​

  2. 取消标识源类

    通过 CancellationTokenSource​ 取消标识源类,来控制 CancellationTokenSource​ 对象可以达到延迟取消、取消回调等功能

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    CancellationTokenSource c;

    private void Start()
    {
    c = new CancellationTokenSource();
    c.CancelAfter(5000); //延迟5000毫秒也就是5秒后使IsCancellationRequested属性为true
    c.Token.Register(() => //当IsCancellationRequested属性为true了要执行的回调函数
    {
    print("任务取消了");
    });

    Task.Run(() =>
    {
    int i = 0;
    while (!c.IsCancellationRequested) //这是该取消标识源对象的是否取消请求的bool值,可以用这个来控制线程循环的结束,默认为false
    {
    print("计时:" + i);
    i++;
    Thread.Sleep(1000);
    }
    });
    }

    private void Update()
    {
    if (Input.GetKeyDown(KeyCode.Space))
    {
    c.Cancel(); //这会使IsCancellationRequested属性为true
    }
    }
    • 是否取消请求属性

      这是该取消标识源对象的是否取消请求的 bool​ 值,可以用这个来控制线程循环的结束,默认为 false

      1
      c.IsCancellationRequested
    • 延迟取消方法

      传入延迟多少毫秒取消

      1
      c.CancelAfter(5000);
    • 取消回调

      取消后的回调方法

      1
      2
      3
      4
      c.Token.Register(() =>
      {
      print("任务取消了");
      });