线程安全代码变得过于笨拙

本文关键字:于笨拙 安全 代码 线程 | 更新日期: 2023-09-27 18:07:11

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

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

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

为例,使用下面的简单代码,

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

如此累人和不可读…

是否有任何方法告诉编译器只是简单地锁定sbLocker每次有任何访问sb?

为什么我的代码需要为这么简单的规则这么笨拙?没有太多的考虑到这个具体的,但非常有用的技术。能不能更简单一点?

我们甚至不能继承StringBuilder,因为它是密封的。

当然,也可以把整个类包装起来:

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

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

任何想法如何使线程安全实践在这个意义上?

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

线程安全代码变得过于笨拙

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

你认为它不能以可读的方式编写是错误的。不幸的是,很难把它变成一个答案,我所能做的就是提供一些指导:

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

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

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

var sb = new StringBuilder();
var tw = new StringWriter(sb);
var threadSafeWriter = TextWriter.Synchronized(tw);
threadSafeWriter.Write("Hello");
threadSafeWriter.WriteLine(" world");

而且,线程安全的并发集合也很方便。

如果你真的希望"编译器在每次访问对象时都锁定它",而不需要为每种类型编写自定义包装器,你可以使用一些库,比如Castle。代理,在运行时生成包装器。然而,在一些重要的场景中,当应该自动执行多个对象访问时,这将不会产生您需要的结果。