我制作了一个简单的库来帮助我在web应用程序中进行A/B测试。这个想法很简单:对于给定的页面,我有两个或更多的页面选项( URL ),每次对库方法的调用都应该给我一个URL,所以在最后,所有选项都被赋予相同的流量。
我的问题是:
下面是测试和实现:
[TestFixture]
public class ABTestTest
{
[SetUp]
public void Setup()
{
ABTest.ResetAll();
}
[Test]
public void GetUrlWithTwoCandidates()
{
ABTest.RegisterUrl("CarrinhoPagamento", "Url1.aspx");
ABTest.RegisterUrl("CarrinhoPagamento", "Url2.aspx");
ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url1.aspx");
ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url2.aspx");
ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url1.aspx");
ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url2.aspx");
ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url1.aspx");
ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url2.aspx");
ABTest.GetUrl("CarrinhoPagamento").Should().Be.EqualTo("Url1.aspx");
}
[Test]
public void GetUrlWithThreeCandidates()
{
var OptionsSelected = new Dictionary<string, int>();
OptionsSelected.Add("Url1.aspx", 0);
OptionsSelected.Add("Url2.aspx", 0);
OptionsSelected.Add("Url3.aspx", 0);
ABTest.RegisterUrl("CarrinhoPagamento", "Url1.aspx");
ABTest.RegisterUrl("CarrinhoPagamento", "Url2.aspx");
ABTest.RegisterUrl("CarrinhoPagamento", "Url3.aspx");
var nextUrl = "";
for (int i = 1; i < 10; i++)
{
nextUrl = ABTest.GetUrl("CarrinhoPagamento");
OptionsSelected[nextUrl]++;
}
OptionsSelected["Url1.aspx"].Should().Be.EqualTo(3);
OptionsSelected["Url2.aspx"].Should().Be.EqualTo(3);
OptionsSelected["Url3.aspx"].Should().Be.EqualTo(3);
}
[Test]
public void GetUrlWithThreeCandidatesThreaded()
{
var OptionsSelected = new Dictionary<string, int>();
OptionsSelected.Add("Url1.aspx", 0);
OptionsSelected.Add("Url2.aspx", 0);
OptionsSelected.Add("Url3.aspx", 0);
ABTest.RegisterUrl("CarrinhoPagamento", "Url1.aspx");
ABTest.RegisterUrl("CarrinhoPagamento", "Url2.aspx");
ABTest.RegisterUrl("CarrinhoPagamento", "Url3.aspx");
ThreadPool.SetMaxThreads(3, 3);
for (int i = 1; i < 10; i++)
{
ThreadPool.QueueUserWorkItem((object state) =>
{
var nextUrl = ABTest.GetUrl("CarrinhoPagamento");
OptionsSelected[nextUrl]++;
});
}
while (OptionsSelected.Select(x => x.Value).Sum() != 9)
{
Thread.Sleep(100);
}
OptionsSelected["Url1.aspx"].Should().Be.EqualTo(3);
OptionsSelected["Url2.aspx"].Should().Be.EqualTo(3);
OptionsSelected["Url3.aspx"].Should().Be.EqualTo(3);
}
}
public class ABTest
{
private volatile static Hashtable NextOption = new Hashtable();
private static Hashtable Options = new Hashtable();
public static void ResetAll()
{
lock (NextOption)
{
NextOption = new Hashtable();
}
lock (Options)
{
Options = new Hashtable();
}
}
public static void RegisterUrl(string key, string url)
{
if (Options.ContainsKey(key.GetHashCode()))
{
lock (Options)
{
((List<ABTestOption>)Options[key.GetHashCode()]).Add(new ABTestOption()
{
Url = url,
Count = 0
});
}
}
else
{
if (!Options.ContainsKey(key.GetHashCode()))
{
lock (Options)
{
if (!Options.ContainsKey(key.GetHashCode()))
{
Options.Add(
key.GetHashCode(),
new List<ABTestOption>() {
new ABTestOption() {
Url = url,
Count = 0
}
});
}
}
}
if (!NextOption.ContainsKey(key.GetHashCode()))
{
lock (NextOption)
{
if (!NextOption.ContainsKey(key.GetHashCode()))
{
NextOption.Add(key.GetHashCode(), url);
}
}
}
}
}
public static string GetUrl(string key)
{
lock (NextOption)
{
var nextUrl = (string)NextOption[key.GetHashCode()];
var keyOptions = (List<ABTestOption>)Options[key.GetHashCode()];
var selectedOption = keyOptions.Where(x => x.Url == nextUrl).First();
selectedOption.Count++;
NextOption.Remove(key.GetHashCode());
NextOption.Add(key.GetHashCode(), keyOptions.OrderBy(x => x.Count).First().Url);
return nextUrl;
}
}
private class ABTestOption
{
public string Url { get; set; }
public int Count { get; set; }
}
}发布于 2012-10-05 18:54:56
这里有一种可能的方法,希望它易于阅读。
我相信一个比我聪明的人(也会)指出这个建议可能存在的任何问题。
options字段从不为null,而且该字段的set操作是原子的,因此我认为您不需要对其进行锁定。
public static class ABTest
{
private static IDictionary<string, ABTestOption> options = new Dictionary<string, ABTestOption>();
private readonly static object locker = new object();
public static string GetUrl(string key)
{
ABTestOption option;
lock (locker)
{
if (!options.TryGetValue(key, out option))
{
return null;
}
}
return option.GetNextUrl();
}
public static void RegisterUrl(string key, string url)
{
ABTestOption option;
lock (locker)
{
if (!options.TryGetValue(key, out option))
{
option = new ABTestOption();
options[key] = option;
}
}
option.AddUrl(url);
}
public static void ResetAll()
{
options = new Dictionary<string, ABTestOption>();
}
private class ABTestOption
{
private readonly object locker = new object();
private readonly List<string> urls = new List<string>();
private int totalCallCount = -1;
public void AddUrl(string url)
{
lock (this.locker)
{
if (!this.urls.Contains(url))
{
this.urls.Add(url);
}
}
}
internal string GetNextUrl()
{
lock (this.locker)
{
int urlCount = this.urls.Count;
int position = Interlocked.Increment(ref this.totalCallCount) % urlCount;
return this.urls[position];
}
}
}
}我无法构建您的线程池测试,因为您的for语句是不完整的,并且我不确定应该如何完成它们,所以我使用了这两个测试:
[TestFixture]
public class ABTestTest
{
[Test]
public void GetUrlWithTwoCandidates()
{
ABTest.RegisterUrl("CarrinhoPagamento", "Url1.aspx");
ABTest.RegisterUrl("CarrinhoPagamento", "Url2.aspx");
Assert.AreEqual("Url1.aspx", ABTest.GetUrl("CarrinhoPagamento"));
Assert.AreEqual("Url2.aspx", ABTest.GetUrl("CarrinhoPagamento"));
Assert.AreEqual("Url1.aspx", ABTest.GetUrl("CarrinhoPagamento"));
Assert.AreEqual("Url2.aspx", ABTest.GetUrl("CarrinhoPagamento"));
}
[Test]
public void GetUrlWithThreeCandidates()
{
ABTest.RegisterUrl("CarrinhoPagamento", "Url1.aspx");
ABTest.RegisterUrl("CarrinhoPagamento", "Url2.aspx");
ABTest.RegisterUrl("CarrinhoPagamento", "Url3.aspx");
Assert.AreEqual("Url1.aspx", ABTest.GetUrl("CarrinhoPagamento"));
Assert.AreEqual("Url2.aspx", ABTest.GetUrl("CarrinhoPagamento"));
Assert.AreEqual("Url3.aspx", ABTest.GetUrl("CarrinhoPagamento"));
Assert.AreEqual("Url1.aspx", ABTest.GetUrl("CarrinhoPagamento"));
Assert.AreEqual("Url2.aspx", ABTest.GetUrl("CarrinhoPagamento"));
Assert.AreEqual("Url3.aspx", ABTest.GetUrl("CarrinhoPagamento"));
}
[SetUp]
public void Setup()
{
ABTest.ResetAll();
}
}编辑:更新后的反馈。
锁很简单,而且速度很快,您可以通过使用ReaderWriterLockSlim获得更大的吞吐量,但这要复杂得多,除非您能够证明锁导致了很大的阻塞,否则我一开始会保留它。
https://codereview.stackexchange.com/questions/16211
复制相似问题