线程安全代码变得太笨拙了?

时间:2013-04-02 19:03:41

标签: c# multithreading thread-safety

我从未真正编写过多线程应用程序。

在我写过的几次中,在我看来,线程安全太快就太笨拙了。

互联网上有很多关于线程安全通用技术的教程,但我发现现实世界的编程问题并不多。

例如,采用这个简单的代码,(什么都不用)

    StringBuilder sb = new StringBuilder();
    void Something()
    {
        sb.AppendLine("Numbers!");
        int oldLength = sb.Length;
        int i = 0;
        while (sb.Length < 500 + oldLength)
        {
            sb.Append((++i).ToString());
            //something slow
            Thread.Sleep(1000);
            if (i % 2 == 0)
            {
                sb.Append("!");
            }
            sb.AppendLine();
        }
    }

现在假设我想从多个线程运行此方法,所有线程都写入相同的字符串构建器。

我希望他们一起写入同一个目标,因此可能会有一行来自一个线程,紧接着另一个线程的另一行,然后是第一个线程的下一行。

为了进行对话,我们假设即使另一行的中间中的一个线程有一条线

这是代码:

    StringBuilder sb = new StringBuilder();
    object sbLocker = new object();
    void SomethingSafe()
    {
        int oldLength;
        int length;

        lock (sbLocker)
        {
            sb.AppendLine("Numbers!");
            oldLength = sb.Length;
            length = oldLength;
        }


        int i = 0;

        while (length < 500 + oldLength)
        {
            lock (sbLocker)
                sb.Append((++i).ToString());

            //something slow
            Thread.Sleep(1000);
            if (i % 2 == 0)
            {
                lock (sbLocker)
                    sb.Append("(EVEN)");
            }

            lock (sbLocker)
            {
                sb.AppendLine();
                length = sb.Length;
            }
        }
    }

令人筋疲力尽......

有没有办法告诉编译器每次有权访问sb时只是简单地锁定sbLocker?

为什么我的代码需要对这么简单的规则如此笨拙?对这种特定但非常有用的技术没有太多的思考。它可以更轻松地完成吗?

我们甚至无法继承StringBuilder,因为它已被密封。

当然,人们可以继续整课:

public class SafeStringBuilder
{
    private StringBuilder sb = new StringBuilder();
    object locker = new object();
    public void Append(string s)
    {
        lock (locker)
        {
            sb.Append(s);
        }
    }

    //................
}

但这很疯狂......因为我们正在使用这么多不同的类。

知道如何在这个意义上做出线程安全的做法吗?

我知道,为了创建完全相同的结果,可能会有一个更简单的解决方案......但这只是一个例子。我很确定我遇到了类似的问题而没有任何可读的解决方案。

2 个答案:

答案 0 :(得分:3)

你是正确的,写线程安全代码可能比编写单线程代码复杂得多。在任何不必要的地方都应该避免这种情况。

假设无法以可读方式编写,则表示错误。不幸的是,很难将其作为答案,我能做的最好的就是提供一些指导原则:

  1. 在线程中查找分区的逻辑位置。像构建一个普通字符串之类的东西并不真正有意义,因为它不会加速你正在做的事情。为了使多线程有意义,必须有一些部分可以独立地(或大部分独立地)完成程序的其他部分。很好的例子包括矩阵乘法和响应客户端请求的服务器。有可能如果你是多线程的东西,不适合多线程,那么编写优雅代码将非常困难。
  2. 无论何时需要数据共享,都要尝试遵循模型:锁定,访问/更改,解锁。
  3. 按照层次结构锁定订单。
  4. 尽可能避免通过复制,消息传递等方式直接共享数据。
  5. 尽可能多地隐藏代码的其余部分。给他们尽可能处理共享数据的函数。
  6. 不幸的是,编写优雅的多线程代码比使用堆栈溢出问题可以教授的内容更能完善多年,但希望这个答案能够带来一些启发。

答案 1 :(得分:1)

某些框架类为您提供了可以使用的线程安全包装器。例如,您可以在StringBuilder上创建StringWriter,然后使用TextWriter.Synchronized来获取可以从多个线程同时访问的线程安全包装器。

var sb = new StringBuilder();
var tw = new StringWriter(sb);

var threadSafeWriter = TextWriter.Synchronized(tw);

threadSafeWriter.Write("Hello");
threadSafeWriter.WriteLine(" world");

而且,还有线程安全的并发集合,可以很方便。

如果你真的想要“编译器在每次访问时锁定对象”而不为每种类型编写自定义包装器,你可以使用一些库,比如Castle.Proxy,在运行时生成包装器。但是,在非平凡的场景中,当应该以原子方式执行多个对象访问时,这不会产生您需要的结果。