Thread.MemoryBarrier和lock在一个简单属性中的区别

4

对于以下情况,使用 MemoryBarrier 是否会对线程安全性、结果和性能产生任何差异?

private SomeType field;

public SomeType Property
{
    get
    {
        Thread.MemoryBarrier();
        SomeType result = field;
        Thread.MemoryBarrier();
        return result;
    }
    set
    {
        Thread.MemoryBarrier();
        field = value;
        Thread.MemoryBarrier();
    }
}

还有lock语句(Monitor.EnterMonitor.Exit

private SomeType field;
private readonly object syncLock = new object();

public SomeType Property
{
    get
    {
        lock (syncLock)
        {
            return field;
        }
    }
    set
    {
        lock (syncLock)
        {
            field = value;
        }
    }
}

因为引用赋值是原子的,所以在这种情况下我们不需要任何锁定机制。

性能 对于发布版本,MemoryBarrier比锁实现快约2倍。以下是我的测试结果:

Lock
Normaly: 5397 ms
Passed as interface: 5431 ms

Double Barrier
Normaly: 2786 ms
Passed as interface: 3754 ms

volatile
Normaly: 250 ms
Passed as interface: 668 ms

Volatile Read/Write
Normaly: 253 ms
Passed as interface: 697 ms

ReaderWriterLockSlim
Normaly: 9272 ms
Passed as interface: 10040 ms

Single Barrier: freshness of Property
Normaly: 1491 ms
Passed as interface: 2510 ms

Single Barrier: other not reodering
Normaly: 1477 ms
Passed as interface: 2275 ms

这是我在LINQPad中进行测试的方法(在首选项中设置了优化):
void Main()
{   
    "Lock".Dump();
    string temp;
    var a = new A();
    var watch = Stopwatch.StartNew();
    for (int i = 0; i < 100000000; ++i)
    {
        temp = a.Property;
        a.Property = temp;
    }
    Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms");
    Test(a);

    "Double Barrier".Dump();
    var b = new B();
    watch.Restart();
    for (int i = 0; i < 100000000; ++i)
    {
        temp = b.Property;
        b.Property = temp;
    }
    Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms");
    Test(b);

    "volatile".Dump();
    var c = new C();
    watch.Restart();
    for (int i = 0; i < 100000000; ++i)
    {
        temp = c.Property;
        c.Property = temp;
    }
    Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms");
    Test(c);

    "Volatile Read/Write".Dump();
    var d = new D();
    watch.Restart();
    for (int i = 0; i < 100000000; ++i)
    {
        temp = d.Property;
        d.Property = temp;
    }
    Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms");
    Test(d);

    "ReaderWriterLockSlim".Dump();
    var e = new E();
    watch.Restart();
    for (int i = 0; i < 100000000; ++i)
    {
        temp = e.Property;
        e.Property = temp;
    }
    Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms");
    Test(e);

    "Single Barrier: freshness of Property".Dump();
    var f = new F();
    watch.Restart();
    for (int i = 0; i < 100000000; ++i)
    {
        temp = f.Property;
        f.Property = temp;
    }
    Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms");
    Test(f);

    "Single Barrier: other not reodering".Dump();
    var g = new G();
    watch.Restart();
    for (int i = 0; i < 100000000; ++i)
    {
        temp = g.Property;
        g.Property = temp;
    }
    Console.WriteLine("Normaly: " + watch.ElapsedMilliseconds + " ms");
    Test(g);
}

void Test(I a)
{
    string temp;
    var watch = Stopwatch.StartNew();
    for (int i = 0; i < 100000000; ++i)
    {
        temp = a.Property;
        a.Property = temp;
    }

    Console.WriteLine("Passed as interface: " + watch.ElapsedMilliseconds + " ms\n");
}

interface I
{
    string Property { get; set; }
}

class A : I
{
    private string field;
    private readonly object syncLock = new object();

    public string Property
    {
        get
        {
            lock (syncLock)
            {
                return field;
            }
        }
        set
        {
            lock (syncLock)
            {
                field = value;
            }
        }
    }
}

class B : I
{
    private string field;

    public string Property
    {
        get
        {
            Thread.MemoryBarrier();
            string result = field;
            Thread.MemoryBarrier();
            return result;
        }
        set
        {
            Thread.MemoryBarrier();
            field = value;
            Thread.MemoryBarrier();
        }
    }
}

class C : I
{
    private volatile string field;

    public string Property
    {
        get
        {
            return field;
        }
        set
        {
            field = value;
        }
    }
}

class D : I
{
    private string field;

    public string Property
    {
        get
        {
            return Volatile.Read(ref field);
        }
        set
        {
            Volatile.Write(ref field, value);
        }
    }
}

class E : I
{
    private string field;
    private ReaderWriterLockSlim locker = new ReaderWriterLockSlim();

    public string Property
    {
        get
        {
            locker.EnterReadLock();
            string result = field;
            locker.ExitReadLock();
            return result;
        }
        set
        {
            locker.EnterReadLock();
            field = value;
            locker.ExitReadLock();
        }
    }
}

class F : I
{
    private string field;

    public string Property
    {
        get
        {
            Thread.MemoryBarrier();
            return field;
        }
        set
        {
            field = value;
            Thread.MemoryBarrier();
        }
    }
}

class G : I
{
    private string field;

    public string Property
    {
        get
        {
            string result = field;
            Thread.MemoryBarrier();
            return result;
        }
        set
        {
            Thread.MemoryBarrier();
            field = value;
        }
    }
}

你已经阅读了这篇文章吗?https://dev59.com/xHI-5IYBdhLWcg3wHUnP - Michał Komorowski
我读了几遍 :) 它真的很棒!缓存 - 这就是为什么我添加了Thread.MemoryBarrier(),因为:“最简单的内存屏障是完整的内存屏障(完整的栅栏),它防止在该栅栏周围进行任何指令重排序或缓存。” - Pellared
1
@Luaan:在写入操作周围设置完整的栅栏并将读取操作解锁是不够的。我正在撰写一对关于这个主题的文章。第一篇在这里:http://blog.coverity.com/2014/03/12/can-skip-lock-reading-integer/ 第二篇将于2014年3月26日发布。 - Eric Lippert
1
@EricLippert 非常棒的文章,谢谢。但是,除非在属性上进行多个操作的锁定,否则“Interlocked”写入时裸读取仍然与完全锁定的“set”和“get”一样不安全,对吗?如果我们不知道属性访问周围的代码,我们实际上可以说有关安全性的任何通用信息吗? - Luaan
2
@Luaan:确实,如果不知道代码的其余部分和程序不变量是什么,很难推断程序的正确性。这就是为什么低锁编码如此困难的原因;你真的无法孤立地分析它。仅仅因为每个砖块都很坚固,并不意味着房子不是空心的。 - Eric Lippert
显示剩余6条评论
3个回答

