Анонимный делегат не использует новый local для каждой итерации, когда данные на локальном stackalloc - PullRequest
0 голосов
/ 29 января 2019

При использовании анонимных delegate s в C # CLR сгенерирует копию локальной (например, переменные в текущей области) в куче для используемых переменных.Такой локальный объект будет помещен в кучу для каждой объявленной переменной текущей области.

Вы можете увидеть это поведение в этом примере:

class Program
{
    static void Main(string[] args)
    {
        for (int i = 0; i < 5; i++)
            ThreadPool.QueueUserWorkItem(delegate { execute(i); });

        Thread.Sleep(1000);

        Console.WriteLine();

        for (int i = 0; i < 5; i++)
        {
            int j = i;

            ThreadPool.QueueUserWorkItem(delegate { execute(j); });
        }

        Thread.Sleep(1000);
    }

    static void execute(int number)
    {
        Console.WriteLine(" * NUM=" + number.ToString());
    }
}

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

 * NUM=5
 * NUM=5
 * NUM=5
 * NUM=5
 * NUM=5

 * NUM=0
 * NUM=1
 * NUM=2
 * NUM=3
 * NUM=4

C # всегда должен генерировать новую локальную копию при вызове в методе.Это работает, как предполагалось в этом примере:

class Program
{
    static void Main(string[] args)
    {
        for (int i = 0; i < 5; i++)
            call(i);

        Thread.Sleep(1000);
    }

    static void call(int number)
    {
        ThreadPool.QueueUserWorkItem(delegate { execute(number); });
    }

    static void execute(int number)
    {
        Console.WriteLine(" * NUM=" + number.ToString());
    }
}

Вывод:

 * NUM=0
 * NUM=1
 * NUM=2
 * NUM=3
 * NUM=4

Это рассматриваемый случай: Однако, это не работает при присвоении переменной зарезервированной области stackalloc:

class Program
{
    static void Main(string[] args)
    {
        for (int i = 0; i < 5; i++)
            call(i);

        Thread.Sleep(1000);
    }

    static unsafe void call(int number)
    {
        int* ints = stackalloc int[64];

        ints[32] = number;

        ThreadPool.QueueUserWorkItem(delegate { execute(ints[32]); });
    }

    static void execute(int number)
    {
        Console.WriteLine(" * NUM=" + number.ToString());
    }
}

Вывод:

 * NUM=4
 * NUM=4
 * NUM=4
 * NUM=4
 * NUM=4

При использовании обычной локальной переменной - просто замените метод call например выше:

static void call(int number)
{
    int j = number;

    ThreadPool.QueueUserWorkItem(delegate { execute(j); });
}

Вывод:

 * NUM=0
 * NUM=1
 * NUM=2
 * NUM=3
 * NUM=4

Эта ситуация заставляет меня не доверять анонимным delegate s в C # - потому что я не понимаю, когда именно C # не будетиспорти мои звонки анонимным delegate с.

Мои вопросы: Почему C # не отслеживает пространство stackalloc относительно анонимных delegate с? Я знаю оC # не отслеживает.Я хочу знать , почему не отслеживает, если это происходит с обычной переменной.

Я использовал .NET Core 2.1 с C # 7.3, включая переключатель /unsafe для этих примеров.

1 Ответ

0 голосов
/ 29 января 2019

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

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

Переменная ints«правильно» копируется в класс, сгенерированный компилятором, но значение переменной относится к памяти, которая была в стеке во время вызова метода call.Когда я запустил код, я получил вывод «каким бы ни был аргумент Thread.Sleep. Компилятор C # захватывает переменные , что не то же самое, что« захват всего содержимого стека ».

Вам не нужно полностью избегать делегатов - вам просто нужно избегать смешивания делегатов с небезопасным кодом и выделением стека.

Вы можете увидеть эту проблему, вообще не используя никаких анонимных функций:

using System;
using System.Threading;

class Program
{
    static void Main(string[] args)
    {
        for (int i = 0; i < 5; i++)
        {
            Call(i);
        }

        Thread.Sleep(999);
    }

    static unsafe void Call(int number)
    {
        Helper helper = new Helper();
        int* tmp = stackalloc int[64];
        helper.ints = tmp;
        helper.ints[32] = number;        
        ThreadPool.QueueUserWorkItem(helper.Method);
    }

    static void Execute(int number)
    {
        Console.WriteLine(" * NUM=" + number.ToString());
    }

    unsafe class Helper
    {
        public int* ints;

        public void Method(object state)            
        {
            Execute(ints[32]);
        }
    }    
}

Вы можете легко увидеть это, не используя каких-либо делегатов, но выполнив то же самое: «выделите стеку некоторую память и используйте указатель на нее после того, как этот стек исчезнет»:

using System;

class Program
{
    unsafe static void Main(string[] args)
    {
        int*[] pointers = new int*[5];
        for (int i = 0; i < 5; i++)
        {
            pointers[i] = Call(i);
        }
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine(pointers[i][32]);
        }
    }

    unsafe static int* Call(int number)
    {
        int* ints = stackalloc int[64];
        ints[32] = number;
        return ints;
    }
}
...