I'm new to working with multi-thread compatible software, so this may well be a really simple question.
If I have created a class in which I want to expose a static instance (of itself), is there a way to synchronize it across threads so that the current internal state of it is the same everywhere, and if so what would be the way to accomplish this?
The class (in my particular case) is similar to the built in Random Class, and I can't ensure that all calls will originate from the same thread, but no matter which thread needs to generate a value, I need to ensure that the state across threads remains the same.
As requested I will post the code relating to the state of the class as well as the static access members
public class ChaosEngine {
// The internal state of the class that needs to be synchronized
private int _Seed;
private int _INext;
private int _INextP;
private int[] _SeedArray;
public int Seed{ get=>_Seed; set=>Reseed(value); }
// The shared instance (as it is currently written)
[ThreadStatic] private static ChaosEngine _Shared;
public static ChaosEngine Shared {
get {
if(_Shared == null)
_Shared = new ChaosEngine();
return _Shared;
}
}
}
If the full code is required it can be found here.
CodePudding user response:
Based on the update in your comments, it seems there is one point of interaction with your component, which means synchronizing thread access is straightforward. You can use a simple lock, e.g.:
public static class RandomNumberComponent
{
private static readonly object locker = new object();
private static Random random = new Random();
public static double NextDouble()
{
lock (locker)
return random.NextDouble();
}
}
You can test it like this:
var tasks = Enumerable.Range(0, 100)
.Select(i => Task.Run(() =>
Console.WriteLine(RandomNumberComponent.NextDouble())))
.ToArray();
Task.WaitAll(tasks);
Note that if you have multiple points of interaction, protecting access to internal state can become more complex and introduces the possibility of race conditions and deadlocks.
CodePudding user response:
As far as I can tell from your linked code, the critical operations seem to be the instantiation of the Shared
instance and the Reseed(int? seed)
method.
By changing your Shared
instance to no longer use [ThreadStatic]
and instead initializing inline, you will have thread-safe instantiation, and it will remain thread-safe. This also allows you to remove m_Shared
.
public static ChaosEngine Shared = new ChaosEngine();
That said, your call to Seed
is not thread-safe since it can update the m_Seed value. One option is to lock inside of Reseed(int? seed)
which means any reads to Seed
may not be thread-safe, but any writes to m_Seed
would be synchronized.
private static readonly object s_seedLocker = new object();
public int? Seed { get => lock(s_seedLocker) { m_Seed }; set => Reseed(value); }
public void Reseed(int? seed)
{
lock (s_seedLocker)
{
m_Seed = seed ?? Guid.NewGuid().GetHashCode();
...
}
}
You'll have to determine if this is sufficient for your needs. If not, you'd need to synchronize all access to Seed
.
private static readonly object s_seedLocker = new object();
public int? Seed
{
get
{
lock (s_seedLocker)
{
return m_Seed;
}
}
set
{
lock (s_seedLocker)
{
Reseed(value);
}
}
}
Note that in the following scenario:
var seed1 = ChaosEngine.Shared.Seed;
var engine = new ChaosEngine();
var seed2 = engine.Seed;
var seed3 = ChaosEngine.Shared.Seed;
seed1
and seed3
will be identical, but seed2
will be unique. It is unclear if this is behavior that you would expect.
You do have the ability to synchronize each instantiation of ChaosEngine
to the same seed with the following:
Task.Run(() => new ChaosEngine(ChaosEngine.Shared.Seed.Value));
Task.Run(() => new ChaosEngine(ChaosEngine.Shared.Seed.Value));
Task.Run(() => new ChaosEngine(ChaosEngine.Shared.Seed.Value));
Task.Run(() => new ChaosEngine(ChaosEngine.Shared.Seed.Value));
Task.Run(() => new ChaosEngine(ChaosEngine.Shared.Seed.Value));
Task.Run(() => new ChaosEngine(ChaosEngine.Shared.Seed.Value));
Task.Run(() => new ChaosEngine(ChaosEngine.Shared.Seed.Value));
Task.Run(() => new ChaosEngine(ChaosEngine.Shared.Seed.Value));
Task.Run(() => new ChaosEngine(ChaosEngine.Shared.Seed.Value));
Task.Run(() => new ChaosEngine(ChaosEngine.Shared.Seed.Value));
which will use the same common seed value for all instances.