Как синхрониировать 3 потока C#

312
06 марта 2017, 09:27

Есть задача. Учебная, так что вопросы оптимальности решения отходят на второй план. Есть 3 потока, работу которых нужно синхронизировать. Первый поток собирает информацию о файлах и папках. Работает циклично и рекурсивно. Найдя информацию, он должен остановиться и дать потокам 2 и 3 записать ее в файл и память. Когда потоки 2 и 3 закончат запись, поток 1 возобновляет работу и ищет следующий файл(папку). Потоков должно быть 3. Писать каждый найденный файл в новом потоке - не вариант.
Вопросы следующие:
1. Какой механизм лучше применить для синхронизации потоков: AutoResetEvent (свой для каждого из двух потоков - см. код), ManualResetEvent или Semaphore.
2. Как нужно исправить код, чтобы потоки запускались и останавливались когда нужно? Буду благодарен за пояснения почему именно так, потому что с методами Set(), Reset(), Wait() я игрался долго, но безуспешно.
3. Как правильно организовать остановку вторичных потоков после окончания работы первичного? Я приравнял null первичный поток, но думаю, что это не лучшая идея.
4. И был бы благодарен за разъяснение как решать аналогичные задачи в нормальных, не учебных проектах.
5. Нужно ли в данном примере проводить считывание информации вторичными потоками в критической секции? Как я предполагаю, информация в процессе считывания не поменяется, так как основной поток ждет завершения вторичных, и необходимости в критической секции нет.

Код:

private Queue<Nested3> collection;
        static int counter=100;
        object locked;
        Thread SearcherThread;
        Thread Handler1Thread;
        Thread Handler2Thread;
        static WaitHandle[] events;
        private class Nested3
        {
            public int Val;
            public bool IsHandled;
        public Nested3(int val)
            {
                this.Val = val;
                IsHandled = false;
            }
       }
        public ARETest()
        {
            collection = new Queue<Nested3>();
            events = new WaitHandle[] { new AutoResetEvent(false), new AutoResetEvent(false) };
            SearcherThread = new Thread(Searcher);
            SearcherThread.Start();
            Handler1Thread = new Thread(Handler1);
            Handler2Thread = new Thread(Handler2);
            locked = new object();
        }
       private void Searcher()
        {  
                // Очередь для двух задач в двух разных потоках. 
         ThreadPool.QueueUserWorkItem(new WaitCallback(Handler1), events[0]);
         ThreadPool.QueueUserWorkItem(new WaitCallback(Handler2), events[1]);
            for (int i = 0; i < 3; i++)
            {
                Thread.Sleep(2);  // имитация поиска
                Monitor.Enter(locked); // применение класса Monitor для создания критической секции обусловлено условием задачи
                collection.Enqueue(new Nested3(++counter)); // Найден первый пакет информации для записи вторичными потками
                Monitor.Exit(locked);
                (events[0] as AutoResetEvent).Set(); // запуск вторичных потоков
                (events[1] as AutoResetEvent).Set();
                (events[0] as AutoResetEvent).Reset(); // сброс в несигнальное состояние
                (events[1] as AutoResetEvent).Reset();
                // Ожидание пока все задачи завершаться.
                WaitHandle.WaitAll(events);
                Monitor.Enter(locked); // Очистка очереди от записанной информации
                collection.Dequeue();
                Monitor.Exit(locked);
            if (i<2) Searcher();   // Рекурсивный вызов                  
            }
            SearcherThread = null;
            // Запуск  последнего прохода цикла  вторичными потоками
            (events[0] as AutoResetEvent).Set(); 
            (events[1] as AutoResetEvent).Set();
        }
    private  void Handler1(object state)
    { 
        var auto = (AutoResetEvent)state;
        do
        {
        auto.WaitOne(); // здесь я хочу чтобы потоки ждали сигнала от первичного потока
        Thread.Sleep(15); // имитация записи в файл
        auto.Set(); // Здесь вторичный поток должен дать первичному сигнал, что он закончил итерацию 
        } while (SearcherThread != null);
    }
    private void Handler2(Object state)  // работает аналогично предыдущему методу, но с другой задержкой
    {
        var auto = (AutoResetEvent)state;
        do
        {
         auto.WaitOne(); // здесь я хочу чтобы потоки ждали сигнала от первичного потока
        Thread.Sleep(15); // имитация записи в файл
        auto.Set(); // Здесь вторичный поток должен дать первичному сигнал, что он закончил итерацию 
        } while (SearcherThread != null);

    }

}
Answer 1

