Эффективное использование оператора using (не в MSDN)

Я уже прочитал соответствующую Doc-Page, но на мой вопрос до сих пор нет ответа. Предположим, я хочу использовать disposable Object в цикле while, например:

StreamReader reader;
while (!ShouldStop)
{
    using (reader = new StreamReader(networkStream))
    {
         // Some code here...    
    }
}

Как видите, я объявляю StreamReade reader вне оператора using. Я обычно так делаю, потому что думаю, что тогда область памяти выделяется для этого StreamReader только один раз. И когда я использую оператор using следующим образом:

while (!ShouldStop)
{
    using (StreamReader reader = new StreamReader(networkStream))
    {
        // Some code here...            
    }
}

Я думаю, что существует непрерывное выделение памяти для StreamReader-объекта, и поэтому он гораздо более менее эффективен и производительен. Однако я не знаю, регулярно ли при первом использовании оператора using вызывается функция Dispose() экземпляра. Итак, первое использование оператора using совпадает со вторым использованием?


person Feve123    schedule 01.05.2015    source источник
comment
Вы проверяли, есть ли разница в эффективности и производительности? В обоих случаях у вас будет несколько распределений памяти, поэтому между двумя реализациями не должно быть измеримой разницы.   -  person Alex    schedule 01.05.2015
comment
Память выделяется оператором new, а не там, где определена переменная. Так что в обоих случаях у вас есть множественное выделение памяти.   -  person Mehrzad Chehraz    schedule 01.05.2015
comment
к спасибо. Я думал, что когда я объявляю объект, резервируется область памяти. И когда я объявляю + инициализирую объект, экземпляр всегда занимает другую область памяти (но, конечно, тот же размер/количество байтов)   -  person Feve123    schedule 01.05.2015


Ответы (5)


Я обычно делаю это, потому что думаю, что тогда область памяти выделяется для этого StreamReader только один раз.

Это то, что вы получаете неправильно.

Определенный объем стека занимает локальная переменная на время ее использования, а также определенный объем кучи занимает объект после new. Это не изменится.

Действительно, компилятор в конечном итоге занимает немного больше места в стеке с вашим подходом. Просто сравните IL двух методов, использующих каждый подход. Мы будем использовать этот С#:

private static string LastLine1(NetworkStream networkStream)
{
    string last = null;
    StreamReader reader;
    while(!ShouldStop)
    {
        using(reader = new StreamReader(networkStream))
        {
            string line = reader.ReadLine();
            if(line != null)
                last = line;
        }
    }
    return last;
}
private static string LastLine2(NetworkStream networkStream)
{
    string last = null;
    while(!ShouldStop)
    {
        using(StreamReader reader = new StreamReader(networkStream))
        {
            string line = reader.ReadLine();
            if(line != null)
                last = line;
        }
    }
    return last;
}

И мы получаем этот CIL:

.method private hidebysig static 
    string LastLine1 (
        class [System]System.Net.Sockets.NetworkStream networkStream
    ) cil managed 
{
    .maxstack 2
    .locals init (
        [0] string,
        [1] class [mscorlib]System.IO.StreamReader,
        [2] string,
        [3] class [mscorlib]System.IO.StreamReader
    )

    IL_0000: ldnull
    IL_0001: stloc.0
    IL_0002: br.s IL_0025
    IL_0004: ldarg.0
    IL_0005: newobj instance void [mscorlib]System.IO.StreamReader::.ctor(class [mscorlib]System.IO.Stream)
    IL_000a: dup
    IL_000b: stloc.1
    IL_000c: stloc.3
    .try
    {
        IL_000d: ldloc.1
        IL_000e: callvirt instance string [mscorlib]System.IO.TextReader::ReadLine()
        IL_0013: stloc.2
        IL_0014: ldloc.2
        IL_0015: brfalse.s IL_0019

        IL_0017: ldloc.2
        IL_0018: stloc.0

        IL_0019: leave.s IL_0025
    }
    finally
    {
        IL_001b: ldloc.3
        IL_001c: brfalse.s IL_0024

        IL_001e: ldloc.3
        IL_001f: callvirt instance void [mscorlib]System.IDisposable::Dispose()

        IL_0024: endfinally
    }
    IL_0025: call bool Demonstrate.Program::get_ShouldStop()
    IL_002a: brfalse.s IL_0004

    IL_002c: ldloc.0
    IL_002d: ret
}

