C # создает столько экземпляров класса, сколько есть процессоров

2

У меня есть приложение GUI С#, которое имеет одну кнопку Start/Stop.

Первоначально этот GUI создавал один экземпляр класса, который запрашивает базу данных и выполняет некоторые действия, если есть результаты, и получает одну "задачу" за раз из базы данных.

Затем меня попросили попытаться использовать всю вычислительную мощность на некоторых из 8 основных систем. Используя число процессоров, я полагаю, что могу создать это количество экземпляров моего класса и запустить их все и приблизиться к справедливому объему вычислительной мощности.

Environment.ProccessorCount;

Используя это значение, в форме GUI я пытаюсь пройти через цикл ProccessorCount количество раз и запустить новый поток, который вызывает метод типа doWork в классе. Затем "Сон" в течение 1 секунды (чтобы убедиться, что начальный запрос прошел), а затем перейдите к следующей части цикла.

У меня все время возникали проблемы с этим, потому что, казалось, дождались завершения цикла, чтобы запустить запросы, приводящие к некоторому конфликту (получение того же значения из базы данных MySQL).

В основной форме после запуска "рабочих" она меняет текст кнопки на STOP, и если кнопка снова нажата, она должна выполнять на каждом "рабочем" методе "stopWork".

Делает ли то, что я пытаюсь сделать, имеет смысл? Есть ли лучший способ сделать это (это не связано с реструктуризацией рабочего класса)?

  • 2
    меньше описания больше кода
Теги:
multithreading

9 ответов

2
Лучший ответ

Перестройте свой дизайн, чтобы в фоновом режиме был один поток, проверяющий вашу базу данных для работы.

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

Не забудьте использовать инструменты синхронизации, такие как семафоры и мьютексы, для ключевых ограниченных ресурсов. Точная настройка синхронизации стоит вашего времени.

Вы также можете поэкспериментировать с максимальным количеством рабочих потоков - я предполагаю, что это будет немного больше вашего текущего количества процессоров.

  • 0
    Как это выглядит сейчас, мне кажется, это лучшее решение. Я думаю, что небольшая реструктуризация также решит и некоторые другие мои проблемы совместной работы с данными. Последний вариант, который я попытаюсь сделать, состоит в том, чтобы он запрашивал по одной задаче за раз, требовал одну (если таковая имеется) и передавал ее рабочему классу, как только позаботились обо всех общих данных. Оттуда не должно быть проблем со встречной записью / чтением ... и т. Д.
2

Хотя исчерпывающий ответ на лучшие практики многопоточного развития немного превосходит то, что я могу здесь написать, пару вещей:

  • Не используйте Sleep(), чтобы ждать, когда что-то продолжит, если АБСОЛЮТНО необходимо. Если у вас есть другой процесс кода, который вам нужно дождаться завершения, вы можете либо Join() использовать этот поток, либо использовать ManualResetEvent или AutoResetEvent. В MSDN есть много информации об их использовании. Потратьте некоторое время, чтобы прочитать его.
  • Вы не можете гарантировать, что ваши потоки будут выполняться на своем ядре. Хотя вполне вероятно, что планировщик потоков ОС сделает это, просто имейте в виду, что он не гарантируется.
  • 0
    действительно, используйте Join с параметром времени, т.е. thread.Join (1000);
  • 0
    @ Богдан: Это, конечно, вариант, но вряд ли единственный или "лучший". Например, вы предполагаете, что тайм-аут подходит. С другой стороны, вы предполагаете, что не хотите продолжать, пока не закончится другой поток. Сигнальный объект, такой как ManualResetEvent или AutoResetEvent, является другим в равной степени подходящим подходом в зависимости от того, что вы делаете.
1

Да, имеет смысл то, что вы пытаетесь сделать.

Было бы разумно сделать 8 рабочих, каждый из которых будет выполнять задачи из очереди. Вы должны позаботиться о том, чтобы правильно синхронизировать потоки, если им нужно получить доступ к общему состоянию. Из описания вашей проблемы кажется, что у вас проблема с синхронизацией потоков.

