1. 程式人生 > 其它 >[Unity] 在Unity中整合puerts使用js多執行緒(偽)

[Unity] 在Unity中整合puerts使用js多執行緒(偽)

技術標籤:Unity學習筆記多執行緒typescriptunity

puerts為Unity提供了Typescript程式設計解決方案,開源地址:https://github.com/Tencent/puerts.

本文基於puerts_unity_v8版本,unity版本2019.4.8f1.

大致思路:

在js裡面將js object封裝成C#object傳遞給C#,然後C#分發到主執行緒傳遞給js拆包還原成jsobject(物件原型鏈暫時無法處理).

這樣只涉及到值型別傳遞(跨語言拷貝),不涉及到多執行緒js記憶體共享

/**
* main.ts檔案
*/

console.log("Worker");
let worker = new JsWorker();
worker.start("./test"); //從test.ts檔案開始執行
worker.on("data", data => {
    if (typeof data == "function") {
        console.log(data.toString());
        data();
    }
    else if (typeof data == "object") {
        console.log(JSON.stringify(data));
    }
    else
        console.log(data);
});
/**
* test.ts檔案
*/

import * as CS from "csharp";

console.log("Child Working");
globalWorker.post("data", "thread start");

let i = 10;
while (i-- > 0) {
    globalWorker.post("data", { msg: "this is test message", index: i });
    CS.System.Threading.Thread.Sleep(2000);
}

執行列印日誌如下:

關鍵程式碼:

/**
* jsWorker.ts
*/

import * as CS from "csharp";
import { $generic } from "puerts";
let List = $generic(CS.System.Collections.Generic.List$1, CS.System.Object);

const CLOSE = "close";

