2009-12-21 69 views
1

在服务器上的应用程序,我需要分配给每个连接的客户端的唯一的ID,所以我做这种方式:2对锁的性能问题/使用

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

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

+0

如果您确实需要性能,任何形式的CPU辅助同步都会干扰它 - 完全停止。联锁指令比普通锁更好 - 但仅限于短期。 (锁定一个总线来改变一个4字节的值不是很好= D)。 – 2009-12-22 13:39:56

回答

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添加到池中。

+0

谢谢!那么这些ID会发送给客户端,因为客户端经常重新连接(通常每小时1000个),所以它必须是可重用的。我在你的“游泳池”解决方案中有点麻烦,你可以详细说明一下吗?谢谢! – Thomas 2009-12-21 13:23:20

+0

@Tomas:我不明白每小时1000个意味着他们必须是可重用的。即使你使用从0开始的Int32,那仍然会给你每小时1000个唯一ID的245 *年* ......使用Interlocked.Increment在这里确实会更简单。 – 2009-12-21 13:28:07

+0

是的,我不会用完ID,我很感兴趣不向客户发送大量数据,有很多操作。 – Thomas 2009-12-21 13:31:26

2

您在扫描数组时正在锁定。

你最好有2堆。一个是免费ID,一个是ID正在使用。这样,您可以弹出第一个堆栈中的一个,并将其推到第二个堆栈上。

这样你锁定的时间就少了很多。

1

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

+0

如果最大值为500,您仍然需要锁定并检查连接的客户数 – Paolo 2009-12-21 13:23:07

+0

我需要尽可能少的数字,然后将这些数字发送回客户端。 – Thomas 2009-12-21 13:27:24

+0

我的建议是基于您只需要一个唯一ID的假设。如果500限制和低数字是一个要求,你需要一个免费的ID池,像乔恩建议的那样。 – 2009-12-21 13:34:57

1

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

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

2

You can allocate state on thread local memory.线程本地内存是线程安全的(只要你不通过指针)。

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

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

整数2:上线初始化开始在0

这个整数你会使用这两个整数 - 这是存储在线程局部存储器 - 作为唯一的整数,而整数2将增加通常(解锁)。

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

- 编辑:高速缓存一致性 -

from :

Cache一致性

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

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

联锁指令在这个 点有相当大的差异。确切地说,互锁 指令是在锁定总线下直接在物理 存储器上制造 的命令。这意味着 该缓存不一致并不影响 地方共享 变量的程序,只有 互锁指令(注意, 谁读取 变量,并写入了这两个过程,即,应该 使用互锁指令)进行访问。

- 编辑:进一步clarrification: -

根据当前的设计互锁递增是indeed your best bet,却是很不理想。你的CPU在片内有一个非常快的高速缓存(通常与CPU速度相同)。如果你的线程有本地内存,它将被拉进你的线程所在的CPU,这意味着你的CPU不必去主内存,它可以全速飞行。

如果使用互锁递增你的CPU将有

  1. 锁定总线。
  2. 增加32位字。
  3. 释放公共汽车。

你可以不这样做。我可能看起来行事迂腐,因为开销可能只有be a 100% decrease in relative performance。然而,在一个具有4个物理CPU和16个内核的工业服务器应用程序中,这个UID发生器会在每次请求时触发...相信我,您的总线将被拧紧。微优化是编程中的一个重要领域,尤其是当我们现在横向扩展时。

+0

Interlocked.Increment是现代CPU上的单一CPU指令,与外部抓取锁定然后递增值不同。 – Paolo 2009-12-21 18:58:19

+0

是的,它是一条单指令,但它锁定总线只是为了更改一个单词。 – 2009-12-22 13:23:47

+1

@Hassan:请记住这里的背景 - 这不是*必须优化到一英寸内的东西。简单性胜过微型优化,直到你遇到瓶颈,IMO。 – 2009-12-22 14:09:53