线程安全代码变得过于笨拙
本文关键字:于笨拙 安全 代码 线程 | 更新日期: 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);
}
}
//................
}
但这太疯狂了…因为我们使用了很多不同的类。
任何想法如何使线程安全实践在这个意义上?
我知道创建完全相同的结果可能有一个更简单的解决方案…但这只是一个例子。我很确定我遇到过类似的问题,但没有任何可读的解决方案。
您是正确的,编写线程安全代码可能比编写单线程代码要复杂得多。在没有必要的情况下,这可能是应该避免的。
你认为它不能以可读的方式编写是错误的。不幸的是,很难把它变成一个答案,我所能做的就是提供一些指导:
- 在线程中找到分区的逻辑位置。像建立一个普通的字符串这样的东西并没有真正的意义,因为它不会加快你所做的事情。为了使多线程有意义,必须有一些部分可以独立(或大部分独立)完成程序的其他部分。好的例子包括矩阵乘法和响应客户机请求的服务器。很有可能,如果你正在多线程处理一些不适合多线程的东西,那么编写优雅的代码将是非常困难的。
- 任何需要数据共享的地方,请尝试遵循以下模型:锁,访问/修改,解锁。
- 锁顺序遵循层次结构。
- 尽可能避免通过复制、消息传递等方式直接共享数据。 在尽可能多的代码中隐藏锁。只要可能,就给它们提供处理共享数据的函数。
不幸的是,编写优雅的多线程代码更多的是需要你多年的努力才能完善的东西,而不是可以在堆栈溢出问题中教授的东西,但希望这个答案能给你一些启示。
一些框架类为您提供了可以使用的线程安全包装器。例如,您可以在StringBuilder上创建一个StringWriter
,然后使用textwwriter。同步以获得一个线程安全的包装器,可以从多个线程同时访问。
var sb = new StringBuilder();
var tw = new StringWriter(sb);
var threadSafeWriter = TextWriter.Synchronized(tw);
threadSafeWriter.Write("Hello");
threadSafeWriter.WriteLine(" world");
而且,线程安全的并发集合也很方便。
如果你真的希望"编译器在每次访问对象时都锁定它",而不需要为每种类型编写自定义包装器,你可以使用一些库,比如Castle。代理,在运行时生成包装器。然而,在一些重要的场景中,当应该自动执行多个对象访问时,这将不会产生您需要的结果。