Запуск потока с/без делегата()

В чем разница между:

new Thread(new ThreadStart(SomeFunc))

и

new Thread( delegate() { SomeFunc();} )

Этот код дает странные выходы на моем компьютере:

public class A
{
    int Num;

    public A(int num)
    {
        Num = num;
    }

    public void DoObj(object obj)
    {
        Console.Write(Num);
    }

    public void Do()
    {
        Console.Write(Num);
    }
}

/////// in void main()

for (int i = 0; i < 10; i++)
{
    (new Thread(new ThreadStart((new A(i)).Do))).Start(); // Line 1
    (new Thread(new ThreadStart(delegate() { (new A(i)).Do(); }))).Start(); // Line 2
    (new Thread(delegate() { (new A(i)).Do(); })).Start(); // Line 3
}

Если выполняется только строка 1, результат выглядит примерно так:

0 2 3 1 5 6 4 7 8 9

что нормально, но если выполняется строка 2 или 3, вывод:

3 3 3 5 5 7 7 9 9 10

Есть несколько кратных чисел и 10, что довольно странно, что цикл никогда не запускается с номером 10. Каков трюк этих?

Спасибо.

Ответы

Ответ 1

С делегатом вы захватываете i.

Разница в том, что при new ThreadStart((new A(i)).Do)) вы создаете новый экземпляр A в цикле for с i в качестве параметра. Это означает, что в этот момент значение i берется и отправляется конструктору. Таким образом, делегат, который вы отправляете, не является созданием A, но вы фактически отправляете конструктору конструктор A делегата метода Do экземпляра A.

Однако с delegate() { (new A(i)).Do(); }) (оба из них) вы отправляете ссылку i в поток.

Затем поток запускается некоторое время, а между тем цикл for продолжается. К моменту i используется в делегате (т.е. Поток запущен), цикл for переместился на 3 и это то, что вы видите. То же самое касается второго и третьего потоков. Три потока запускаются, но подождите, пока начальный поток завершит какую-либо работу. Затем создаются потоки (нитки 1, 2 и 3), и они выполняют свою работу. Windows возвращается к потоку с циклом for и продолжает запускать потоки 4 и 5.

Некоторые материалы для чтения:

Ответ 2

Чтобы ответить на вашу первую точку, delegate() { SomeFunc();} создает функцию, вызывающую SomeFunc(), тогда как не используя delegate() просто использует функцию SomeFunc непосредственно как метод ThreadStart.

В вашем втором вопросе вы используете детали реализации анонимных функций С#. Все три ссылки на i относятся к тому же i, который увеличивается в три раза. У вас есть условие гонки между тремя функциями, которые означают, что i может увеличиваться несколько раз до запуска запущенных потоков.

Ответ 3

'Когда используется конструктор для объекта A?' помогает ответить на вопрос.

new ThreadStart((new A(i)).Do))

Когда эта строка кода выполняется - вызывается конструктор и ссылка на функцию .Do на вновь созданном объекте A хранится делегатом ThreadStart.

В строках 2 и 3 вы используете анонимный делегат (представленный на С# 2.0).

delegate() { (new A(i)).Do(); })

Содержимое анонимного делегата не выполняется до вызова делегата; в этом случае потоком, когда для этого назначается временной срез.

Переменная я объявляется только в начале цикла for, а в содержимом делегата есть ссылка на нее (делегаты это сделают) - когда код выполняется, он должен захватить значение я во время выполнение.

Это объясняет значение 10. я имеет значение 10, когда цикл завершил выполнение. Если один из потоков выполняется после завершения цикла, он выдает 10.

Чтобы избежать проблемы с несколькими номерами, вы можете создать локальную копию переменной цикла. Делегат будет ссылаться на свою версию icopy;

for (int i = 0; i < 10; i++)
{
     int icopy = i;
     (new Thread(new ThreadStart(delegate() { (new A(icopy)).Do(); }))).Start();
}