协程Coroutine主要用于控制代码在特定的时机执行,给人感觉和线程Thread一样是在异步执行,但其实协程和Update函数一样是在Unity的MainThread中执行的,Unity在每帧都会处理对象上的协程。值得注意的是,通过StartCoroutine开启的协程是立即执行的,只有遇到yield语句才会挂起,在下一帧满足条件后会继续执行后面的代码。协程在遇到yield语句后,一般是在LateUpdate函数之后执行的。调用对象的SetActive(false)会让对象上的所有协程停止执行,再调用SetActive(true)也不会恢复执行;调用MonoBehaviour.enabled = false,对协程没有影响。Coroutine主要利用IEnumerator迭代器实现,IEnumerator接口有两个方法 Current 和 MoveNext(),迭代功能主要靠 MoveNext() 实现,每次通过MoveNext()迭代的Current对应了其中的一个yield语句,使用了yield语法糖可以不用写Current和MoveNext()部分的代码实现。
StartCoroutine 用于启动一个协程。可能的实现方式:新建一个任务,当前 Ienumerator 函数作为一个参数,然后添加到任务列表中;开启另一个 Ienumerator,用一个 while 循环等待该任务的完成;然后在在 Update 中取出队列中的一个任务执行 MoveNext,完成之后继续下一个任务。
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
|
readonly List<CoroutineInfo> newWorkers = new List<CoroutineInfo>();
readonly LinkedList<CoroutineInfo> workers = new LinkedList<CoroutineInfo>();
class CoroutineInfo
{
public IEnumerator CoRoutine;
public bool IsFinished;
}
IEnumerator StartCustomCoroutine(IEnumerator process)
{
var data = new CoroutineInfo()
{
CoRoutine = process,
};
newWorkers.Add(data);
return WaitUntilFinished(data);
}
IEnumerator WaitUntilFinished(CoroutineInfo workerData)
{
while (!workerData.IsFinished)
{
yield return null;
}
}
void AddNewWorkers()
{
foreach (var worker in newWorkers)
{
workers.AddLast(worker);
}
newWorkers.Clear();
}
void Update()
{
AddNewWorkers();
if (workers.Count == 0)
{
return;
}
var currentNode = workers.First;
while (currentNode != null)
{
var next = currentNode.Next;
var worker = currentNode.Value;
try
{
worker.IsFinished = !worker.CoRoutine.MoveNext();
}
catch (Exception e)
{
worker.IsFinished = true;
Debug.LogException(e);
}
if (worker.IsFinished)
{
workers.Remove(currentNode);
}
currentNode = next;
}
AddNewWorkers();
}
|
协程不是异步执行的,和Update在一个主线程上执行,所以相当于并发,即协程的任务和其它任务是间隔执行的。而线程之间是可以在同一时刻执行的,这称做并行,多线程在涉及对相同数据读写时需要考虑安全问题。
如果执行一个比较耗时的任务,为了不阻塞主线程,可以通过协程把任务细分,然后在切分点执行yield return null语句来实现,但最好的办法还是新开一个线程,等执行完了再继续执行。下面以读一个大文件为例,看看在Unity中如何使用线程。由于在新建的线程中,很多Unity的API无法使用,所以线程的使用是受限的。
TestScript.cs
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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.IO;
using System;
using System.Threading;
public class TestScript : MonoBehaviour {
[Serializable]
class JsonData {
public List<JsonObject> objectList = new List<JsonObject> ();
}
[Serializable]
class JsonObject {
public string bookId;
public string bookName;
public float price;
}
// http://JacksonDunstan.com/articles/3746
void Start () {
string filePath = Application.dataPath + "/file.txt";
StartCoroutine (ReadFileByTask (filePath));
//StartCoroutine (ReadFileByThread (filePath));
}
IEnumerator ReadFileByTask (string filePath)
{
yield return new WaitForThreadedTask (() => {
ReadFile(filePath);
});
}
IEnumerator ReadFileByThread (string filePath) {
bool done = false;
new Thread (() => {
ReadFile(filePath);
done = true;
}).Start ();
while (!done) {
yield return null;
}
}
void ReadFile (string filePath)
{
if (File.Exists (filePath)) {
string json = File.ReadAllText (filePath);
JsonData dataList = JsonUtility.FromJson<JsonData> (json);
Debug.LogWarning ("read");
}
}
void CreateFile (string filePath)
{
if (File.Exists (filePath)) {
File.Delete (filePath);
}
FileStream file = new FileStream (filePath, FileMode.OpenOrCreate);
StreamWriter sw = new StreamWriter (file);
JsonData test = new JsonData ();
for (int i = 0; i < 1000000; i++) {
JsonObject obj = new JsonObject () { bookId = "book_id_10_" + i, bookName = "book_name_1_" + i, price = 128.5f*i };
test.objectList.Add (obj);
}
string json = JsonUtility.ToJson (test);
sw.Write (json);
sw.Close ();
file.Close ();
Debug.LogWarning ("created");
}
}
|
WaitForThreadedTask.cs
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
|
using System;
using System.Threading;
/// <summary>
/// executes a task on a new thread and keeps waiting until it's done, this may not be safe
/// </summary>
public class WaitForThreadedTask : UnityEngine.CustomYieldInstruction
{
/// <summary>
/// if the thread is running
/// </summary>
private bool isRunning;
/// <summary>
/// If the coroutine should keep waiting
/// </summary>
public override bool keepWaiting { get { return isRunning; } }
public WaitForThreadedTask (Action task, ThreadPriority priority = ThreadPriority.Normal) {
isRunning = true;
new Thread (() => {
task();
isRunning = false;
}).Start (priority);
}
}
|
可以先用 CreateFile 来创建一个大文件,然后再读取,可以明显发现使用线程后不会阻塞Unity主线程。