class JsWorker {
    public readonly isMain: boolean;
    private readonly worker: CS.JsWorker;
    private readonly callbacks: Map<string, ((data?: any) => void)[]>;
    constructor(worker?: CS.JsWorker) {
        if (!worker) {
            worker = CS.JsWorker.New(CS.JsManager.GetInstance().Loader);
            this.isMain = true;
        } else {
            this.isMain = false;
        }
        this.worker = worker;
        this.callbacks = new Map();
        this.working();
    }
    private working() {
        let getValue = (data: CS.JsWorker.Package) => {
            if (data !== undefined && data !== null && data !== void 0) {
                return this.unpackage(data);
            }
            return undefined;
        };
        let onmessage = (name: string, data: CS.JsWorker.Package): CS.JsWorker.Package => {
            let result = undefined;
            let arr = this.callbacks.get(name);
            if (arr) {
                let o = getValue(data);
                for (let cb of arr) {
                    result = cb(o);
                }
            }
            if (result !== undefined && result !== null && result !== void 0)
                return this.package(result);
            return undefined;
        };
        if (this.isMain)
            this.worker.messageByMain = (name, data) => {
                if (name === CLOSE) {
                    let o = getValue(data), closing = true;
                    let arr = this.callbacks.get(name);
                    if (arr)
                        arr.forEach(cb => {
                            if ((cb as (data?: any) => boolean)(o) === false)
                                closing = false;
                        });
                    if (closing)
                        this.dispose();
                    return this.package(closing);
                } else
                    return onmessage(name, data);
            };
        else
            this.worker.messageByChild = onmessage;
    }
    private package(data: any, refs?: WeakMap<object, number>, refCount?: number): CS.JsWorker.Package {
        refCount = refCount ?? 1;
        refs = refs ?? new WeakMap();

        let result = new CS.JsWorker.Package();
        let type = typeof (data);
        if ((type === "object" || type === "function") && refs.has(data)) {
            result.type = CS.JsWorker.Type.RefObject;
            result.value = refs.get(data);
        }
        else {
            switch (type) {
                case "object":
                    {
                        //新增引用
                        let id = refCount++;
                        result.id = id;
                        refs.set(data, id);
                        //建立C#物件
                        if (data instanceof CS.System.Object) {
                            result.type = CS.JsWorker.Type.Value;
                            result.value = data;
                        }
                        else if (data instanceof ArrayBuffer) {
                            result.type = CS.JsWorker.Type.ArrayBuffer;
                            result.value = CS.JsWorker.Package.ToBytes(data);
                        }
                        else if (Array.isArray(data)) {
                            let list = new List() as CS.System.Collections.Generic.List$1<any>;
                            for (let i = 0; i < data.length; i++) {
                                let item = this.package(data[i], refs, refCount);
                                item.info = i;
                                list.Add(item);
                            }
                            result.type = CS.JsWorker.Type.Array;
                            result.value = list;
                        }
                        else {
                            let list = new List() as CS.System.Collections.Generic.List$1<any>;
                            Object.keys(data).forEach(key => {
                                let item = this.package(data[key], refs, refCount);
                                item.info = key;
                                list.Add(item);
                            });
                            result.type = CS.JsWorker.Type.Object;
                            result.value = list;
                        }
                    }
                    break;
                case "function":
                    {
                        //新增引用
                        let id = refCount++;
                        result.id = id;
                        refs.set(data, id);
                        //建立C#物件
                        result.type = CS.JsWorker.Type.Function;
                        result.value = data.toString();
                    }
                    break;
                case "string":
                case "number":
                case "bigint":
                case "boolean":
                    result.type = CS.JsWorker.Type.Value;
                    result.value = data;
                    break;
                default:
                    result.type = CS.JsWorker.Type.Unknown;
                    break;
            }
        }
        return result;
    }
    private unpackage(data: CS.JsWorker.Package, refs?: Map<number, Object>): any {
        refs = refs ?? new Map();
        //console.log(data.id, data.type, data.value, data.info);

        let result = undefined, id = data.id, value = data.value;
        switch (data.type) {
            case CS.JsWorker.Type.Object:
                {
                    result = {};
                    if (id > 0) refs.set(id, result); //Add ref object
                    let arr = value as CS.System.Collections.Generic.List$1<CS.JsWorker.Package>;
                    for (let i = 0; i < arr.Count; i++) {
                        let item = arr.get_Item(i);
                        result[item.info] = this.unpackage(item, refs);
                    }
                }
                break;
            case CS.JsWorker.Type.Array:
                {
                    result = [];
                    if (id > 0) refs.set(id, result); //Add ref object
                    let arr = value as CS.System.Collections.Generic.List$1<CS.JsWorker.Package>;
                    for (let i = 0; i < arr.Count; i++) {
                        let item = arr.get_Item(i);
                        result[item.info] = this.unpackage(item, refs);
                    }
                }
                break;
            case CS.JsWorker.Type.ArrayBuffer:
                result = CS.JsWorker.Package.ToArrayBuffer(value);
                if (id > 0) refs.set(id, result); //Add ref object
                break;
            case CS.JsWorker.Type.Function:
                result = eval(value);
                if (id > 0) refs.set(id, result); //Add ref object
                break;
            case CS.JsWorker.Type.RefObject:
                if (refs.has(value))
                    result = refs.get(value);
                else
                    result = "Error: ref id " + value + " not found";
                break;
            case CS.JsWorker.Type.Unknown:
            default:
                result = value;
                if (id > 0) refs.set(id, result); //Add ref object
                break;
        }
        return result;
    }
    public start(scriptName: string) {
        if (globalWorker && globalWorker["worker"] == this.worker)
            throw new Error("不能在JsWorker裡面啟動例項");

        this.worker.Startup(scriptName);
    }
    public dispose() {
        if (globalWorker && globalWorker["worker"] == this.worker)
            this.post(CLOSE);
        else {
            this.worker.Dispose();
            this.callbacks.clear();
        }
    }
    public post(eventName: string, data?: any) {
        let o: CS.JsWorker.Package;
        if (data !== undefined && data !== null && data !== void 0) {
            o = this.package(data);
        }
        if (this.isMain)
            this.worker.ToChild(eventName, o);
        else
            this.worker.ToMain(eventName, o);
    }
    public postSync<T>(eventName: string, data?: any): T {
        let o: CS.JsWorker.Package;
        if (data !== undefined && data !== null && data !== void 0) {
            o = this.package(data);
        }
        let result = undefined;
        if (this.isMain)
            result = this.worker.ToChildSync(eventName, o);
        else
            result = this.worker.ToMainSync(eventName, o);
        if (result !== undefined && result !== null && result !== void 0) {
            result = this.unpackage(result);
        }
        return result;
    }
    public on(eventName: string, cb: (data?: any) => void) {
        if (eventName && cb) {
            let arr = this.callbacks.get(eventName);
            if (!arr) {
                arr = [];
                this.callbacks.set(eventName, arr);
            }
            arr.push(cb);
        }
    }
    public remove(eventName: string, cb: (data?: any) => void) {
        let arr = this.callbacks.get(eventName);
        if (arr) {
            let index = arr.indexOf(cb);
            if (index >= 0)
                this.callbacks.set(eventName, [...arr.slice(0, index), ...arr.slice(index + 1)]);
        }
    }
    public removeAll(eventName?: string) {
        if (eventName)
            this.callbacks.delete(eventName);
        else
            this.callbacks.clear();
    }
}
(function () {
    let _this = (this ?? globalThis);
    _this["JsWorker"] = JsWorker;
    _this["globalWorker"] = undefined;
})();

