[Unity] 在Unity中整合puerts使用js多執行緒(偽)
阿新 • • 發佈:2021-01-21
技術標籤: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
}
}