C# 如何在查询完成之前按顺序开始处理并行查询的结果?

C# 如何在查询完成之前按顺序开始处理并行查询的结果?,c#,.net,.net-4.0,plinq,C#,.net,.net 4.0,Plinq,我有一个字符串集合,需要对其执行两个操作 其中第一个可以安全地以任何顺序(yay)独立处理,但随后必须以原始顺序顺序(boo)处理输出 下面的Plinq让我了解了大部分情况: myStrings.AsParallel().AsOrdered() .Select( str => Operation1(str) ) .AsSequential() .Select( str => Operation2(str) ); //immagin

我有一个字符串集合,需要对其执行两个操作

其中第一个可以安全地以任何顺序(yay)独立处理,但随后必须以原始顺序顺序(boo)处理输出

下面的Plinq让我了解了大部分情况:

myStrings.AsParallel().AsOrdered()
         .Select( str => Operation1(str) )
         .AsSequential()
         .Select( str => Operation2(str) );
//immagine Operation2() maintains some sort of state and must take the outputs from Operation1 in the original order    
这在很大程度上让我明白了这一点,但问题是,由于AsOrdered(),首先对每个字符串执行操作1,然后将结果元素排序回其原始顺序,最后开始执行操作2

理想情况下,只要Operation1调用返回第一个字符串(即myStrings[0],而不是返回的第一个字符串),我就希望Operation2开始它的工作

因此,这是我试图从总体上解决这个问题的尝试:

public static class ParallelHelper
{
    public static IEnumerable<U> SelectAsOrdered<T, U>(this ParallelQuery<T> query, Func<T, U> func)
    {
        var completedTasks = new Dictionary<int, U>();
        var queryWithIndexes = query.Select((x, y) => new { Input = x, Index = y })
                                    .AsParallel()
                                    .Select(t => new { Value = func(t.Input), Index = t.Index })
                                    .WithMergeOptions(ParallelMergeOptions.NotBuffered);

        int i = 0;
        foreach (var task in queryWithIndexes)
        {
            if (i==task.Index)
            {
                Console.WriteLine("immediately yielding task: {0}", i);
                i++;
                yield return task.Value;

                U previouslyCompletedTask;
                while (completedTasks.TryGetValue(i, out previouslyCompletedTask))
                {
                    completedTasks.Remove(i);
                    Console.WriteLine("delayed yielding task: {0}", i);
                    yield return previouslyCompletedTask;
                    i++;
                }
            }
            else
            {
                completedTasks.Add(task.Index, task.Value);
            }
        }
        yield break;
    }
}
一旦myStrings[0]从操作1中出来,操作2就会开始