/**
 * 全域性介面
 */
declare global {
    class JsWorker {
        public readonly isMain: boolean;
        public constructor(worker?: CS.JsWorker, isMain?: boolean);
        /**
         * 開始執行指令碼(例項生命週期內僅呼叫一次) 
         */
        public start(scriptName: string): void;
        /**
         * 關閉JsWorker例項, 不可在內部關閉例項
         */
        public dispose(): void;
        /**
         * 傳送一條訊息(非同步)
         */
        public post(eventName: string, data?: any): void;
        /**
         * 同步傳送訊息並獲取返回值
         */
        public postSync<T>(eventName: string, data?: any): T;
        /**
         * 監聽事件資訊
         */
        public on(eventName: string, cb: (data?: any) => void): void;
        /**
         * 監聽並劫持JsWorker例項close訊息
         */
        public on(eventName: "close", cb: (state?: any) => boolean): void;
        /**
         * 移除一條監聽
         */
        public remove(eventName: string, cb: (data?: any) => void): void;
        /**
         * 移除所有監聽
         */
        public removeAll(eventName: string): void;
        /**
         * 移除所有監聽
         */
        public removeAll(): void;
    }
    /**
     * 只能在JsWorker執行緒內部訪問, 與主執行緒互動的物件
     */
    const globalWorker: JsWorker;
}

/**
* JsWorker.cs
*/

using System;
using System.Collections.Generic;
using System.Threading;
using Puerts;
using UnityEngine;

public class JsWorker : MonoBehaviour, IDisposable
{
    public static JsWorker New(ILoader loader, string startScript)
    {
        var obj = new GameObject("JsWorker");
        DontDestroyOnLoad(obj);
        var ins = obj.AddComponent<JsWorker>();
        ins.loader = new _SyncLoader(loader);
        if (!string.IsNullOrEmpty(startScript))
            ins.Working(startScript);

        return ins;
    }
    public static JsWorker New(ILoader loader)
    {
        return New(loader, null);
    }

    //jsWorker.ts檔案相對根目錄路徑
    private const string jsWorkerPath = "require('./common/jsWorker')";

    public JsEnv JsEnv { get; private set; }
    private _SyncLoader loader;
    //訊息介面
    public Func<string, Package, Package> messageByMain { get; set; }
    public Func<string, Package, Package> messageByChild { get; set; }
    //執行緒
    private Thread thread;
    private bool running = false;
    private bool finish = false;
    //訊息集合
    private Queue<_Event> mainEvents = new Queue<_Event>();
    private Queue<_Event> childEvents = new Queue<_Event>();
    //執行緒安全
    private ReaderWriterLock locker = new ReaderWriterLock();
    private const int lockTimeout = 1000;
    //同步訊息
    private string mainName = null;
    private Package mainData = null;
    private string childName = null;
    private Package childData = null;

