C #: Как я могу сделать поток IEnumerable <T>безопасным? - PullRequest
39 голосов
/ 22 октября 2009

Скажите, у меня есть этот простой метод:

public IEnumerable<uint> GetNumbers()
{
    uint n = 0;
    while(n < 100)
        yield return n++;
}

Как бы вы сделали эту тему безопасной? Под этим я подразумеваю, что вы получите этот перечислитель один раз, и несколько потоков будут обрабатывать все числа без дубликатов.

Я полагаю, что где-то должна использоваться блокировка, но где должна быть эта блокировка, чтобы блок итератора был потокобезопасным? Что, в общем, нужно помнить, если вы хотите потокобезопасный IEnumerable<T>? Или, скорее, я думаю, что это будет потокобезопасным IEnumerator<T> ...?

Ответы [ 6 ]

40 голосов
/ 22 октября 2009

В этом есть внутренняя проблема, потому что IEnumerator<T> имеет как MoveNext(), так и Current. Вы действительно хотите один звонок, такой как:

bool TryMoveNext(out T value)

в этот момент вы можете атомно перейти к следующему элементу и получить значение. Реализовать это и все еще иметь возможность использовать yield может быть сложно ... Я подумаю об этом, хотя. Я думаю, что вам нужно было бы обернуть «не поточный» итератор в потокобезопасный, который атомарно выполнял MoveNext() и Current для реализации интерфейса, показанного выше. Я не знаю, как бы вы обернули этот интерфейс обратно в IEnumerator<T>, чтобы вы могли использовать его в foreach, хотя ...

Если вы используете .NET 4.0, параллельные расширения могут помочь вам - вам нужно будет объяснить больше о том, что вы пытаетесь сделать.

Это интересная тема - возможно, мне придется написать об этом в блоге ...

РЕДАКТИРОВАТЬ: я сейчас написал об этом в блоге с двумя подходами.

2 голосов
/ 20 июня 2012

Я только что проверил этот бит кода:

static IEnumerable<int> getNums()
{
    Console.WriteLine("IENUM - ENTER");

    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(i);
        yield return i;
    }

    Console.WriteLine("IENUM - EXIT");
}

static IEnumerable<int> getNums2()
{
    try
    {
        Console.WriteLine("IENUM - ENTER");

        for (int i = 0; i < 10; i++)
        {
            Console.WriteLine(i);
            yield return i;
        }
    }
    finally
    {
        Console.WriteLine("IENUM - EXIT");
    }
}

getNums2 () всегда вызывает последнюю часть кода. Если вы хотите, чтобы ваш IEnumerable был потокобезопасным, добавьте все блокировки потоков, которые вы хотите, вместо писем, вяните с помощью ReaderWriterSlimLock, Semaphore, Monitor и т. Д.

0 голосов
/ 22 октября 2009

Я думал, что вы не можете сделать ключевое слово yield поточно-безопасным, если только вы не сделаете так, чтобы оно зависело от уже поточно-безопасного источника значений:

public interface IThreadSafeEnumerator<T>
{
    void Reset();
    bool TryMoveNext(out T value);
}

public class ThreadSafeUIntEnumerator : IThreadSafeEnumerator<uint>, IEnumerable<uint>
{
    readonly object sync = new object();

    uint n;

    #region IThreadSafeEnumerator<uint> Members
    public void Reset()
    {
        lock (sync)
        {
            n = 0;
        }
    }

    public bool TryMoveNext(out uint value)
    {
        bool success = false;

        lock (sync)
        {
            if (n < 100)
            {
                value = n++;
                success = true;
            }
            else
            {
                value = uint.MaxValue;
            }
        }

        return success;
    }
    #endregion
    #region IEnumerable<uint> Members
    public IEnumerator<uint> GetEnumerator()
    {
        //Reset(); // depends on what behaviour you want
        uint value;
        while (TryMoveNext(out value))
        {
            yield return value;
        }
    }
    #endregion
    #region IEnumerable Members
    System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
    {
        //Reset(); // depends on what behaviour you want
        uint value;
        while (TryMoveNext(out value))
        {
            yield return value;
        }
    }
    #endregion
}

Вам необходимо решить, следует ли при каждом типичном инициировании перечислителя сбрасывать последовательность или код клиента должен это делать.

0 голосов
/ 22 октября 2009

Ну, я не уверен, но, может быть, с некоторыми блокировками в звонилке?

Проект:

Monitor.Enter(syncRoot);
foreach (var item in enumerable)
{
  Monitor.Exit(syncRoot);
  //Do something with item
  Monitor.Enter(syncRoot);
}
Monitor.Exit(syncRoot);
0 голосов
/ 22 октября 2009

Вы можете просто возвращать полную последовательность каждый раз вместо использования yield:

return Enumerable.Range(0, 100).Cast<uint>().ToArray();

0 голосов
/ 22 октября 2009

Полагаю, вам нужен Enumerator с сохранением потоков, поэтому вам, вероятно, следует реализовать его.

...