.method private hidebysig static 
    string LastLine2 (
        class [System]System.Net.Sockets.NetworkStream networkStream
    ) cil managed 
{
    .maxstack 1
    .locals init (
        [0] string,
        [1] class [mscorlib]System.IO.StreamReader,
        [2] string
    )

    IL_0000: ldnull
    IL_0001: stloc.0
    IL_0002: br.s IL_0023
    IL_0004: ldarg.0
    IL_0005: newobj instance void [mscorlib]System.IO.StreamReader::.ctor(class [mscorlib]System.IO.Stream)
    IL_000a: stloc.1
    .try
    {
        IL_000b: ldloc.1
        IL_000c: callvirt instance string [mscorlib]System.IO.TextReader::ReadLine()
        IL_0011: stloc.2
        IL_0012: ldloc.2
        IL_0013: brfalse.s IL_0017

        IL_0015: ldloc.2
        IL_0016: stloc.0

        IL_0017: leave.s IL_0023
    }
    finally
    {
        IL_0019: ldloc.1
        IL_001a: brfalse.s IL_0022

        IL_001c: ldloc.1
        IL_001d: callvirt instance void [mscorlib]System.IDisposable::Dispose()

        IL_0022: endfinally
    }

    IL_0023: call bool Demonstrate.Program::get_ShouldStop()
    IL_0028: brfalse.s IL_0004

    IL_002a: ldloc.0
    IL_002b: ret
}

(Строго говоря, эти два действительно должны были привести к идентичному коду, но на самом деле это не так, и ваш подход заключался в немного более длинном и немного большем использовании пространства стека).

Поскольку компилятор C# не смог оптимизировать использование reader за пределами использования, на самом деле именно ваш подход приводит к тому, что дополнительное пространство стека занимает другая копия reader.

Если вы не знакомы с CIL, сравните, как ILSpy пытается снова декомпилировать их обратно в C#:

private static string LastLine1(NetworkStream networkStream)
{
    string result = null;
    while (!Program.ShouldStop)
    {
        StreamReader streamReader2;
        StreamReader streamReader = streamReader2 = new StreamReader(networkStream);
        try
        {
            string text = streamReader.ReadLine();
            if (text != null)
            {
                result = text;
            }
        }
        finally
        {
            if (streamReader2 != null)
            {
                ((IDisposable)streamReader2).Dispose();
            }
        }
    }
    return result;
}

private static string LastLine2(NetworkStream networkStream)
{
    string result = null;
    while (!Program.ShouldStop)
    {
        using (StreamReader streamReader = new StreamReader(networkStream))
        {
            string text = streamReader.ReadLine();
            if (text != null)
            {
                result = text;
            }
        }
    }
    return result;
}

(Возможно также, что вы уменьшили вероятность того, что проверка на нулевое значение будет оптимизирована, когда она затем будет преобразована в машинный код, который фактически запускается).

это гораздо менее эффективно и производительно. Однако я не знаю, регулярно ли первое использование оператора using вызывает функцию Dispose() экземпляра.

using вызывает Dispose() нормально в любом случае, однако вы немного более расточительны и, следовательно, немного менее эффективны и производительны. Вероятно, незначительно, но подход, которого вы избегаете, определенно не «гораздо менее эффективен и эффективен», как вы утверждаете.


В общем, держите свои прицелы в напряжении. Основная причина заключается в том, что переменная, которая больше не находится в области видимости, — это переменная, с которой вы больше не можете сделать что-то не так или даже не должны думать о ней, поэтому у вас будет более чистый код с меньшим количеством ошибок и там, где ошибки будут легче найти. Вторая причина заключается в том, что в некоторых случаях, подобных этому, более широкая область действия приводит к чуть более расточительному коду.

Теперь размещение назначений вне цикла действительно может быть более эффективным. Если ваш код может работать с:

using(var reader = new StreamReader(networkStream))
  while(!ShouldStop)
  {
    // do stuff
  }

Тогда это уменьшит отток кучи и, прежде всего, делает меньше, и, следовательно, если это сработает, это будет улучшением.

Объявления, однако, ничего не делают, поэтому их наличие вне циклов не помогает, а иногда и немного мешает.

person Jon Hanna    schedule 01.05.2015

