【Unity】【CSharp】多线程

Task和Thread的区别

  • 基于不同的 .NET 框架:Thread 是基于 Windows 操作系统提供的 API 实现,而 Task 则是基于 .NET框架提供的 TPL(Task Parallel Library)实现。

  • 默认执行线程池:Thread 默认使用前台线程,而 Task 默认使用后台线程。这意味着,Thread 会阻塞主线程,而 Task不会。

  • 异步执行:Task 支持异步执行,而 Thread 不支持。这意味着,在使用 Task 时,可以通过 await 和 async关键字轻松实现异步编程,而 Thread 则需要手动管理线程的启动和等待。

  • 异常处理:Task 提供了更好的异常处理机制,可以将异常传递给调用方,而 Thread 则需要在每个线程中处理异常。

  • 任务调度器:Task 提供了任务调度器(TaskScheduler),可以控制任务的并发性和调度方式,而 Thread 则没有这个功能。

  • 返回值:Task 可以有返回值,而 Thread 没有。这是因为 Task 是基于 TPL 实现的,可以利用 .NET框架提供的并发编程模型来实现任务之间的依赖和调度。

Task

TaskTask<T>的创建

Task的创建

使用Task的构造函数

1
2
3
Task task = new Task(() => {
// 异步操作的代码
});

使用Task.Run的构造函数

1
2
3
Task task = Task.Run(() => {
// 异步操作的代码
});
Task<T>的创建

Task<T>与会返回一个类型为T的结果

使用Task的构造函数

1
2
3
4
Task<int> task = new Task<int>(() =>{
// 异步操作的代码,返回int类型的结果
return 42;
});

使用Task.Run的构造函数

1
2
3
4
Task<int> task = Task.Run(() => {
// 异步操作的代码,返回int类型的结果
return 7;
});

启动和等待TaskTask<T>

启动

使用start方法启动

1
task.Start();
使用Wait()阻塞等待

直接控制Task,实现异步等待任务的完成
会阻塞主线程,类似于thread1.Join()

1
2
3
4
5
task.Wait();	// 阻塞当前线程,等待任务完成
int result = task.Result; // 阻塞当前线程,等待任务完成,并获取结果

Task.WaitAll(new Task[]{ task1, task2 }); // 等待所有的task都执行完成再解除阻塞
Task.WaitAny(new Task[]{ task1, task2 }); // 只要有一个task执行完毕就解除阻塞
使用WhenAllWhenAny控制线程

不会阻塞主线程

1
2
Task.WhenAll(task1, task2).ContinueWith((t) => { Console.Writeline("执行异步代码"); });	// 当task1和task2执行完毕后,再执行后续代码
Task.WhenAny(task1, task2).ContinueWith((t) => { Console.Writeline("执行异步代码"); }); // 只要有一个执行完毕,就执行后续代码
使用await等待TaskTask<T>

在异步代码中使用await等待其他的任务完成(为.net5.0推出的方法)
不会阻塞主线程

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
using System;
using System.Threading.Tasks;

namespace AsyncTest
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("主线程开始");

t().Start();

Console.WriteLine("主线程结束");

Console.ReadKey();
}

private static Task t()
{
return new Task(async () =>
{
Console.WriteLine("开始执行t");
await Task.Delay(2000); // 等待两秒,模拟一个异步操作
Console.WriteLine("结束执行t");
});
}
}
}

运行结果

1
2
3
4
主线程开始
主线程结束
开始执行t
结束执行t
三个等待的区别
  • Wait()针对线程操作,会阻塞主线程
  • WhenAll针对线程操作,不会阻塞主线程
  • await在线程中针对其他线程操作,不会阻塞主线程

Thread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System.Threading;

void start()
{
Thread t1 = new Thread(A);
Thread t2 = new Thread(B);
t1.start();
t2.start();
}

void A()
{
Debug.Log("我是A函数1");
Debug.Log("我是A函数2");
}

void B()
{
Debug.Log("我是B函数1");
Debug.Log("我是B函数2");
}

此时的输出结果是不可控的,可能先执行A,也可能先执行B,这个是操作系统根据CPU自动计算出来的。
而且A和B是会嵌套交叉执行的

如何让程序先执行A,执行完A之后在执行B;或者先执行完B再执行A:使用lock关键字

lock关键字

可以通过lock关键字来控制A和B的执行顺序。使用同一个lock参数的代码,程序会等待前面的代码执行完之后再执行后面的

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
34
35
36
using System.Threading;

void start()
{
static Object o = new object();
Thread t1 = new Thread(A);
Thread t2 = new Thread(B);
t1.start();
t2.start();
Thread t3 = new Thread(C);
t3.start();
}

static void A()
{
lock(o)
{
Debug.Log("我是A函数1");
Debug.Log("我是A函数2");
}
}

static void B()
{
lock(o)
{
// Thread.Sleep(1000); // 暂停1秒
Debug.Log("我是B函数1");
Debug.Log("我是B函数2");
}
}

static void C()
{
DEbug.Log("我是随机函数");
}

此时可能会先执行A,执行完A后再执行B;也有可能先执行B,执行完B之后再执行A。C函数没有被锁住,所以他能出现在任意位置。

补充:这里的o是Object类(基类)。所以,lock的参数可以是任意的类

拓展

在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
34
35
36
37
38
39
40
41
42
using UnityEngine;
using System.Collections.Generic;
using System;

public class MainThreadDispatcher : MonoBehaviour
{
private static MainThreadDispatcher instance;

private Queue<Action> actionQueue = new Queue<Action>(); // 初始化一个队列:先进先出的一个数据结构

private void Awake()
{
if (instance == null)
{
instance = this;
}
else
{
Destroy(gameObject);
}
}

private void Update()
{
lock (actionQueue)
{
while (actionQueue.Count > 0)
{
Action action = actionQueue.Dequeue(); // 取出队列中的一个函数,并执行
action.Invoke();
}
}
}

public static void RunOnMainThread(Action action)
{
lock (instance.actionQueue) // 如果有其他的代码(包括自己)使用了lock(instance.actionQueue),则会等待前面的执行完再执行自己
{
instance.actionQueue.Enqueue(action); // 将传进来的action函数插入到队列中
}
}
}

在其他函数中可以通过调用RunOnMainThread()函数将方法转移到主线程上执行
常用与数据请求上,接收到的数据一般都是在子线程上。但是在unity的子线程中无法访问transform属性等,故需要转移到主线程上执行

1
2
3
4
5
RunOnMainThread(() =>
{
// 转移到主线程上执行代码
textValue.text = "你好";
});