Вы должны помнить, что вы можете обновлять GUI только из потока графического интерфейса. Это также может быть источником ваших проблем.

Невозможно сказать, в чем проблема, без дополнительной информации или примера кода.

  • 0
    Да, звучит как поток безопасности / синхронизация является ключом здесь ...
1

Я бы предположил, что самый простой способ увеличить использование процессоров - это просто вызвать рабочие методы на потоках из ThreadPool (вызывая ThreadPool.QueueUserWorkItem). Если вы сделаете это в цикле, среда выполнения закроет потоки из пула потоков и параллельно будет запускать рабочие потоки.

ThreadPool.QueueUserWorkItem(state => DoWork());
1

Никогда не используйте Sleep для синхронизации потоков.

В вашем вопросе недостаточно подробностей, но вы можете использовать ManualResetEvent, чтобы заставить рабочих ждать первоначальный запрос.

0

Есть несколько хороших идей, опубликованных выше. Одна из вещей, с которыми мы столкнулись, заключается в том, что мы не только хотели использовать многопроцессорное приложение, но и приложение с поддержкой нескольких серверов. В зависимости от вашего приложения мы используем очередь, которая обернута в блокировку через общий веб-сервер (заставляя других блокироваться), в то время как мы получаем следующую вещь, которую нужно обработать.

