使用Parallel Linq Extensions结合两个序列,如何才能首先产生最快的结果?

时间:2011-11-09 13:25:48

标签: c# .net parallel-processing plinq parallel-extensions

假设我有两个序列返回整数1到5。

第一个返回1,2和3非常快,但4和5每个返回200毫秒。

public static IEnumerable<int> FastFirst()
{
    for (int i = 1; i < 6; i++)
    {
        if (i > 3) Thread.Sleep(200);
        yield return i;
    }
}

第二个返回1,2和3,延迟时间为200ms,但快速返回4和5。

public static IEnumerable<int> SlowFirst()
{
    for (int i = 1; i < 6; i++)
    {
        if (i < 4) Thread.Sleep(200);
        yield return i;
    }
}

联合这两个序列只给出数字1到5。

FastFirst().Union(SlowFirst());

我不能保证两种方法中的哪一种在什么时候有延迟,所以执行的顺序不能保证为我提供解决方案。因此,我想将联盟并行化,以便最小化我的例子中的(人为的)延迟。

真实场景:我有一个缓存,它返回一些实体,以及一个返回所有实体的数据源。我希望能够从一个内部并行处理请求的方法返回迭代器到缓存和数据源,以便缓存的结果尽可能快地生成。

注1:我意识到这仍然在浪费CPU周期;我不是在问我怎样才能防止序列迭代它们的慢元素,以及如何尽可能快地将它们结合起来。

更新1:我已经定制了achitaka-san对接受多个生产者的好反应,并使用ContinueWhenAll将BlockingCollection的CompleteAdding设置为一次。我只是把它放在这里,因为它会因缺少注释格式而丢失。任何进一步的反馈都会很棒!

public static IEnumerable<TResult> SelectAsync<TResult>(
    params IEnumerable<TResult>[] producer)
{
    var resultsQueue = new BlockingCollection<TResult>();

    var taskList = new HashSet<Task>();
    foreach (var result in producer)
    {
        taskList.Add(
            Task.Factory.StartNew(
                () =>
                    {
                        foreach (var product in result)
                        {
                            resultsQueue.Add(product);
                        }
                    }));
    }

    Task.Factory.ContinueWhenAll(taskList.ToArray(), x => resultsQueue.CompleteAdding());

    return resultsQueue.GetConsumingEnumerable();
}

2 个答案:

答案 0 :(得分:3)

看看这个。 第一种方法只是返回结果的顺序。 第二个检查唯一性。如果你把它们连在一起,你就会得到你想要的结果。

public static class Class1
{
    public static IEnumerable<TResult> SelectAsync<TResult>(
        IEnumerable<TResult> producer1,
        IEnumerable<TResult> producer2,
        int capacity)
    {
        var resultsQueue = new BlockingCollection<TResult>(capacity);
        var producer1Done = false;
        var producer2Done = false;

        Task.Factory.StartNew(() =>
        {
            foreach (var product in producer1)
            {
                resultsQueue.Add(product);
            }
            producer1Done = true;
            if (producer1Done && producer2Done) { resultsQueue.CompleteAdding(); }
        });

        Task.Factory.StartNew(() =>
        {
            foreach (var product in producer2)
            {
                resultsQueue.Add(product);
            }
            producer2Done = true;
            if (producer1Done && producer2Done) { resultsQueue.CompleteAdding(); }
        });

        return resultsQueue.GetConsumingEnumerable();
    }


    public static IEnumerable<TResult> SelectAsyncUnique<TResult>(this IEnumerable<TResult> source)
    {
        HashSet<TResult> knownResults = new HashSet<TResult>();
        foreach (TResult result in source)
        {
            if (knownResults.Contains(result)) {continue;}
            knownResults.Add(result);
            yield return result;
        }
    }
}

答案 1 :(得分:0)

与从数据库中提取相比,缓存几乎是即时的,因此您可以先从缓存中读取并返回这些项目,然后从数据库中读取并返回除缓存中找到的项目以外的项目。

如果你尝试将其并行化,你会增加很多复杂性,但收益会很小。

编辑:

如果源的速度没有可预测的差异,您可以在线程中运行它们并使用同步的哈希集来跟踪您已经获得的项目,将新项目放入队列中,然后让主线程从队列中读取:

public static IEnumerable<TItem> GetParallel<TItem, TKey>(Func<TItem, TKey> getKey, params IEnumerable<TItem>[] sources) {
  HashSet<TKey> found = new HashSet<TKey>();
  List<TItem> queue = new List<TItem>();
  object sync = new object();
  int alive = 0;
  object aliveSync = new object();
  foreach (IEnumerable<TItem> source in sources) {
    lock (aliveSync) {
      alive++;
    }
    new Thread(s => {
      foreach (TItem item in s as IEnumerable<TItem>) {
        TKey key = getKey(item);
        lock (sync) {
          if (found.Add(key)) {
            queue.Add(item);
          }
        }
      }
      lock (aliveSync) {
        alive--;
      }
    }).Start(source);
  }
  while (true) {
    lock (sync) {
      if (queue.Count > 0) {
        foreach (TItem item in queue) {
          yield return item;
        }
        queue.Clear();
      }
    }
    lock (aliveSync) {
      if (alive == 0) break;
    }
    Thread.Sleep(100);
  }
}

测试流程:

public static IEnumerable<int> SlowRandomFeed(Random rnd) {
  int[] values = new int[100];
  for (int i = 0; i < 100; i++) {
    int pos = rnd.Next(i + 1);
    values[i] = i;
    int temp = values[pos];
    values[pos] = values[i];
    values[i] = temp;
  }
  foreach (int value in values) {
    yield return value;
    Thread.Sleep(rnd.Next(200));
  }
}

测试:

Random rnd = new Random();
foreach (int item in GetParallel(n => n, SlowRandomFeed(rnd), SlowRandomFeed(rnd), SlowRandomFeed(rnd), SlowRandomFeed(rnd))) {
  Console.Write("{0:0000 }", item);
}