    void Update()
    {
        ProcessMain();
        ProcessMainSync();
        if (this.loader != null)
            this.loader.Process();
    }
    void OnDestroy()
    {
        Dispose();
    }
    void Working(string startScript)
    {
        if (JsEnv != null || thread != null || running)
            throw new Exception("JsEnv is runing!");
        if (loader == null)
            throw new Exception("Loader is null!");
        if (finish)
            throw new Exception("Loader is finish!");

        running = true;
        thread = new Thread(new ThreadStart(() =>
        {
            JsEnv jsEnv = null;
            try
            {
                jsEnv = JsEnv = new JsEnv(loader);
                jsEnv.UsingAction<string, Package>();
                jsEnv.UsingFunc<string, Package, Package>();
                jsEnv.UsingFunc<string, Package, object>();
                jsEnv.Eval(jsWorkerPath);
                jsEnv.Eval<Action<JsWorker>>(@"(function (_w){ (this ?? globalThis)['globalWorker'] = new JsWorker(_w, false); })")(this);
                jsEnv.Eval(string.Format("require(\"{0}\")", startScript));
                while (running)
                {
                    if (JsEnv == null)
                        break;
                    Thread.Sleep(20);
                    jsEnv.Tick();
                    ProcessChild();
                    ProcessChildSync();
                }
            }
            catch (Exception e)
            {
                UnityEngine.Debug.LogWarning(e.Message);
            }
            finally
            {
                jsEnv?.Dispose();
            }
        }));
        thread.IsBackground = true;
        thread.Start();
    }
    public void Startup(string startScript)
    {
        Working(startScript);
    }
    public void Dispose()
    {
        messageByMain = null;
        messageByChild = null;
        running = false;
        finish = true;

        //此處僅通知執行緒中斷, 由執行緒自行結束(使用Abort阻塞將導致puerts crash)
        if (thread != null) thread.Interrupt();
        //if (JsEnv != null) JsEnv.Dispose();
        JsEnv = null;
        thread = null;
    }
    public void ToMain(string name, Package data)
    {
        lock (mainEvents)
        {
            mainEvents.Enqueue(new _Event()
            {
                name = name,
                data = data
            });
        }
    }
    public void ToChild(string name, Package data)
    {
        lock (childEvents)
        {
            childEvents.Enqueue(new _Event()
            {
                name = name,
                data = data
            });
        }
    }
    public object ToMainSync(string name, Package data)
    {
        if (name == null) return null;
        //寫入主執行緒
        locker.AcquireWriterLock(lockTimeout);
        this.mainName = name;
        this.mainData = data;
        locker.ReleaseWriterLock();
        //檢測主執行緒狀態
        try
        {
            while (true)
            {
                locker.AcquireReaderLock(lockTimeout);
                if (this.mainName == null)
                    break;
                locker.ReleaseReaderLock();
            }
            return this.mainData;
        }
        finally
        {
            locker.ReleaseReaderLock();
        }
    }
    public object ToChildSync(string name, Package data)
    {
        if (name == null) return null;
        //寫入子執行緒
        locker.AcquireWriterLock(lockTimeout);
        this.childName = name;
        this.childData = data;
        locker.ReleaseWriterLock();
        //檢測子執行緒狀態
        try
        {
            while (true)
            {
                locker.AcquireReaderLock(lockTimeout);
                if (this.childName == null)
                    break;
                locker.ReleaseReaderLock();
            }
            return this.childData;
        }
        finally
        {
            locker.ReleaseReaderLock();
        }
    }
    private void ProcessMain()
    {
        if (mainEvents.Count > 0)
        {
            _Event _e = null;
            lock (mainEvents)
            {
                if (mainEvents.Count > 0)
                    _e = mainEvents.Dequeue();
            }
            if (_e != null && messageByMain != null)
            {
                try
                {
                    messageByMain(_e.name, _e.data);
                }
                catch (Exception e)
                {
                    UnityEngine.Debug.LogError(e.Message);
                }
            }
        }
    }
    private void ProcessChild()
    {
        if (childEvents.Count > 0)
        {
            _Event _e = null;
            lock (childEvents)
            {
                if (childEvents.Count > 0)
                    _e = childEvents.Dequeue();
            }
            if (_e != null && messageByChild != null)
            {
                try
                {
                    messageByChild(_e.name, _e.data);
                }
                catch (Exception e)
                {
                    UnityEngine.Debug.LogError(e.Message);
                }
            }
        }
    }
    private void ProcessMainSync()
    {
        if (this.mainName != null)
        {
            try
            {
                locker.AcquireWriterLock(lockTimeout);
                Package data = null;
                if (this.mainName != null && this.messageByMain != null)
                    data = this.messageByMain(this.mainName, this.mainData);
                this.mainData = data;
            }
            catch (Exception e)
            {
                this.mainData = null;
                throw e;
            }
            finally
            {
                this.mainName = null;
                locker.ReleaseWriterLock();
            }
        }
    }
    private void ProcessChildSync()
    {
        if (this.childName != null)
        {
            try
            {
                locker.AcquireWriterLock(lockTimeout);
                Package data = null;
                if (this.childName != null && this.messageByMain != null)
                    data = this.messageByMain(this.childName, this.childData);
                this.childData = data;
            }
            catch (Exception e)
            {
                this.childData = null;
                throw e;
            }
            finally
            {
                this.childName = null;
                locker.ReleaseWriterLock();
            }
        }
    }
    private class _SyncLoader : ILoader
    {
        //指令碼快取
        private Dictionary<string, string> scripts;
        private Dictionary<string, string> debugPaths;
        private Dictionary<string, bool> state;
        //這個ILoader只能在主執行緒呼叫, 而本例項化物件在子執行緒中使用需要通過主執行緒同步
        private ILoader loader;
        //執行緒安全
        private ReaderWriterLock locker = new ReaderWriterLock();
        private const int lockTimeout = 1000;
        //載入內容
        private string existsPath = null;
        private bool exists = false;
        private string readPath = null;
        private string readContent = null;
        private string debugpath = null;

