关于锁性能/使用的2个问题

时间:2009-12-21 13:13:50

标签: c# multithreading locking

在服务器应用程序上,我需要为每个连接的客户端分配一个唯一的ID,所以我这样做:

private short GetFreeID()
{
    lock (this.mUsedPlayerIDsSynchronization)
    {
        for (short I = 1; I < 500; I++)
        {
            if (ClientIDPool[I] == false)
            {
                ClientIDPool[I] = true;
                return I;
            }
        }
        return -1;
    }
}

我的第一个问题:能否更有效地完成,我的意思是表现更好?我在这里读过,我们应该学会编写没有锁的代码。我还在那里阅读了一些原子操作还有其他选择。 第二个问题:如果我想锁定整个课程以便不允许在其中进行任何更改,该怎么办?例如:一个客户端将更新第二个客户端数据,我可以锁定它被绝对阻止的整个第二个客户端类吗?我仍然认为“锁定”只会确保其片段中的代码当时只被一个线程输入,所以我不知道“lock(client2)”是否会导致该类中的任何内容都无法更改,直到此锁定为止释放。

5 个答案:

答案 0 :(得分:11)

锁定通常是最简单的方式,这是非常重要的。通常情况下,如果有更有效的处理方式并不重要,只要你有清晰的代码并且足够

然而,更高效的方法是生成随机GUID,或者如果 想要重用ID,则拥有“池”(例如LinkedList)未使用的ID。然后,您可以非常快速地从池中取出,并在完成后将ID返回池中(再次快速)。

或者,如果你真的只需要一个整数并且它不 是一个低整数,你可以有一个静态变量,它从0开始,你只是每次递增 - 你如果您愿意,可以使用Interlocked.Increment进行锁定。我怀疑你会用完64位整数,例如:)

关于你的第二个问题:是的,锁是建议性的。如果类中的所有内容在更改任何字段之前取出相同的锁(并且字段是私有的)那么这会阻止其他代码行为不端......但是每个代码 需要取出锁

编辑:如果你真的只需要一个整数,我仍然建议你只使用Interlocked.Increment - 即使你的流量增加了1000倍,你也可以使用64位整数。但是,如果您想重用ID,那么我建议创建一个新类型来表示“池”。给 计算已创建的数量的计数器,这样如果你用完就可以分配一个新项目。然后只需将可用的内容存储在Queue<int>LinkedList<int>Stack<int>中(您使用的内容并不重要)。假设您可以信任自己的代码以合理地返回ID,那么您可以使API简单如下:

int AllocateID()
void ReturnID(int id)

AllocateID将检查池是否为空,如果是,则分配新ID。否则,它只会删除池中的第一个条目并返回该条目。 ReturnID只会将指定的ID添加到池中。

答案 1 :(得分:2)

您在扫描数组时锁定。

你最好有2个筹码。一个是带有免费ID的,一个是带有ID的。这样你就可以弹出第一个堆叠中的一个并将其推到第二个堆栈上。

这样你锁定的时间就会长得多。

答案 2 :(得分:2)

You can allocate state on thread local memory.线程本地内存是线程安全的(只要你没有传递指针arround)。

您可以使用两个整数生成唯一编号,只有一个是同步编号。

整数1:一个递增的整数,代表线程,每次初始化一个线程时都会产生一个新的数字(这应该是一个罕见的事件)。

Integer2:在线程初始化时,您将在0开始此整数。

您将使用存储在线程本地存储器中的两个整数作为唯一整数,并且整数2将正常递增(解锁)。

这样生成唯一整数绝对是线程安全的 - 即,您不必使用原子CPU指令--Interlocked.increment(这会导致硬件级性能损失)。

- 编辑:缓存一致性 -

from :

  

缓存一致性

     

减少内存访问所需的时间不同的缓存   使用:最近访问的内存   复制在CPU缓存中   明显快于普通人   记忆。未来访问相同   地址将使用保存在缓存中的数据,   减少获取时间。问题   出现在SMP中(对称的   多处理系统,在那里   几个处理器都有自己的缓存   内存:当一个处理器发生变化时   内存区域中的变量,由...使用   它同时是几个处理器   实际上改变了自己的副本   变量,位于缓存中,而   共享变量仍然具有原始性   值。这个问题不可能   通过在a上使用volatile关键字解决   共享变量,因为这只会   保证写入内存   指示将出现在结果中   程序,但与之相关的操作   缓存仍未指定。的   当然,可以禁用CPU   缓存,将内存映射为无缓存   (PAGE_NOCACHE保护标志   VirtualAlloc()Win32 API函数),   但同时显着放缓   这有一些限制:因为   例如,互锁指令可以   在无缓存上引发硬件异常   存储器中。

     

为了正确处理SMP系统中存储在更多缓存中的数据   比一个处理器应该是相同的   在所有缓存中。这意味着CPU   缓存必须同步(保持   连贯的)硬件级别**。但它   重要的是要注意缓存   同步(缓存流程   一致性交通)   与程序执行异步:**   当一个CPU更改共享的值时   暂时变量另一个CPU   观察旧价值。 这意味着CPU   继续执行而不等待   用于缓存一致性操作   完成。此外,如果两个   变量(a然后b)被改变了   第一个CPU,另一个CPU可以观察到   b已经改变了早于。

     

互锁指令在此方面存在很大差异   点。完全,互锁   指令是一个命令   直接在物理上的东西   锁定总线下的内存。这意味着   缓存不一致的情况并非如此   影响共享的程序   仅使用访问变量   互锁指令(注意   两个过程,谁读的   变量和写入它应该   使用互锁指令)。

- 编辑:进一步澄清: -

根据您当前的设计,互锁增量为indeed your best bet,但远非理想。您的CPU有一个非常快的缓存片(通常以与CPU相同的速度计时)。如果你的线程有本地内存,它将被拉入你的线程所在的CPU,这意味着你的CPU不必转到主内存,它可以全速飞行。

如果使用互锁增量,则CPU必须

  1. 锁定公共汽车。
  2. 增加32位字。
  3. 释放公共汽车。
  4. 你可以不用这个。我可能看起来很迂腐,因为开销只有be a 100% decrease in relative performance。然而,在具有4个物理CPU和16个核心的工业服务器应用程序中,每个请求都会触发此UID生成器...相信你,你的总线将被搞砸。微优化是编程中的一个重要领域,特别是当我们现在横向扩展时。

答案 3 :(得分:1)

我建议您使用GUID,除非您需要使用短片。 Guid.NewGuid()是线程安全的,因此您无需使用锁或其他同步机制。

答案 4 :(得分:1)

你关心返回的ID吗?您可以每次使用Interlocked.Increment递增客户端ID或生成GUID(前者可能更快)。

然后使用一个简单的计数器来跟踪连接的客户端数量,而不是每次都扫描数组。