В нашем случае мы обрабатываем много данных, мы сохраняем единицу, мы запираем объект, получаем идентификатор следующего необработанного элемента, отмечаем его как обрабатываемую, разблокируем объект, передаем идентификатор записи, обрабатывается обратно в основной поток на вызывающем сервере, а затем обрабатывается. Кажется, что это хорошо работает для нас, так как время, необходимое для блокировки, получения, обновления и выпуска, очень мало, и в то время как блокировка действительно происходит, мы никогда не сталкиваемся с ситуацией взаимоблокировки, ожидая ресурсов (поскольку мы используем блокировку (объект ) {} и приятная жесткая попытка поймать внутри, чтобы мы корректно обрабатывали ошибки внутри.

Как упоминалось в другом месте, все это обрабатывается в основном потоке. Учитывая информацию, которую нужно обработать, мы подталкиваем ее к новому потоку (который для нас идет и извлекает 100 МБ данных и обрабатывает его за вызов). Такой подход позволил нам масштабироваться за пределы одного сервера. Раньше нам приходилось сталкиваться с высоким уровнем аппаратного обеспечения, и теперь мы можем бросить несколько более дешевых, но все же очень способных серверов. Мы также можем это сделать через нашу ферму виртуализации в периоды низкой загрузки.

С другой стороны, я не упомянул, мы также используем блокировки мьютексов внутри нашего хранимого процесса, так что, если два приложения на двух серверах называют это одновременно, он обрабатывается изящно. Таким образом, концепция выше применима к нашему приложению и к базе данных. Бэкэндом наших клиентов является серия MySql 5.1, и она выполняется всего несколькими строками.

Одна из тех вещей, которые, как я думаю, забывают, когда они разрабатываются, заключается в том, что вы хотите быстро и быстро встать и выйти из замка. Если вы хотите вернуть большие куски данных, я лично не сделал бы этого в самом замке, если вам действительно не пришлось. В противном случае вы не сможете делать много лишних вещей, если все ждут получения данных.

Хорошо, нашел мой код MySql для того, чтобы делать то, что вам нужно.

    DELIMITER //

    CREATE PROCEDURE getnextid(
        I_service_entity_id INT(11)
      , OUT O_tag VARCHAR(36)
    )

    BEGIN
      DECLARE L_tag VARCHAR(36) DEFAULT '00000000-0000-0000-0000-000000000000';
      DECLARE L_locked INT DEFAULT 0;

      DECLARE C_next CURSOR FOR
        SELECT tag FROM workitems
        WHERE status in (0)
          AND processable_date <= DATE_ADD(NOW(), INTERVAL 5 MINUTE)
        ;

      DECLARE EXIT HANDLER FOR NOT FOUND
      BEGIN
        SET L_tag := '00000000-0000-0000-0000-000000000000';
        DO RELEASE_LOCK('myuniquelockis');
      END;

      SELECT COALESCE(GET_LOCK('myuniquelockis',20), 0) INTO L_locked;
      IF L_locked > 0 THEN
        OPEN C_next;
        FETCH C_next INTO I_tag;
        IF I_tag <> '00000000-0000-0000-0000-000000000000' THEN
          UPDATE workitems SET
              status = 1
            , service_entity_id = I_service_entity_id
            , date_locked = NOW()
          WHERE tag = I_tag;

        END IF;
        CLOSE C_next;
        DO RELEASE_LOCK('myuniquelockis');
      ELSE
        SET I_tag := L_tag;
      END IF;
    END
    //

    DELIMITER ;

В нашем случае мы возвращаем GUID в С# в качестве параметра out. Вы можете заменить SET в конце с помощью SELECT L_tag; и сделать с ним и потерять параметр OUT, но мы вызываем это из другой оболочки...

Надеюсь, что это поможет.

0

Вы также можете сделать блокировку кода "выборка из БД", таким образом, только один поток будет запрашивать базу данных за раз, но, очевидно, это несколько уменьшает прирост производительности.

Некоторый код того, что вы делаете (и, возможно, какой-то SQL, это действительно зависит) будет огромной помощью.

Однако, предполагая, что вы извлекаете задачу из БД, и для этих задач требуется некоторое время на С#, вы, вероятно, захотите что-то вроде этого:

object myLock;

void StartWorking()
{
    myLock = new object(); // only new it once, could be done in your constructor too.
    for (int i = 0; i < Environment.Processorcount; i++)
    {
        ThreadPool.QueueUserWorkItem(null => DoWork());
    }
}

void DoWork(object state)
{
    object task;
    lock(myLock)
    {
        task = GetTaskFromDB();
    }

    PerformTask(task);
}
0

Кажется, вы сталкиваетесь с общей проблемой многопоточного программирования. Он назвал Race Condition, и вам следует сделать некоторые исследования по этой и другим проблемам многопоточности, прежде чем переходить слишком далеко. Очень легко быстро испортить все ваши данные.

Кроме того, вы должны убедиться, что все ваши команды в вашей базе данных (например: Получить доступную задачу) выполняются в рамках одной транзакции.

Я не знаю MySQL. Хорошо, чтобы дать полный ответ, однако очень простой пример для T-SQL может выглядеть так:

BEGIN TRAN 
DECLARE @taskid int 
SELECT @taskid=taskid FROM tasks WHERE assigned = false
UPDATE tasks SET assigned=true WHERE taskid = @taskID 
SELECT * from tasks where taskid = @taskid 
COMMIT TRAN

MySQL 5 и выше поддержка транзакций тоже.

0

Я подозреваю, что у вас есть такая проблема: вам нужно сделать копию переменной цикла (задачи) в currenttask, иначе все потоки фактически будут иметь одну и ту же переменную.

<main thread>
var tasks = db.GetTasks();
foreach(var task in tasks) {
   var currenttask = task; 
   ThreadPool.QueueUserWorkItem(state => DoTask(currenttask));
   // or, new Thread(() => DoTask(currentTask)).Start()
   // ThreadPool.QueueUserWorkItem(state => DoTask(task)); this doesn't work!
}

Обратите внимание, что вы не должны Thread.Sleep() в основном потоке ждать завершения рабочих потоков. если вы используете threadpool, вы можете продолжать ставить в очередь рабочие элементы, если вы хотите дождаться завершения выполняемых задач, вы должны использовать что-то вроде AutoResetEvent, чтобы дождаться окончания потоков.

Ещё вопросы

Сообщество Overcoder
Наверх
Меню