无锁线程安全状态同步?

时间:2016-03-29 13:45:17

标签: c# multithreading thread-safety thread-synchronization

首先,我知道这样的问题:

reference assignment is atomic so why is Interlocked.Exchange(ref Object, Object) needed?

...但我仍然不确定是否可以避免在我的情况下使用lock(){}。

在我的情况下,我有一个代表某个状态的类,只有一个可以不时修改该状态的SINGLE线程。虽然有许多线程可以读取状态。

我的状态对象上是否需要Interlocked.Exchange()? 我绝对必须使用lock(){}

这是我的示例代码减少到最低限度:

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace MultiThreadingExample
{
class State
{
    public int X { get; set; }
    public string Str { get; set; }
    public DateTime Current { get; set; }
}

class Example
{
    State state;
    CancellationTokenSource cts = new CancellationTokenSource();

    Task updater;
    List<Task> readers = new List<Task>();

    public void Run()
    {
        updater = Task.Factory.StartNew(() =>
        {
            while (!cts.Token.IsCancellationRequested)
            {
                // wait until we have a new state from some source
                Thread.Sleep(1000);

                var newState = new State() { Current = DateTime.Now, X = DateTime.Now.Millisecond, Str = DateTime.Now.ToString() };

                // critical part
                state = newState;
            }
        }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

        for (int i = 0; i < 10; i++)
        {
            readers.Add(Task.Factory.StartNew(() =>
            {
                while (!cts.Token.IsCancellationRequested)
                {
                    // critical part
                    var readState = state;

                    // use it
                    if (readState != null)
                    {
                        Console.WriteLine(readState.Current);
                        Console.WriteLine(readState.Str);
                        Console.WriteLine(readState.X);
                    }
                }
            }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default));
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        new Example().Run();
        Console.ReadKey();
    }
}
}

4 个答案:

答案 0 :(得分:1)

如果您只有一个正在更新的线程且只有一个线程读取,我不相信您会遇到任何运行时错误。然而,您的示例显示了10个读者线程话虽这么说,我不认为你应该假设你不需要任何东西来使你的应用程序线程安全。你应该至少引入锁定,以确保你的线程能够很好地相互配合。因为当您读取读者线程中的值时, State 对象是一个复杂的对象,所以您可能无法按预期获得所有内容。如果没有锁定,在读取操作期间可能会更改一个或两个属性,但不会更改第三个以下是我正在讨论的修改示例。

class State
{
    public int X { get; set; }
    public string Str { get; set; }
    public DateTime Current { get; set; }
}

class Example
{
    State state;
    CancellationTokenSource cts = new CancellationTokenSource();
    Object syncObj = new Object();
    Task updater;
    List<Task> readers = new List<Task>();

    public void Run()
    {
        updater = Task.Factory.StartNew(() =>
        {
            while (!cts.Token.IsCancellationRequested)
            {
                // wait until we have a new state from some source
                Thread.Sleep(1000);

                var newState = new State() { Current = DateTime.Now, X = DateTime.Now.Millisecond, Str = DateTime.Now.ToString() };

                // critical part
                lock(syncObj) {
                    state = newState;
                }
            }
        }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

        for (int i = 0; i < 10; i++)
        {
            readers.Add(Task.Factory.StartNew(() =>
            {
                while (!cts.Token.IsCancellationRequested)
                {
                    State readState = null;

                    // critical part
                    lock(syncObj) {
                        readState = state.Clone();
                    }

                    // use it
                    if (readState != null)
                    {
                        Console.WriteLine(readState.Current);
                        Console.WriteLine(readState.Str);
                        Console.WriteLine(readState.X);
                    }
                }
            }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default));
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        new Example().Run();
        Console.ReadKey();
    }
}

这是一个小小的改动,但它确保你对State对象有线程安全。

答案 1 :(得分:1)

怀疑时使用锁

除非您确实注意到性能问题有一个可以由异步逻辑访问的部分,可能是也可能不是多线程的,需要等待和发送信号会建议使用锁来进行线程之间的同步。

那就是说

可以使用Thread.MemoryBarrier来做你想要的事情

只要您不关心正在阅读的数据的准确性,您最终会遇到的问题与编译器优化有关这将错误地重新排序您的指令,因为它是多线程的。

你可以避免使用Thread.MemoryBarrier方法。

            while (!cts.Token.IsCancellationRequested)
            {
                //No command before the barrier
                Thread.MemoryBarrier();
                //Can end up on this side of the barrier  

                // critical part
                var readState = state;

                // use it
                if (readState != null)
                {
                    Console.WriteLine(readState.Current);
                    Console.WriteLine(readState.Str);
                    Console.WriteLine(readState.X);
                }
            }

这被称为半栅栏,这里有更多关于栅栏的信息比我更好地解释a free ebook, Threading in C# by Joseph Albahari

答案 2 :(得分:1)

现在编写代码的方式,单个“更新程序”任务将state设置为某个值,然后所有读者开始阅读并尽快处理它他们有机会这样做。并且他们会永远阅读这种状态,或直到它发生变化。

你可能不希望10个线程做同样的事情,然后在下一个周期再次做同样的事情,直到state被改变。

实施单一生产者/多个消费者的正确方法

至少在其中一个读者中原子地将state设置为null

// read and swap with null atomically
var readState = Interlocked.Exchange(ref state, null);

这仍然会让你的读者疯狂地旋转CPU,这可能不是你想要的。

更好的解决方案是使用BlockingCollection,它可以解决您的大多数问题:

BlockingCollection<State> queue = new BlockingCollection<State>();

updater = Task.Factory.StartNew(() =>
{
    while (!cts.Token.IsCancellationRequested)
    {
        Thread.Sleep(1000);

        var newState = GetNewState();

        queue.Add(newState);
    }
}, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);

for (int i = 0; i < 10; i++)
{
    var readerId = i.ToString();

    readers.Add(Task.Factory.StartNew(() =>
    {
        while (!cts.Token.IsCancellationRequested)
        {
            // get it
            var readState = queue.Take(cts.Token);

            // use it
            if (readState != null)
            {
                Console.WriteLine("Hello from reader #" + readerId);
                Console.WriteLine(readState.X);
            }
        }
    }, cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default));
}

首先,BlockingCollection<T>.Take将阻止所有读者,直到它从编写器(更新程序)线程发出信号。这意味着那些线程无所事事,你的CPU应该处于空闲状态。

此外,方法accepts a CancellationToken很简洁,这意味着您不必担心在完成后解锁您的读者。

您是否只需要共享只读状态?

如果你的意图只是在线程之间共享一些只读状态(恕我直言,你的示例代码没有明确表达这个意图),那么防止自己在脚下射击的正确方法是:

  1. 设置state字段volatile以防止编译器和CPU神奇地重新排序指令并对其进行缓存,

  2. Statereadonly中填写所有字段,以防止您在分配任何字段后修改其中的任何字段。

  3. 确保State类中的所有字段都是基本类型或不可变结构/类。

答案 3 :(得分:1)

如果cts.Token.IsCancellationRequested不会导致内存障碍,则此操作不安全。如果没有,则编译器可以只读取一次并在本地缓存state

我不认为cts.Token.IsCancellationRequested是否会导致记忆障碍。通常情况下,这些问题没有记录在案。