Вы не экономите память, делая:

StreamReader reader;
while (!ShouldStop)
{
    using (reader = new StreamReader(networkStream))
    {
         // Some code here...    
    }
}

Вы объявили переменную вне цикла, но по-прежнему создаете новый объект на каждой итерации.

person kemiller2002    schedule 01.05.2015
comment
Пространство и область объявления не имеют ничего общего со сборкой мусора. Объекты, находящиеся в области действия в исходном коде, могут быть собраны, если ссылка в области действия больше не используется. Если бы это было не так, нам бы не понадобился GC.KeepAlive() специально для предотвращения сбора чего-либо, пока он все еще находится в области видимости. См. первые несколько абзацев этого ответа о том, когда может произойти GC. - person Jon Hanna; 01.05.2015
comment
В некотором смысле часть смысла автоматического сбора данных заключается в том, что мы можем забыть об этом… до тех пор, пока не станет важно помнить! - person Jon Hanna; 01.05.2015

это не имеет значения между этими двумя, потому что всякий раз, когда вы вызываете new, часть памяти рассматривается для выделения. оператор using является синтаксическим сахаром для этого (я имею в виду, что оператор using переводится в это):

        StreamReader reader = new StreamReader(networkStream)

        try
        {
            //some codes here
        }
        finally
        {
            if (reader!=null)
               ( (IDisposable) reader).Dispose();
        }

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

person Technovation    schedule 01.05.2015

Оба сценария будут иметь одинаковый эффект с точки зрения расположения экземпляра. На каждой итерации внутри цикла каждый раз будет создаваться и удаляться объект.

Чтобы лучше понять, пожалуйста, взгляните на следующий код

class Program
{
    static void Main(string[] args)
    {
        Disposableclass obj; 

        for (int i = 0; i < 3; i++)
        {
            using (obj = new Disposableclass())
            { 
            }
        }

        /*for (int i = 0; i < 3; i++)
        {
            using (Disposableclass obj2 = new Disposableclass())
            { 
            }
        }*/

        Console.ReadKey();
    }

}

public class Disposableclass : IDisposable
{
    public Disposableclass()
    {
        Console.WriteLine("Constructor called: " + this.GetType().ToString());
    }
    public void Dispose()
    {
        Console.WriteLine("Dispose called: " + this.GetType().ToString());     

   }
}

В обоих сценариях у нас будет одинаковый вывод, который выглядит следующим образом

Конструктор называется: TestSolution.Disposableclass

Распоряжаться называется: TestSolution.Disposableclass

Конструктор называется: TestSolution.Disposableclass

Распоряжаться называется: TestSolution.Disposableclass

Конструктор называется: TestSolution.Disposableclass

Распоряжаться называется: TestSolution.Disposableclass

person Haseeb Asif    schedule 01.05.2015

все предыдущие ответы о распределении памяти верны.

Что вы делаете, когда объявляете переменную «читатель» вне цикла: вы резервируете память, но только для ссылки, по сути указателя, а не для самого объекта; сам объект выделяется в куче с помощью оператора new() и удаляется, когда Dispose() вызывается в конце используемого блока. Следующий new() выделяет новый объект.

Опасность заключается в следующем: если вы воспользуетесь ссылкой на объект для чтения после блока «using», вы получите исключение ObjectDisposedException.

В вашем контексте не имеет смысла постоянно удалять и воссоздавать объект читателя. Вероятно, вам следует поместить оператор using вне цикла while.

Но это приводит к более широкому вопросу, чего вы вообще пытаетесь достичь. Почему вы проверяете ShouldStop? Вы хотите, чтобы цикл чтения можно было отменить? По другой ветке? Убедитесь, что межпотоковое взаимодействие реализовано правильно, и вы не находитесь в заблокированном состоянии, ожидая чтения данных, когда кто-то хочет вас отменить. С другой стороны, вы не хотите запускать цикл слишком часто, иначе вы создадите ненужную нагрузку.

person user4854209    schedule 01.05.2015
comment
Объявления не резервируют память, они резервируют имя. Соответствующее использование памяти может не быть однозначным; после компиляции он может быть полностью оптимизирован, он может стать более чем одним слотом в локальных переменных метода на уровне IL, или слоты могут быть повторно использованы, а затем при джиттинге то же самое применяется снова. - person Jon Hanna; 01.05.2015