我有一个本地类,其中有一个用于构建字符串列表的方法,我发现当我按下这个方法时(在一个1000次的for循环中),它通常不会返回我请求的数量。
我有一个全局变量:
string[] cachedKeys
传递给方法的参数:
int requestedNumberToGet
该方法与此类似:
List<string> keysToReturn = new List<string>();
int numberPossibleToGet = (cachedKeys.Length <= requestedNumberToGet) ?
cachedKeys.Length : requestedNumberToGet;
Random rand = new Random();
DateTime breakoutTime = DateTime.Now.AddMilliseconds(5);
//Do we have enough to fill the request within the time? otherwise give
//however many we currently have
while (DateTime.Now < breakoutTime
&& keysToReturn.Count < numberPossibleToGet
&& cachedKeys.Length >= numberPossibleToGet)
{
string randomKey = cachedKeys[rand.Next(0, cachedKeys.Length)];
if (!keysToReturn.Contains(randomKey))
keysToReturn.Add(randomKey);
}
if (keysToReturn.Count != numberPossibleToGet)
Debugger.Break();
我在cachedKeys中有大约40个字符串,长度不超过15个字符。
我不是线程专家,所以我实际上只是在循环中调用此方法1000次并且始终在那里进行调试。
运行的机器是一个相当强大的桌面,所以我希望突破时间是真实的,实际上它在循环的任何一点随机断开(我已经看过20s,100s,200s,300s)。
任何人都有任何想法,我在这里错了吗?
编辑:仅限于.NET 2.0
编辑:突破的目的是如果方法执行时间太长,客户端(使用XML提要数据的多个Web服务器)将不必等待其他项目依赖项初始化,他们只会得到0结果。
编辑:我想发布效果统计数据
原始
碟
Freddy Rios
答案 0 :(得分:8)
为什么不把列表的副本 - O(n) - 随机播放,也是O(n) - 然后返回已经请求的密钥数。实际上,shuffle只需要是O(nRequested)。继续使用未抽取位的开头交换列表中未抽取位的随机成员,然后将混洗位扩展为1(只是一个名义计数器)。
编辑:这里有一些代码可以将结果产生为IEnumerable<T>
。请注意,它使用延迟执行,因此如果您在首次开始迭代结果之前更改传入的源,您将看到这些更改。获取第一个结果后,元素将被缓存。
static IEnumerable<T> TakeRandom<T>(IEnumerable<T> source,
int sizeRequired,
Random rng)
{
List<T> list = new List<T>(source);
sizeRequired = Math.Min(sizeRequired, list.Count);
for (int i=0; i < sizeRequired; i++)
{
int index = rng.Next(list.Count-i);
T selected = list[i + index];
list[i + index] = list[i];
list[i] = selected;
yield return selected;
}
}
我们的想法是,在您获取n
元素之后的任何时候,列表中的第一个n
元素都将是这些元素 - 因此我们确保不会再次选择这些元素。然后从“其余”中选择一个随机元素,将其交换到正确的位置并产生它。
希望这会有所帮助。如果你正在使用C#3,你可能希望通过将“this”放在第一个参数前面来使它成为一个扩展方法。
答案 1 :(得分:4)
一些想法。
首先,您的keysToReturn列表可能会在每次循环中添加,对吧?您正在创建一个空列表,然后将每个新密钥添加到列表中。由于列表没有预先调整大小,因此每个添加都成为O(n)操作(see MSDN documentation)。要解决此问题,请尝试像这样预先调整列表大小。
int numberPossibleToGet = (cachedKeys.Length <= requestedNumberToGet) ? cachedKeys.Length : requestedNumberToGet;
List<string> keysToReturn = new List<string>(numberPossibleToGet);
其次,你的突破时间在Windows上是不现实的(好的,好的,不可能的)。我在Windows计时中读过的所有信息表明,你可能希望的最好的是10毫秒的分辨率,但实际上它更像是15-18毫秒。实际上,请尝试以下代码:
for (int iv = 0; iv < 10000; iv++) {
Console.WriteLine( DateTime.Now.Millisecond.ToString() );
}
您将在输出中看到的是离散跳跃。这是我刚在机器上运行的示例输出。
13
...
13
28
...
28
44
...
44
59
...
59
75
...
毫秒值从13跳到28到44到59到75.在我的机器的DateTime.Now函数中,这大约是15-16毫秒的分辨率。此行为与您在C运行时ftime()调用中看到的一致。换句话说,它是Windows计时机制的系统特征。关键是,你不应该依赖一致的5毫秒突破时间,因为你不会得到它。
第三,我是否正确地假设突破时间是阻止主线程锁定?如果是这样,那么将你的函数产生到ThreadPool线程并让它运行完成无论花多长时间都很容易。然后,您的主线程可以对数据进行操作。
答案 2 :(得分:4)
主要问题是在随机场景中使用重试以确保获得唯一值。这很快就会失控,特别是如果请求的物品数量接近要获得的物品数量,即如果增加钥匙数量,您会发现问题较少,但可以避免。
以下方法通过保留剩余的键列表来实现。
List<string> GetSomeKeys(string[] cachedKeys, int requestedNumberToGet)
{
int numberPossibleToGet = Math.Min(cachedKeys.Length, requestedNumberToGet);
List<string> keysRemaining = new List<string>(cachedKeys);
List<string> keysToReturn = new List<string>(numberPossibleToGet);
Random rand = new Random();
for (int i = 0; i < numberPossibleToGet; i++)
{
int randomIndex = rand.Next(keysRemaining.Count);
keysToReturn.Add(keysRemaining[randomIndex]);
keysRemaining.RemoveAt(randomIndex);
}
return keysToReturn;
}
您的版本需要超时,因为您可能会继续重试以获取较长时间的值。特别是当您想要检索整个列表时,在这种情况下,您几乎肯定会因依赖重试的版本而失败。
更新:上述效果优于以下变化:
List<string> GetSomeKeysSwapping(string[] cachedKeys, int requestedNumberToGet)
{
int numberPossibleToGet = Math.Min(cachedKeys.Length, requestedNumberToGet);
List<string> keys = new List<string>(cachedKeys);
List<string> keysToReturn = new List<string>(numberPossibleToGet);
Random rand = new Random();
for (int i = 0; i < numberPossibleToGet; i++)
{
int index = rand.Next(numberPossibleToGet - i) + i;
keysToReturn.Add(keys[index]);
keys[index] = keys[i];
}
return keysToReturn;
}
List<string> GetSomeKeysEnumerable(string[] cachedKeys, int requestedNumberToGet)
{
Random rand = new Random();
return TakeRandom(cachedKeys, requestedNumberToGet, rand).ToList();
}
一些具有10.000次迭代的数字:
Function Name Elapsed Inclusive Time Number of Calls
GetSomeKeys 6,190.66 10,000
GetSomeKeysEnumerable 15,617.04 10,000
GetSomeKeysSwapping 8,293.64 10,000
答案 3 :(得分:2)
使用HashSet
代替HashSet
查找要比List
HashSet<string> keysToReturn = new HashSet<string>();
int numberPossibleToGet = (cachedKeys.Length <= requestedNumberToGet) ? cachedKeys.Length : requestedNumberToGet;
Random rand = new Random();
DateTime breakoutTime = DateTime.Now.AddMilliseconds(5);
int length = cachedKeys.Length;
while (DateTime.Now < breakoutTime && keysToReturn.Count < numberPossibleToGet) {
int i = rand.Next(0, length);
while (!keysToReturn.Add(cachedKeys[i])) {
i++;
if (i == length)
i = 0;
}
}
答案 4 :(得分:1)
考虑使用Stopwatch
代替DateTime.Now
。当你谈论毫秒时,它可能只是DateTime.Now
的不准确性。
答案 5 :(得分:0)
问题可能就在这里:
if (!keysToReturn.Contains(randomKey))
keysToReturn.Add(randomKey);
这将需要遍历列表以确定密钥是否在返回列表中。但是,可以肯定的是,您应该尝试使用工具进行分析。此外,5毫秒非常快.005秒,你可能想要增加它。