web-dev-qa-db-ja.com

Parallel.Foreach +イールドリターン?

このような並列ループを使用して何かを処理したい:

_public void FillLogs(IEnumerable<IComputer> computers)
{
    Parallel.ForEach(computers, cpt=>
    {
        cpt.Logs = cpt.GetRawLogs().ToList();
    });

}
_

わかりました、それはうまくいきます。しかし、FillLogsメソッドがIEnumerableを返すようにするにはどうすればよいですか?

_public IEnumerable<IComputer> FillLogs(IEnumerable<IComputer> computers)
{
    Parallel.ForEach(computers, cpt=>
    {
        cpt.Logs = cpt.GetRawLogs().ToList();
        yield return cpt // KO, don't work
    });

}
_

[〜#〜]編集[〜#〜]

それは不可能のようです...しかし私はこのようなものを使用します:

_public IEnumerable<IComputer> FillLogs(IEnumerable<IComputer> computers)
{
    return computers.AsParallel().Select(cpt => cpt);
}
_

しかし、私がcpt.Logs = cpt.GetRawLogs().ToList();命令を置いたところ

27
Florian

短いバージョン-いいえ、それはイテレータブロックを介しては不可能です。長いバージョンでは、呼び出し元のイテレータスレッド(デキューを実行)と並列ワーカー(エンキューを実行)の間で同期されたキュー/デキューが含まれる可能性があります。ただし、補足として、ログは通常IOバウンドであり、IOバウンドであるものを並列化してもうまく機能しないことがよくあります。

呼び出し元がそれぞれconsumeに時間がかかる場合は、一度に1つのログのみを処理するアプローチにはメリットがあるかもしれませんが、それを行うことができますwhile呼び出し元は前のログを消費しています。つまり、開始 a Task次のアイテムの場合yieldの前で、yieldの後で完了を待ちます。 ..しかし、それもまた、かなり複雑です。簡単な例として:

static void Main()
{
    foreach(string s in Get())
    {
        Console.WriteLine(s);
    }
}

static IEnumerable<string> Get() {
    var source = new[] {1, 2, 3, 4, 5};
    Task<string> outstandingItem = null;
    Func<object, string> transform = x => ProcessItem((int) x);
    foreach(var item in source)
    {
        var tmp = outstandingItem;

        // note: passed in as "state", not captured, so not a foreach/capture bug
        outstandingItem = new Task<string>(transform, item);
        outstandingItem.Start();

        if (tmp != null) yield return tmp.Result;
    }
    if (outstandingItem != null) yield return outstandingItem.Result;
}
static string ProcessItem(int i)
{
    return i.ToString();
}
13
Marc Gravell

攻撃的になりたくないのですが、理解不足かもしれません。 Parallel.ForEachは、TPLが複数のスレッドで使用可能なハードウェアに従ってforeachを実行することを意味します。しかし、それはiiがその作業を並行して行うことが可能であることを意味します! yield returnは、リスト(またはその他)からいくつかの値を取得し、必要に応じてそれらを1つずつ返す機会を提供します。これにより、最初に条件に一致するすべてのアイテムを見つけてから、それらを繰り返す必要がなくなります。これは確かにパフォーマンス上の利点ですが、並行して実行することはできません。

3
Fischermaen

次の拡張方法を使用できます

public static class ParallelExtensions
{
    public static IEnumerable<T1> OrderedParallel<T, T1>(this IEnumerable<T> list, Func<T, T1> action)
    {
        var unorderedResult = new ConcurrentBag<(long, T1)>();
        Parallel.ForEach(list, (o, state, i) =>
        {
            unorderedResult.Add((i, action.Invoke(o)));
        });
        var ordered = unorderedResult.OrderBy(o => o.Item1);
        return ordered.Select(o => o.Item2);
    }
}

次のように使用します。

public void FillLogs(IEnumerable<IComputer> computers)
{
    cpt.Logs = computers.OrderedParallel(o => o.GetRawLogs()).ToList();
}

これで時間を節約できることを願っています。

0

質問は古いですが、私はただ楽しみのために何かをすることができました。

class Program
{
    static void Main(string[] args)
    {
        foreach (var message in GetMessages())
        {
            Console.WriteLine(message);
        }
    }


    // Parallel yield
    private static IEnumerable<string> GetMessages()
    {
        int total = 0;
        bool completed = false;
        var batches = Enumerable.Range(1, 100).Select(i => new Computer() { Id = i });
        var qu = new ConcurrentQueue<Computer>();
        Task.Run(() =>
        {
            try
            {
                Parallel.ForEach(batches,
                    () => 0,
                    (item, loop, subtotal) =>
                    {
                        Thread.Sleep(1000);
                        qu.Enqueue(item);
                        return subtotal + 1;
                    },
                    result => Interlocked.Add(ref total, result));
            }
            finally
            {
                completed = true;
            }
        });

        int current = 0;
        while (current < total || !completed)
        {
            SpinWait.SpinUntil(() => current < total || completed);
            if (current == total) yield break;
            current++;
            qu.TryDequeue(out Computer computer);
            yield return $"Completed {computer.Id}";
        }
    }
}

public class Computer
{
    public int Id { get; set; }
}

Korayの回答と比較すると、これは実際にはすべてのCPUコアを使用しています。

0
CSharpBender

どうですか

            Queue<string> qu = new Queue<string>();
            bool finished = false;
            Task.Factory.StartNew(() =>
            {
                Parallel.ForEach(get_list(), (item) =>
                {
                    string itemToReturn = heavyWorkOnItem(item);         
                    lock (qu)
                       qu.Enqueue(itemToReturn );                        
                });
                finished = true;
            });

            while (!finished)
            {
                lock (qu)
                    while (qu.Count > 0)
                        yield return qu.Dequeue();
                //maybe a thread sleep here?
            }

編集:これが良いと思います:

        public static IEnumerable<TOutput> ParallelYieldReturn<TSource, TOutput>(this IEnumerable<TSource> source, Func<TSource, TOutput> func)
        {
            ConcurrentQueue<TOutput> qu = new ConcurrentQueue<TOutput>();
            bool finished = false;
            AutoResetEvent re = new AutoResetEvent(false);
            Task.Factory.StartNew(() =>
            {
                Parallel.ForEach(source, (item) =>
                {
                    qu.Enqueue(func(item));
                    re.Set();
                });
                finished = true;
                re.Set();
            });

            while (!finished)
            {
                re.WaitOne();
                while (qu.Count > 0)
                {
                    TOutput res;
                    if (qu.TryDequeue(out res))
                        yield return res;
                }
            }
        }   

Edit2:短いいいえの答えに同意します。このコードは役に立ちません。歩留まりループを壊すことはできません。

0
Koray