Запуск потока с/без делегата()
В чем разница между:
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();
}