        public _SyncLoader(ILoader loader)
        {
            this.loader = loader;
            this.scripts = new Dictionary<string, string>();
            this.debugPaths = new Dictionary<string, string>();
            this.state = new Dictionary<string, bool>();
        }

        public bool FileExists(string filepath)
        {
            bool result = false;
            if (this.state.TryGetValue(filepath, out result))
                return result;
            //寫入主執行緒
            locker.AcquireWriterLock(lockTimeout);
            this.existsPath = filepath;
            this.exists = false;
            locker.ReleaseWriterLock();
            //檢測主執行緒狀態
            try
            {
                while (true)
                {
                    locker.AcquireReaderLock(lockTimeout);
                    if (this.existsPath == null)
                        break;
                    locker.ReleaseReaderLock();
                }
                this.state.Add(filepath, this.exists);
                return this.exists;
            }
            finally
            {
                locker.ReleaseReaderLock();
            }
        }
        public string ReadFile(string filepath, out string debugpath)
        {
            string script = null;
            if (this.scripts.TryGetValue(filepath, out script))
            {
                debugpath = this.debugPaths[filepath];
                return script;
            }
            //寫入主執行緒
            locker.AcquireWriterLock(lockTimeout);
            this.readPath = filepath;
            this.readContent = null;
            this.debugpath = null;
            locker.ReleaseWriterLock();
            //檢測主執行緒狀態
            try
            {
                while (true)
                {
                    locker.AcquireReaderLock(lockTimeout);
                    if (this.readPath == null)
                        break;
                    locker.ReleaseReaderLock();
                }
                this.scripts.Add(filepath, this.readContent);
                this.debugPaths.Add(filepath, this.debugpath);

                debugpath = this.debugpath;
                return this.readContent;
            }
            finally
            {
                locker.ReleaseReaderLock();
            }
        }

        //主執行緒驅動介面
        public void Process()
        {
            if (this.existsPath != null || this.readPath != null)
            {
                locker.AcquireWriterLock(lockTimeout);
                if (this.existsPath != null)
                {
                    this.exists = loader.FileExists(this.existsPath);
                    this.existsPath = null;
                }
                if (this.readPath != null)
                {
                    this.readContent = loader.ReadFile(this.readPath, out this.debugpath);
                    this.readPath = null;
                }
                locker.ReleaseWriterLock();
            }
        }
    }
    private class _Event
    {
        public string name;
        public Package data;
    }
    public class Package
    {
        /**此資料型別 */
        public Type type;
        /**資料內容 */
        public object value;
        /**資料資訊 */
        public object info;
        /**如果是物件, 代表物件Id */
        public int id = -1;

        public static byte[] ToBytes(Puerts.ArrayBuffer value)
        {
            if (value != null)
            {
                var source = value.Bytes;
                var result = new byte[source.Length];
                Array.Copy(source, 0, result, 0, source.Length);
                return result;
            }
            return null;
        }
        public static Puerts.ArrayBuffer ToArrayBuffer(byte[] value)
        {
            if (value != null)
                return new Puerts.ArrayBuffer(value);
            return null;
        }
    }
    public enum Type
    {
        Unknown,
        Value,
        Object,
        Array,
        Function,
        /**ArrayBuffer型別為指標傳遞,如果直接傳將會造成多執行緒共享記憶體crash */
        ArrayBuffer,
        RefObject
    }
}