13

关于线程安全性是否有区别?

两者都确保在读写操作周围设置适当的障碍。

结果?

在两种情况下,两个线程可以争先恐后地写入一个值。但是,读和写不能向前或向后移动超过锁定或完整屏障。

性能?

你以两种方式编写了代码。现在运行它。如果想知道哪个更快,请运行它并找出答案!如果你有两匹马,想知道哪匹更快,请比赛。不要问陌生人他们认为哪匹马更快。

话虽如此,更好的技术是设定性能目标,编写清晰正确的代码,然后测试是否达到了目标。如果做到了,不要浪费宝贵的时间尝试进一步优化已经足够快的代码;花时间优化其他不够快的东西。

你没有问的问题:

你会怎么做?

我不会写多线程程序。如果必须要写,我会使用进程作为并发单元。

如果我必须编写多线程程序,那么我会使用最高级别的工具。我会使用任务并行库、异步等待、Lazy<T>等。我会避免共享内存;我会将线程视为返回异步值的轻量级进程。

如果我必须编写共享内存的多线程程序,那么我会一直锁定所有内容。如今我们经常编写可以从卫星链路获取十亿字节视频并将其发送到手机的程序。花费20纳秒来获取一个锁并不会致命。

我不够聪明去尝试编写低锁定代码,所以我根本不会这样做。如果我必须这样做,我将使用低锁定代码构建更高级别的抽象并使用该抽象。幸运的是,我不必这样做,因为已经有人建立了我需要的抽象。


