Почему параллельно этот код работает иногда? - PullRequest
1 голос
/ 02 сентября 2011

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

Так что я подумал, что сам попробую написать какой-нибудь код, чтобы он был как можно меньше, без использования барьера и т. Д.не вести себя согласованно, однако.Иногда это работает, иногда нет, и я не могу понять, почему.

Этот код - просто механизм, который я использую, чтобы попытаться синхронизировать два потока.Он не делает ничего полезного, только минимальный объем кода, необходимый для воспроизведения ошибки.

Итак, вот код:

    // node in linkedlist of work elements
        class WorkItem {
            public int Value;
            public WorkItem Next;
        }

        static void Test() {

            WorkItem fst = null; // first element

            Action create = () => {
                WorkItem cur=null;
                for (int i = 0; i < 1000; i++) {                    

                    WorkItem tmp = new WorkItem { Value = i }; // create new comm class

                    if (fst == null) fst = tmp; // if it's the first add it there
                    else cur.Next = tmp;        // else add to back of list

                    cur = tmp; // this is the current one
                }
                cur.Next = new WorkItem { Value = -1 }; // -1 means stop element
#if VERBOSE
                Console.WriteLine("Create is done");
#endif
            };

            Action consume = () => {
                //Thread.Sleep(1); // this also seems to cure it
#if VERBOSE
                Console.WriteLine("Consume starts"); // especially this one seems to matter
#endif

                WorkItem cur = null;
                int tot = 0;

                while (fst == null) { } // busy wait for first one
                cur = fst;
#if VERBOSE
                Console.WriteLine("Consume found first");
#endif
                while (true) {
                    if (cur.Value == -1) break; // if stop element break;
                    tot += cur.Value;

                    while (cur.Next == null) { } // busy wait for next to be set
                    cur = cur.Next; // move to next
                } 
                Console.WriteLine(tot);
            };

            try { Parallel.Invoke(create, consume); }
            catch (AggregateException e) {
                Console.WriteLine(e.Message);
                foreach (var ie in e.InnerExceptions) Console.WriteLine(ie.Message);
            }

            Console.WriteLine("Consume done..");
            Console.ReadKey();
        }

Идея состоит в том, чтобы иметь Linkedlist рабочих элементов.Один поток добавляет элементы в конец этого списка, а другой поток читает их, что-то делает и опрашивает поле Next, чтобы увидеть, установлено ли оно.Как только он установлен, он переместится на новый и обработает его.Он опрашивает поле Next в тесном цикле занятости, потому что оно должно быть установлено очень быстро.Переход в режим сна, переключение контекста и т. Д. Убьет выгоду от парализации кода.Время, необходимое для создания рабочего элемента, было бы вполне сопоставимо с его выполнением, поэтому потраченные впустую циклы должны быть весьма малы.

Когда я запускаю код в режиме выпуска, иногда он работает, иногда он ничего не делает.Кажется, проблема в потоке Consumer, поток Create всегда кажется завершенным.(Вы можете проверить, поигравшись с Console.WriteLines).Он всегда работал в режиме отладки.В релизе это около 50% хит и мисс.Добавление нескольких Console.Writelines помогает коэффициенту успеха, но даже в этом случае он не равен 100%.(#define VERBOSE материал).

Когда я добавляю Thread.Sleep (1) в поток 'Consumer', он также, кажется, исправляет это.Но неспособность воспроизвести ошибку - это не то же самое, что знать наверняка, что она исправлена.

Кто-нибудь здесь знает, что здесь происходит не так?Это какая-то оптимизация, которая создает локальную копию или что-то, что не обновляется?Что-то вроде того?

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

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

Заранее благодарим за любые советы,

Герт-Ян

Ответы [ 2 ]

2 голосов
/ 02 сентября 2011

Я делаю все возможное, чтобы избежать любого минного поля взаимодействия замыкания / стека любой ценой.Это, вероятно, условие гонки (на уровне языка), но без учета Parallel.Invoke я не уверен.По сути, иногда fst изменяется с помощью create (), а иногда нет.В идеале, его НИКОГДА не следует менять (если у c # было хорошее поведение при закрытии).Это может быть связано с тем, какой поток Parallel.Invoke выберет для запуска create () и потребление ().Если create () выполняется в главном потоке, он может изменить fst, прежде чем потребление () получит его копию.Или create () может работать в отдельном потоке и получать копию fst.В принципе, насколько я люблю c #, это очень неприятно в этом отношении, поэтому просто обойдите его и рассматривайте все переменные, участвующие в замыкании, как неизменяемые.

Чтобы заставить его работать:

//Replace 
WorkItem fst = null
    //with
WorkItem fst = WorkItem.GetSpecialBlankFirstItem();

//And 
if (fst == null) fst = tmp;
   //with
if (fst.Next == null) fst.Next = tmp;
1 голос
/ 02 сентября 2011
...