0

I have been developing a multithreading algorithm and I have some doubts about sharing a class member between threads in C#.

Let's assume that we have two classes Algorithm and Processor. Processor has a main method DoWork and additional AvaliableResources method which is called occasionally to change the number of available resources for processing. The methods Run and UpdateResources in Algorithm object are invoked by two different threads, possibly working on different Cores.

Is it possible that _processor variable will be stored in CPU's cache and will never be uploaded to memory, and AvaliableResources would be never called because _processor is null for the second thread?

class Processor
{

    public void DoWork() { ...  }
    public void AvaliableResources(int x) { ... }
}

class Algorithm
{
    private Processor _processor;

    public void Run()
    {
        _processor = new Processor();
        _processor.DoWork();
        _processor = null;
    }

    public void UpdateResources(int x)
    {
        _processor?.AvaliableResources(x);
    }
}

If there is a synchronization problem would the following code be a solution to for it?

Alternative 1

class Processor
{
    public void DoWork() { ... }
    public void UpdateResources(int x) { ... }
}

class Algorithm
{
    private volatile Processor _processor; // added volatile keyword

    public void Run()
    {
        _processor = new Processor();
        _processor.DoWork();
        _processor = null;
    }

    public void UpdateResources(int x)
    {
        _processor?.UpdateResources(x);
    }
}

Alternative 2

class Processor
{
    public void DoWork() { ... }
    public void UpdateResources(int x) { ... }
}

class Algorithm
{
    private Processor _processor;

    public void Run()
    {
        _processor = new Processor();
        Thread.MemoryBarrier(); // Added memory barier
        _processor.DoWork();
        _processor = null;
    }

    public void UpdateResources(int x)
    {
        Thread.MemoryBarrier(); // Added memory barier
        _processor?.UpdateResources(x);
    }
}

Edit: As you have suggested in comments, please see better explained code:

    class Processor
    {
        private int resources = Environment.ProcessorCount;

        public void DoWork()
        {
            /*do some long running job using avaliable resources*/
        }

        public void UpdateResources(int x)
        {
            resources = x;
        }
    }

    class Algorithm
    {
        private volatile Processor _processor;

        public void Run()
        {
            _processor = new Processor();
            _processor.DoWork();
            _processor = null;
        }

        public void UpdateResources(int x)
        {
            _processor?.UpdateResources(x);
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var algorithm = new Algorithm();
            var result = Task.Run(() => algorithm.Run());
            // The resources were required for other staff and are reduced
            algorithm.UpdateResources(1);
            // The resources are reassigned for the long running algorithm 
            algorithm.UpdateResources(10);
            // wait until the algorithm finishes
            result.Wait();
            // this update should have no effect as the Run method has finished and _processor is null
            algorithm.UpdateResources(10);
        }
    }
  • What's happening when you run the code? – Fabulous Jun 22 '17 at 22:24
  • Also consider if https://stackoverflow.com/questions/3556351/why-we-need-thread-memorybarrier doesn't address your query – Fabulous Jun 22 '17 at 22:26
  • "for the second thread" - neither thread is first or second - they're just different threads; the "second" thread could have done everything it needs to before `Run` even gets invoked... in which case: what is the value of `_processor`? – Marc Gravell Jun 22 '17 at 22:32
  • @Fabulous its look similar but I am concerned if it also applies to reference type. They are discussing the boolean flag wich is value type, on the other type the reference is a pointer which is just a value. Hence my intuition says that the behavior should be the same and I should use MemoryBarrier to ensure portability. – Zygfryd Wieszok Jun 22 '17 at 22:59
  • @MarcGravell I know, its just the terminology I have just to call thread #1 and thread #2. And it has nothing to do with the order of execution :). – Zygfryd Wieszok Jun 22 '17 at 23:14
  • @Fabulous So far the code is working fine on my machine, but I am concerned if there might occur the situation when _processor will be optimized and the thread #2 will always get null (despite the _processor is created) – Zygfryd Wieszok Jun 22 '17 at 23:17
  • Is your goal to write code that at least looks correct or to try to learn memory barriers, caching, and overall lock-free code? – Alexei Levenkov Jun 22 '17 at 23:25
  • @AlexeiLevenkov I am trying to figure out it the memory barriers are required in this case. Indeed I am trying to learn how memory barriers, caching work in C#. – Zygfryd Wieszok Jun 23 '17 at 06:26
  • @ZygfrydWieszok you probably need to ask the question again this time actually asking about what you are interested in. Also you'd need to provide very clear explanation why you believe particular version of lock-free code should / should not work as number of people who can actually answer this type of question is very small and they generally would not spent time re-teaching the same basics... (I can't answer question on memory barriers - so far I've not seen case when understanding such low level concept is required for correct code over easier to understand `lock` and `InterlockedXxxx`) – Alexei Levenkov Jun 23 '17 at 15:12

1 Answers1

0

The 2 alternative is wrong if you want access to _processor obj in this way.

In any way, you should have null in processor because DoWork run and finish faster. Try to look for same second the first thread with sleep and check if available is executed.

Your code not provide enough information for suggest the right way.

Is right to use volatile keyword but if Run is fast available is not call never

EDIT

Ok, good example. But the problem is the main worker with Run finish before you UpdateResources is executed.

If you want that UpdateResources is execute twice before wait, you insert new method that execute _processor= null after wait. In some way every with this code, everytime you call UpdateResources before wait, your risk is _processor is null.

Depend on what you want before wait

Pasalino
  • 992
  • 1
  • 9
  • 15
  • 1
    `volatile` is a very subtle and nuanced beast; most of what people think of as the "effect" of `volatile` are actually just secondary side-effects and implementation details. Asking anyone to define what `volatile` *actually means*, and how that helps a given situation, is ... well, except for a few CPU experts, most people can't give a good answer. I include myself in that group of "unable to give a good answer" – Marc Gravell Jun 22 '17 at 22:35
  • I'm not understand what do you want with this thread. If you want a tips for your problem or other. Volatile is used for have always the most update value in variabile. This keyword comunicate to compiler to optimize value for multi thread access. Anyway, your code not execute Update resource if run is finish. The method update resources is called only if is called during doWork execution – Pasalino Jun 22 '17 at 22:39
  • @Pasalino I have added the better example, I hope it will help. – Zygfryd Wieszok Jun 22 '17 at 23:01
  • @Pasalino I know that I risk _processor being null at any time, but its correct behavior - the UpdateResources has sense only when DoWork is ongoing. If DoWork has finished before UpdateResources invoke then update has no sense and UpdateResources is not called. However, my concern is if the CPU can optimize the line _processor = new Processor() and keep _processor value in CPU register (or cache), never storing it into memory, hence thread #2 would always get null (despite the _processor is alive or not). – Zygfryd Wieszok Jun 23 '17 at 14:34