3

我从来没有真正写过多线程应用程序。

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

互联网上到处都是关于线程安全通用技术的教程,但我发现的关于现实世界编程问题的内容并不多。

例如,采用这个简单的代码,(没有任何用处)

    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);
        }
    }

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

但这太疯狂了……因为我们正在使用很多不同的类。

知道如何在这个意义上进行线程安全实践吗?

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

4

2 回答 2

3

您是正确的,因为编写线程安全代码可能比编写单线程代码复杂得多。在没有必要的地方可能应该避免这种情况。

你假设它不能以可读的方式编写是不正确的。不幸的是,这真的很难成为答案,我能做的最好的就是提供一些指导:

  1. 查找线程中划分的逻辑位置。像构建一个通用字符串这样的东西并没有真正的意义,因为它不会加快你正在做的事情。为了使多线程有意义,必须有一些部分可以独立于(或大部分独立于)程序的其他部分完成。很好的例子包括矩阵乘法和响应客户端请求的服务器。很有可能,如果您正在对不适合多线程的东西进行多线程处理,那么编写优雅的代码将异常困难。
  2. 在需要数据共享的地方,尝试遵循模型:锁定、访问/更改、解锁。
  3. 遵循锁定顺序的层次结构。
  4. 尽可能避免通过复制、消息传递等方式直接共享数据。
  5. 尽可能对其余代码隐藏锁定。尽可能为他们提供处理共享数据的功能。

不幸的是,编写优雅的多线程代码更多的是您经过多年努力才能完善的东西,而不是可以在堆栈溢出问题中教授的东西,但希望这个答案能带来一些启发。

于 2013-04-02T19:53:06.277 回答
1

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

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

var threadSafeWriter = TextWriter.Synchronized(tw);

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

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

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

于 2013-04-02T19:55:40.210 回答