协程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主线程。