А нужна ли Вам здесь вообще прямая работа с потоками? Для выполнения нескольких задач параллельно можно использовать высокоуровневый приметив Parallel.Invoke.

static int counter=100;
private class Nested3 {
    public int Val;
    public bool IsHandled;
    public Nested3(int val) {
        this.Val = val;
        IsHandled = false;
    }
}
private void Searcher() {
    for(int i = 0; i < 3; i++) {
        Thread.Sleep(2); // имитация поиска
        Nested3 currentItem = new Nested3(++counter);
        Parallel.Invoke(() => Handler1(currentItem), () => Handler2(currentItem));
    }
}
private void Handler1(Nested3 currentItem) {
    Thread.Sleep(15); // имитация записи в файл
}
private void Handler2(Nested3 currentItem) {
    Thread.Sleep(15); // имитация записи в файл
}

Если важно использовать именно WaitHandle, то можно поступить следующим образом:

private AutoResetEvent done = new AutoResetEvent(false);
private void Searcher() {
    for(int i = 0; i < 3; i++) {
        Thread.Sleep(2); // имитация поиска
        Nested3 currentItem = new Nested3(++counter);
        ThreadPool.QueueUserWorkItem(_ => {
            Handler2(currentItem);
            done.Set();
        }); // одно из заданий выполняем в пуле потоков
        Handler1(currentItem); // а другое выполняем в текущем потоке
        done.WaitOne(); // ждём завершения задания, которое отдали пулу потоков
    }
}
Answer 2

Спасибо за ответ. Такое решение мне в голову не пришло.
В общем, в итоге метод Searcher() выглядит так.

private void Searcher()
        {  
            for (int i = 0; i < 3; i++)
            {
                Thread.Sleep(2);
                Monitor.Enter(locked);
                collection.Enqueue(new Nested3(++counter)); // теперь очередь можно заменить на единичный объект класса
                Monitor.Exit(locked);
                ThreadPool.QueueUserWorkItem(_ =>
                {
                    Handler2();
                    (events[1] as AutoResetEvent).Set();
                });
                ThreadPool.QueueUserWorkItem(_ =>
                {
                    Handler1();
                    (events[0] as AutoResetEvent).Set();
                });
                WaitHandle.WaitAll(events);
                Console.WriteLine("Доджался");
                Monitor.Enter(locked);
                collection.Dequeue();
                Monitor.Exit(locked);

                Console.WriteLine("Поиск отработан"); 
                Console.WriteLine();
                if (counter<105) Searcher(); // рекурсивыный  вызов
            }
        }
READ ALSO
Обфускация кода C# при использовании MVVM Light

Обфускация кода C# при использовании MVVM Light

Здравствуйте! Хочу защитить программу на C# (WPF,NET 4

302
Привязка команды к MenuItem в CompositeCollection

Привязка команды к MenuItem в CompositeCollection

Использую контекстное меню для изменения строк DataGridКаждый пункт меню представляет собой цвет, который применяется к строкам таблицы

240
Доступ к обьектам коллекции

Доступ к обьектам коллекции

Как сравнить элементы коллекции каждый с каждым? Немного обьясню, у меня есть метод, который работает с двумя обьектами, вызываю его так objectnamemethod(objectname1)в...

360
Эмуляция нажатия клавиши unity

Эмуляция нажатия клавиши unity

Как в Unity сделать эмуляцию нажатия клавиши на клавиатуре? Чтобы на него реагировали и внешние программыМышкой мы можем, например, подвигать...

329