Използване на анонимни делегати с .NET ThreadPool.QueueUserWorkItem

Щях да публикувам въпрос, но го разбрах преди време и реших да публикувам въпроса и отговора - или поне моите наблюдения.

Когато използвате анонимен делегат като WaitCallback, където ThreadPool.QueueUserWorkItem се извиква в foreach цикъл, изглежда, че една и съща foreach-стойност се предава във всяка нишка.

List< Thing > things = MyDb.GetTheThings();
foreach( Thing t in Things)
{
    localLogger.DebugFormat( "About to queue thing [{0}].", t.Id );
    ThreadPool.QueueUserWorkItem(
        delegate()
        {
            try
            {
                WorkWithOneThing( t );
            }
            finally
            {
                Cleanup();
                localLogger.DebugFormat("Thing [{0}] has been queued and run by the delegate.", t.Id ); 
            }
        });
 }

За колекция от 16 екземпляра на Thing в Things наблюдавах, че всяко „Thing“, предадено на WorkWithOneThing, съответства на последния елемент в списъка „things“.

Подозирам, че това е така, защото делегатът осъществява достъп до външната променлива „t“. Обърнете внимание, че също експериментирах с предаването на Thing като параметър на анонимния делегат, но поведението остана неправилно.

Когато префакторирах кода, за да използвам именуван метод WaitCallback и предадох Thing 't' на метода, voilà ... i-тото копие на Things беше правилно предадено в WorkWithOneThing.

Урок по паралелизъм, предполагам. Предполагам също, че семейството Parallel.For се занимава с това, но тази библиотека не беше опция за нас в този момент.

Надявам се това да спести време на някой друг.

Хауърд Хофман


person Howard Hoffman    schedule 05.03.2009    source източник
comment
Ако се опитате да компилирате този код, няма ли да получите грешка System.Threading.WaitCallback' не приема '0' аргументи, тъй като не сте посочили параметър   -  person ram    schedule 12.03.2010
comment
Ram - Опитайте да промените горната декларация от: delegat() { ... } на delegat { ... } Това е, което имах, преди да направя промяната си. Надявам се това да ви помогне.   -  person Howard Hoffman    schedule 22.03.2010


Отговори (3)


Това е правилно и описва как C# улавя външни променливи в затваряния. Не става дума директно за паралелизъм, а по-скоро за анонимни методи и ламбда изрази.

Този въпрос обсъжда тази функция на езика и нейните последици в детайли.

person mqp    schedule 05.03.2009
comment
Тази статия също беше полезна: managed-world.com/archive/2008/06/13/ - person Howard Hoffman; 06.03.2009

Това е често срещано явление при използване на затваряния и е особено очевидно при конструиране на LINQ заявки. Затварянето препраща към променливата, а не към нейното съдържание, следователно, за да направите примера си работещ, можете просто да посочите променлива вътре в цикъла, която приема стойността на t и след това да препратите към това в затварянето. Това ще гарантира, че всяка версия на вашия анонимен делегат препраща към различна променлива.

person Jeff Yates    schedule 05.03.2009

По-долу има връзка с подробности защо се случва това. Написано е за VB, но C# има същата семантика.

http://blogs.msdn.com/jaredpar/archive/2007/07/26/closures-in-vb-part-5-looping.aspx

person JaredPar    schedule 05.03.2009