谢谢你的回答!我想提高自己关于多线程方面的知识。等我有时间做一些性能测试后,我会发布结果的。 - Pellared
我又提了一个关于“你会怎么做”的问题:https://dev59.com/d33aa4cB1Zd3GeqPg7VM。感谢您在这个问题上的帮助! - Pellared

1
没有线程安全方面的区别。然而,我更喜欢:
private SomeType field

public SomeType Property
{
    get
    {
        return Volatile.Read(ref field);
    }
    set
    {
        Volatile.Write(ref field, value);
    }
}

"或者,"
private volatile SomeType field

public SomeType Property
{
    get
    {
        return field;
    }
    set
    {
        field = value;
    }
}

1
是的,但如果您先编写再读取,则编译器可以交换指令-写入和读取之间没有障碍。我不确定如果在读取和写入中仅使用单个字段是否会发生这种情况。另外,您的两个示例实际上并不相同(Volatile.XXX执行Thread.MemoryBarrier)。有关更多DF风格的乐趣,请参见http://joeduffyblog.com/2008/06/13/volatile-reads-and-writes-and-timeliness/。 - Luaan
1
@Luaan:即使在带锁的版本中,两个线程也可能竞争确定读取或写入哪个先发生。这里有什么不同?你指出易失性读写可以被重新排序是正确的,但是想出一个实际的场景来展示坏行为可能会很棘手。 - Eric Lippert
1
@EricLippert 你说得对,这段代码实际上太简单了,没有任何区别。我一直在思考如果这种东西被用作非阻塞同步尝试的一部分会发生什么。总的来说,这段代码似乎是个坏主意。它真正想要完成的是什么,作为更大的画面的一部分?如果需要在某些更大的操作中进行同步,就不应该在属性内部完成。它似乎提供了一些保证,但实际上并没有任何有用的保证,对吧? - Luaan

1
只要所讨论的变量是可以原子地获取/设置的有限变量之一(即引用类型),那么是的,这两个解决方案都适用于相同的线程相关约束。但是,我会诚实地认为MemoryBarrier解决方案的性能比锁定还要差。访问未争用的lock块非常快,它已经针对该情况进行了优化。另一方面,引入内存屏障不仅影响对那个变量的访问,就像lock一样,而且影响所有内存,这可能会在应用程序的其他方面产生显着的负面性能影响。当然,您需要进行一些测试来确定(真实应用程序的测试,因为单独测试这两个不会揭示内存屏障强制同步所有其他应用程序内存的事实,而不仅仅是这一个变量)。

你确定性能没问题吗?lock也会生成一个栅栏。请参考:http://www.albahari.com/threading/part4.aspx。你确定`lock`只为*一个变量*生成栅栏吗?你有任何资源吗? - Pellared
@Pellared 不,我不确定,这就是为什么我特别说了,“我会期望”和“当然你需要做一些测试来确保”。lock只是确定与lock相关的操作如何被重新排序。它不考虑不在lock中的操作。它不考虑不在lock中的操作,这就引出了这个问题 - Servy
MemoryBarrier 也涉及操作排序。感谢您的回答。 - Pellared
2
@Pellared:为了澄清Servy的回答:在实践中,锁创建了栅栏,确保线程上的所有读取不会在锁的开始之前向后移动时间,并且所有写入不会在锁的结束之后向前移动时间。规范的第3.10节指出了C#实现必须提供的略微较弱的保证。两周后,我将撰写一篇关于如何重新排序易失性读写以及如何使用锁来防止这种情况的博客文章;请查看我的博客获取详细信息。 - Eric Lippert
@Eric Lippert:谢谢。澄清一下:Cory Nelson的答案是不正确的,对吗?我还正确地理解了Luaan评论我的问题,即实现不能使用“Interlocked”吗? - Pellared
1
@Pellared: Luaan对Cory Nelson的回答的评论很相关。Volatile.Read / Write引入的屏障比你引入的全屏障要弱。 - Eric Lippert

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接