我想知道的是:

  • 这是并行化中一个相当常见的问题/模式,我是否错过了在.Net框架中实现这一点的现成方法?还是有更简单的方法
  • 虽然上面的扩展方法似乎可以做到这一点,但如何改进呢?代码中有什么东西看起来是个坏主意吗
  • 谢谢

    安迪

    以防您感兴趣:

    • 没有对.WithMergeOptions(ParallelMergeOptions.NotBuffered)的调用,Operation2直到所有Operation1调用都已启动才开始工作(这比等待它们全部完成的原始代码要好)

    • 现实生活中的问题:
      Operation1正在大量文本中搜索法律引文和参考文献(例如:“1989年儿童法案”)。
      这些引用通常是独立的,但有时抄本会包含类似“上述法案第6节”的内容。 Operation2依赖于Operation1中的捕获来获取这些部分引用


    如果需要速度,可以并行化所有流程(加载数据、准备数据、流程数据和聚合数据),我认为最好使用生产者/消费者模式

    但是,如果您要使用“Linq”,则无法以并行方式生成(以一种很好的方式来执行完整的并行工作流)数据(但是是的:准备、处理和恢复)

    另一方面,我认为尝试使用“Linq”作为“并行(A)+顺序(B)”是错误的(你可以,是的),你的过程(我认为)是错误的

    那么,B必须等待A

    为什么不简单地“并行(A/B)”

    你可以做一个助手(扩展),但我认为它通常没有用处

    在实际情况中,只需使用
    信号灯
    ,即可防止过早访问“文章ID”

    并行准备、处理和恢复(无生成)的完整代码为:

    类文本{
    公共静态正则表达式rx=新正则表达式(@“(PREVID | ACTID\=([0-9]+)”);
    私有文本prv;//上一篇文章
    私有字符串ot;//原始文本
    private int id;//对文本执行操作id
    私有信号量isComputed=新信号量(0,1);
    公共国际活动{
    得到{
    isComputed.WaitOne();
    int _id=id;
    isComputed.Release();
    返回_id;
    }
    }
    公共bool ProcessText(){
    var mx=接收匹配(ot);
    var prev=mx.Groups[1]。值==“PREVID”;
    如果(上一个)
    id=prv==null?0:prv.ActID;
    其他的
    如果(!int.TryParse(mx.Groups[2].值,out id))
    抛出新异常(string.Format(@“不正确的项目id”{0}“”,mx.Groups[0].Value));
    isComputed.Release();
    返回!上一个;
    }
    公共文本(字符串原始文本,文本前一个){
    prv=以前的;
    ot=原始文本;
    }
    }
    公共静态void Main(字符串[]args){
    //相同的随机流(用于调试)
    var rnd=新的随机变量(1);
    var noise=@“这些引用通常是独立的,但偶尔会出现”;
    //一些杂音文字
    变量位=新函数(()=>
    noise.Substring(0,rnd.Next(noise.Length));
    //随机文章
    变量文本=新函数(()=>
    string.Format(@“{0}{1}{2}”,bit(),
    rnd.Next()%2==0?“上一个ID”
    :string.Format(@“ACTID={0}”、rnd.Next()、bit());
    //随机数据输入
    var data=新列表();
    文本prv=null;
    对于(变量n=0;n<1000000;n++)
    //生产者/消费者最好并行化加载数据步骤
    data.Add(prv=新文本(Text(),prv));
    控制台。写入(“按键启动…”);
    Console.ReadKey();
    //并行处理
    Console.WriteLine(“{0}唯一ID”),data.AsParallel().Where(n=>n.ProcessText()).Count());
    Console.WriteLine(“进程已完成”);
    }
    
    如您所见,
    ProcessText
    并行处理所有文章。只有PREVID文章会等到他们的前一篇文章计算他们自己的id

    抽象这种行为(我认为)的真正问题是项关系(一项依赖于另一项),在Linq中,自然的方式是“无项关系”(必须使用“分组方式”来执行)


    我建议您使用生产者/消费者模式。

    不仅仅是使用
    并行合并选项。NotBuffered
    (没有字典)提供您想要的吗?似乎没有(除非我做得不正确)——只是尝试MyStrings.AsParallel().AsOrdered()。使用合并选项(ParallelMergeOptions.NotBuffered)。选择(…)在并行查询完成之前,似乎仍会阻塞。Operation2(…)的初始语句中的“AsSequential()”是什么?Operation2(…)需要按顺序一次传递所有字符串,因此AsSequential()用于防止第二个Select并行执行(否则它会这样做)。确定。让我问以下问题:据我所知,经过运算1和运算2的字符串数是相同的。所以您正在将初始序列拆分为proc
    myStrings.AsParallel()
             .SelectAsOrdered( str => Operation1(str) )
             .Select(str => Operation2(str));
    
    B = f(A)
    
    class Text {
        public static Regex rx = new Regex(@" (PREVID|ACTID\=([0-9]+)) ");
    
        private Text prv; // previous article
        private string ot; // original text
        private int id; // act id on text
        private Semaphore isComputed = new Semaphore(0, 1);
    
        public int ActID {
            get {
                isComputed.WaitOne();
                int _id = id;
                isComputed.Release();
                return _id;
            }
        }
    
        public bool ProcessText() {
            var mx = rx.Match(ot);
            var prev = mx.Groups [1].Value == "PREVID";
            if(prev)
                id = prv == null ? 0 : prv.ActID;
            else
                if(!int.TryParse(mx.Groups [2].Value, out id))
                    throw new Exception(string.Format(@"Incorrect article id ""{0}""", mx.Groups [0].Value));
            isComputed.Release();
            return !prev;
        }
    
        public Text(string original_text, Text previous) {
            prv = previous;
            ot = original_text;
        }
    
    }
    
    public static void Main(String [] args) {
    
        // same random stream (for debugging)
        var rnd = new Random(1);
    
        var noise = @"These references are usually independent, but occasionally";
    
        // some noise text
        var bit = new Func<string>(() =>
            noise.Substring(0, rnd.Next(noise.Length)));
    
        // random article
        var text = new Func<string>(() =>
            string.Format(@"{0}{1}{2}", bit(),
                rnd.Next() % 2 == 0 ? " PREVID "
                                    : string.Format(@" ACTID={0} ", rnd.Next()), bit()));
    
        // random data input
        var data = new List<Text>();
        Text prv = null;
        for(var n = 0; n < 1000000; n++)
            // producer / consumer is better to parallelize load data step
            data.Add(prv = new Text(text(), prv));
    
        Console.Write("Press key to start...");
        Console.ReadKey();
    
        // parallel processing
        Console.WriteLine("{0} unique ID's", data.AsParallel().Where(n => n.ProcessText()).Count());
    
        Console.WriteLine("Process completed.");
    }