何时在C#中使用锁定线程?

27

我有一个服务器,处理多个传入的socket连接并创建2个不同的线程以XML格式存储数据。

我几乎在每个异步调用的事件处理程序以及代码不同部分的两个线程中都使用了lock语句来保证线程安全。可悲的是,使用这种方法会导致我的应用程序显着减速。

我尝试了不使用lock,服务器执行非常快,甚至文件存储似乎得到了提升;但是程序在工作30秒至1分钟后因我不理解的原因崩溃了。

所以,我认为最好的方法是尽可能少地使用锁或仅在绝对必要的情况下使用它。 因此,我有两个问题:

  1. 当我只写入公共访问的变量(C#列表)时,是否需要锁定,还是在读取时也需要?

  2. 在由套接字处理程序创建的异步线程中是否只需要锁定,还是在其他地方也需要?

有人能给我一些实用的指南,告诉我如何操作。 这次我不会发布整个代码了。 发布大约2500行的代码没有任何意义。


2
不够具体,请查看此处的一般信息:http://www.albahari.com/threading/ - eulerfx
1
有各种可能的程序 - 从绝对错误/极快到始终正确/相对缓慢。在你的实验中,似乎触及了两个极端。线程安全是一个非常复杂的话题,因此我建议您阅读相关资料,而不仅仅是试图获得“实用指南”。例如,eulerfx的链接是一个很好的起点。 - Alexei Levenkov
5个回答

87

您是否曾经在汽车或公交车上等待红灯时,看着没有横穿马路的车辆而感到时间浪费?锁就像一个完美的红绿灯。只有当有交通时,它才是红色的。

您的问题是:“我花太多时间在等待红灯的交通拥堵中了。我应该直接闯红灯吗?或者更好的办法是,应该取消信号灯,让每个人都以高速通过交叉口,不需要任何控制?”

如果您的锁存在性能问题,那么删除锁是您应该做的最后一件事情您正在等待红灯的原因正是因为有横穿马路的车辆。如果锁没有争用,它们非常快。

您不能在消除横穿马路的车辆之前就取消交通信号灯。因此,最好的解决方案是消除横穿马路的车辆。如果锁从来没有争用,那么您永远不会等待它。找出横穿马路的车辆为什么在交叉口花费了太多时间;不要删除信号灯并希望没有碰撞。那是不可能的。

如果您不能这样做,那么增加更精细的锁有时会有所帮助。也就是说,也许城镇中的每条道路都汇聚在同一个交叉口。也许您可以将其分成两个交叉口,以便代码可以同时通过两个不同的交叉口移动。

请注意,在多线程场景中,使汽车更快(获得更快的处理器)或使道路更短(消除代码路径长度)通常会使问题恶化。就像在现实生活中一样;如果问题是交通拥堵,那么购买更快的汽车并在更短的道路上行驶只会更快地将它们带入交通堵塞中,而不会更快地将它们带离交通堵塞。


22
+1,我总是喜欢这些隐喻式的回答。它们让事情更容易理解。 - Josh Darnell
1
谢谢你的回答。你的回答很清晰,但我的意图是问:“如果在现实生活中人们发明了交通环岛,我如何确定它们应该有多大才能更好地发挥作用。”所以我只是期望得到一个更具体的答案。比如说,“这个圆形应该比我妻子的戒指大100倍”之类的! - Claudio Ferraro

33

在写入和读取公共访问变量(C# 列表)时,是否需要锁定?

是的(即使在读取时也需要)。

只有在套接字处理程序创建的异步线程中需要锁定吗?还是其他地方也需要?

是的。无论代码访问哪个共享的代码部分,都应该进行锁定。


这听起来可能不是锁定单个对象,而是为所有锁定情况锁定一个东西。

如果是这样,请通过创建单独的唯一对象来放置智能的离散锁,这些对象仅与某些部分相关联并锁定,而不会干扰其他线程在其他部分中的操作。

以下是一个示例:

// This class simulates the use of two different thread safe resources and how to lock them
// for thread safety but not block other threads getting different resources.
public class SmartLocking
{
    private string StrResource1 { get; set; }
    private string StrResource2 { get; set; }

    private object _Lock1 = new object();
    private object _Lock2 = new object();

    public void DoWorkOn1( string change )
    {
        lock (_Lock1)
        {
            _Resource1 = change;
        }
    }

    public void DoWorkOn2( string change2 )
    {
        lock (_Lock2)
        {
            _Resource2 = change2;
        }
    }
}

3
嘿,朋友。你救了我的命。我为不同情况使用了不同的锁(带有不同的静态对象),现在这个应用程序运行良好。非常感谢你。 - Claudio Ferraro
我很惊讶这个线程被锁了!决不是恶意。不正确的锁使用可能导致类似竞争条件的情况,这在我看来就是那种情况。我很高兴我的答案能够帮到你。 - ΩmegaMan
10
@ClaudioFerraro:这很好,但现在你可能正在将一个问题(性能不佳)转化为另一个问题(死锁)。你可能会遇到这样的情况:线程1取得了锁A并等待锁B,而线程2取得了锁B并等待锁A,因此它们都将永久等待。当你在程序中添加更细粒度的锁时,你必须为所有锁建立严格的排序协议。例如,你必须说:“我将永远不允许任何线程在获取锁2之后再请求锁1”。你的问题才刚刚开始;这很困难。 - Eric Lippert
2
是的,锁层次结构确实可以避免这个问题。但即使没有它:死锁也很容易调试 - 获取线程当前持有的所有锁的列表以及它们正在等待哪些锁,然后你不仅可以得到导致死锁的确切锁,还可以得到导致死锁的堆栈跟踪。可能是一个应用程序中最好的多线程错误之一 ;) - Voo
1
将您博客中的示例添加到此处会使答案更好。 - ChrisF

2

在访问成员(无论是读取还是写入)时,请始终使用锁。如果您正在遍历集合,并且从另一个线程中删除项目,则可能会很快出现问题。

建议的做法是,当您想要迭代一个集合时,将所有项复制到一个新的集合中,然后迭代该副本。例如:

var newcollection; // Initialize etc.
lock(mycollection)
{
  // Copy from mycollection to newcollection
}

foreach(var item in newcollection)
{
  // Do stuff
}

同样地,只有在实际写入列表时才使用锁定功能。

1

需要在读取时进行锁定的原因是:

假设您正在更改一个属性,而在线程处于锁定状态之间,该属性被读取了两次。一次是在我们进行任何更改之前,另一次是在更改后。那么我们将得到不一致的结果。

希望这有所帮助。


0

基本上这个问题可以很简单地回答:

你需要锁定所有被不同线程访问的东西。实际上,无论是读取还是写入都没有什么区别。如果你正在读取数据,而另一个线程同时正在覆盖该数据,那么读取到的数据可能会变得无效,你可能会执行无效操作。


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