在单例中使用多线程时,需要注意以下几点:
-
线程安全:在多线程环境下,单例对象可能被多个线程同时访问,因此需要确保单例的线程安全,避免出现数据竞争等问题。
-
对象创建:如果在单例对象的构造函数中启动了新的线程,那么可能会在单例对象还没有完全创建完成时就开始执行线程。因此,在创建单例对象时需要考虑到线程的启动时机,可以使用懒汉式的延迟加载方式,在需要使用单例对象时再进行初始化。
-
生命周期管理:如果在单例对象中启动了线程,那么需要考虑线程的生命周期管理,避免线程一直运行导致资源泄漏等问题。可以在单例对象的析构函数中停止线程,或者提供额外的接口供外部调用停止线程。
以下是一个在单例中使用多线程的示例代码:
public class Singleton
{
private static Singleton instance = null;
private static readonly object padlock = new object();
private Thread workerThread;
private bool stopWorkerThread = false;
public static Singleton Instance
{
get
{
lock (padlock)
{
if (instance == null)
{
instance = new Singleton();
}
return instance;
}
}
}
private Singleton()
{
workerThread = new Thread(WorkerThreadMethod);
workerThread.Start();
}
private void WorkerThreadMethod()
{
while (!stopWorkerThread)
{
// Do some work...
}
}
public void StopWorkerThread()
{
stopWorkerThread = true;
}
~Singleton()
{
StopWorkerThread();
}
}
在这个例子中,Singleton 是一个单例类,它在构造函数中启动了一个工作线程,并且提供了一个 StopWorkerThread 接口用于停止工作线程。在 Singleton 的析构函数中会调用 StopWorkerThread 接口来停止工作线程,确保线程的生命周期管理。在使用 Singleton 时,可以通过 Singleton.Instance 来获取单例对象,并且可以调用 StopWorkerThread 接口来停止工作线程。
非阻塞型
将GetInstance()的返回类型从Task改为UniTask,这是Unity针对异步编程所提供的更高效的API。
将_instance声明为UniTaskCompletionSource类型,并在Initialize()方法完成后使用TrySetResult()方法将结果赋值给_instance。
在Instance属性中使用AsyncLazy类型来实现延迟初始化,并确保多个线程安全地访问单例。
下面是修改后的代码示例:
using Cysharp.Threading.Tasks;
public abstract class SingletonTask<T> where T : SingletonTask<T>, new()
{
private static readonly AsyncLazy<T> _instance = new AsyncLazy<T>(async () =>
{
var instance = new T();
await instance.InitializeAsync();
return instance;
});
public static UniTask<T> InstanceAsync => _instance.Value;
protected virtual UniTask InitializeAsync()
{
return UniTask.CompletedTask;
}
}
这里我们使用了AsyncLazy来延迟初始化单例,并将Initialize()方法改为InitializeAsync(),返回UniTask类型。注意到InitializeAsync()方法是虚方法,方便子类进行实现。
使用时,可以通过调用InstanceAsync属性来获取单例,例如:
public class GameManager : SingletonTask<GameManager>
{
private int _score = 0;
public void AddScore(int score)
{
_score += score;
}
protected override UniTask InitializeAsync()
{
Debug.Log("Game manager initialized.");
return UniTask.CompletedTask;
}
}
// 在其他地方获取GameManager单例
GameManager.InstanceAsync.ContinueWith(gameManager => {
gameManager.AddScore(100);
});
这里通过ContinueWith方法来在获取单例后执行添加分数的操作,而不需要等待单例初始化完成。
阻塞型
如果需要等待单例初始化完成,可以在获取单例的时候返回一个 Task 对象,并在单例初始化完成后 Task 对象得到通知。具体的实现可以参考下面的代码:
public abstract class SingletonTask<T> where T : SingletonTask<T>, new()
{
private static readonly object padlock = new object();
private static T _instance;
private static TaskCompletionSource<T> _tcs;
public static async Task<T> GetInstanceAsync()
{
if (_instance != null)
{
return _instance;
}
if (_tcs == null)
{
_tcs = new TaskCompletionSource<T>();
}
await _tcs.Task;
return _instance;
}
protected SingletonTask()
{
lock (padlock)
{
if (_instance != null)
{
throw new InvalidOperationException("Cannot create multiple instances of singleton.");
}
_instance = this as T;
_tcs?.TrySetResult(_instance);
}
}
public abstract Task Initialize();
}
在上面的代码中,GetInstanceAsync 方法返回一个 Task 对象,如果单例已经初始化完成,则直接返回单例;否则创建一个 TaskCompletionSource 对象 _tcs 并返回其 Task 属性。当单例初始化完成时,调用 _tcs.TrySetResult(_instance) 方法,通知等待该 Task 的代码,单例已经初始化完成。具体使用方式如下:
public class MySingleton : SingletonTask<MySingleton>
{
private MySingleton()
{
}
public override async Task Initialize()
{
// 初始化代码
await Task.Delay(1000);
}
}
// 获取单例,并等待初始化完成
var instance = await MySingleton.GetInstanceAsync();
上述代码会等待 MySingleton 的初始化完成,然后返回单例对象。