Вопрос о многопоточности в C#.NET

Я столкнулся с проблемой связи между потоками в приложении С#.NET. Надеюсь, кто-то направит меня в правильном направлении о возможных решениях.

У меня есть приложение на С#.NET. Это приложение формы Windows. Мое приложение имеет два потока: один поток является основным потоком (поток пользовательского интерфейса), а другой - дочерним потоком. Давайте назовем дочерний поток «workerThread». В приложении используется только одна форма. Назовем эту форму «MainForm».

Дочерний поток запускается при загрузке MainForm (используется обработчик события «Load» формы для запуска потока)

В классе MainForm у меня есть переменная с именем «stopWork», которая является общедоступной логической переменной и служит флагом, указывающим, должен ли дочерний поток продолжать работу или должен остановиться.

У меня есть другой класс (помимо класса MainForm), который содержит метод, который я выполняю в дочернем потоке. Давайте назовем этот второй класс «WorkerClass». Я передаю ссылку на текущую форму (MainForm) в конструктор "WorkerClass"

У меня есть кнопка «стоп» в основной форме, которая устанавливает для «stopWork» значение «true», если она нажата, а затем вызывает «workerThread.Join()», чтобы дождаться завершения выполнения дочернего потока.

В дочернем потоке метод "doWork" продолжает проверять состояние "parentForm.stopWork" внутри цикла for. Если для «stopWork» установлено значение «true», цикл прерывается, а затем метод завершается.

Теперь проблема в том, что как только я нажимаю кнопку «Стоп», приложение зависает.

Я вставляю части кода ниже, чтобы его было легче понять:

public partial class MainForm : Form
{
    Thread workerThread = null;
    ThreadStart workerThreadStart = null;
    WorkerClass workerClass = null;

    public bool stopWork = true;

    /*.......... some code ............*/

    private void MainForm_Load(object sender, EventArgs e)
    {
        workerThreadStart = new ThreadStart(startWork);
        workerThread = new Thread(workerThreadStart);
        stopWork = false;
        workerThread.Start();
    }

    private void startWork()
    {
        workerClass = new WorkerClass(this);
    }

    private void buttonStop_Click(object sender, EventArgs e)   //"stop" button
    {
        if (workerThread != null)
        {
            if (workerThread.IsAlive == true)
            {
                stopWork = true;
                workerThread.Join();
            }
        }
    }

    /*.......... some more code ............*/

}

public class WorkerClass
{
    MainForm parentForm=null;

    /*......... some variables and code ........*/

    public WorkerClass(MainForm parentForm)
    {
        this.parentForm=parentForm;
    }

    /* .............. some more code ...........*/

    public void doWork()
    {
       /*.......... some variables and code ...........*/

       for(int i=0;i<100000;i++)
       {
           // ** Here is the check to see if parentForm has set stopWork to true **
           if(parentForm.stopWork==true)
              break;

           /*......... do some work in the loop ..........*/


       }

    }

    /********* and more code .........*/
}

Думаю, я знаю, в чем проблема. Проблема заключается в методе «doWork» в дочернем потоке, пытающемся получить доступ к переменной «stopWork» в родительской форме, когда родительская форма уже заблокирована путем вызова метода «workerThread.Join()». Итак, я думаю, что это проблема "тупика".

Я правильно определил проблему? Или я ошибаюсь и проблема в другом?

В случае, если это действительно тупик, каковы возможные решения для решения этой проблемы?

Я немного погуглил и нашел много ресурсов о синхронизации потоков и о том, как избежать взаимоблокировок. Но я не мог понять, как применить их конкретно к моей проблеме.

Буду очень признателен за любую помощь или рекомендации по решению этой проблемы.


person sandyiscool    schedule 02.04.2011    source источник
comment
Я хочу увидеть часть вашего класса рабочего потока, чтобы увидеть, как он определяет, была ли запрошена остановка работы. Можете ли вы включить это, пожалуйста?   -  person BugFinder    schedule 02.04.2011
comment
Вы обновляете графический интерфейс из своего потока?   -  person Lasse V. Karlsen    schedule 02.04.2011
comment
@Lasse V. Karlsen: Да, я обновляю графический интерфейс из дочернего потока. Я вызываю Invoke в элементе управления форматированного текстового поля, чтобы писать сообщения в пользовательский интерфейс.   -  person sandyiscool    schedule 02.04.2011


Ответы (2)


Да, код, который вы написали, очень уязвим для взаимоблокировок. Класс BackgroundWorker особенно подвержен такому взаимоблокированию.

Проблема находится в коде, который мы не видим в вашем фрагменте, WorkerClass. Вы наверняка делаете там что-то, что так или иначе влияет на пользовательский интерфейс, и это всегда основная причина, по которой стоит задуматься о создании потока в первую очередь. Вероятно, вы используете Control.Invoke(), чтобы запустить некоторый код в потоке пользовательского интерфейса и обновить элемент управления. Возможно также, чтобы сообщить, что рабочий поток завершен, и, скажем, установить для свойства кнопки Enable значение true.

Это тупиковый город, такой код не может работать, пока поток пользовательского интерфейса не станет бездействовать, вернувшись к прокачке своего цикла сообщений. В вашем случае он никогда не будет простаивать, он застрял в Thread.Join(). Рабочий поток не может завершиться, потому что поток пользовательского интерфейса не будет бездействовать, поток пользовательского интерфейса не может простаивать, потому что рабочий поток не завершается. Тупик.

У BackgroundWorker тоже есть эта проблема: событие RunWorkerCompleted не может быть запущено, если поток пользовательского интерфейса не находится в режиме ожидания. Что вам нужно сделать, так это не блокировать поток пользовательского интерфейса. Легче сказать, чем сделать, BGW может помочь вам сделать это правильно, потому что он запускает событие, когда оно завершается. Вы можете сделать так, чтобы это событие делало все, что вы сейчас делаете в коде после вызова Thread.Join(). Вам понадобится логический флаг в вашем классе, чтобы указать, что вы находитесь в состоянии «ожидания завершения». Этот ответ имеет соответствующий код.

person Hans Passant    schedule 02.04.2011
comment
Спасибо за понимание. Вы были правы. Я действительно использую Invoke в элементе управления текстовым полем для написания сообщений в пользовательском интерфейсе !!! :-) - person sandyiscool; 02.04.2011
comment
Я думаю, что главный момент, который следует вынести из ответа Ганса и подобных ответов, заключается в том, что многопоточность трудно понять правильно. Вам абсолютно необходимо полное знание того, что вы делаете, что делают другие потоки и как они взаимодействуют. - person Lasse V. Karlsen; 03.04.2011
comment
Кажется, вы говорите, что BackgroundWorker усугубит эту проблему, и что BackgroundWorker является окончательным решением этой проблемы. - person MusiGenesis; 03.04.2011
comment
Да, если причина взаимоблокировки неясна, BGW хуже, потому что событие RunWorkerCompleted гарантирует взаимоблокировку. Решение этой проблемы эффективно выполняется с помощью BGW, поскольку оно имеет событие RunWorkerCompleted, позволяющее удалить Thread.Join(). Нет противоречия. - person Hans Passant; 03.04.2011
comment
Большое спасибо за ценную информацию. Я нашел обходной путь к проблеме. По сути, мой код вызывал Invoke для элемента управления форматированного текстового поля даже после того, как поток GUI ожидал вызова Join() в дочернем потоке. Итак, я использовал логический флаг, чтобы указать этап ожидания завершения, и всякий раз, когда флаг был истинным, вместо передачи сообщения в поле форматированного текста я собирал все сообщения из дочернего потока в объект StringCollection. После завершения дочернего потока , я распечатал сообщения, которые были сохранены в объекте String Collection. - person sandyiscool; 03.04.2011

Вместо этого используйте BackgroundWorker. Если вы хотите остановить выполнение задачи, вызовите метод CancelAsync фонового рабочего процесса.

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

person MusiGenesis    schedule 02.04.2011
comment
BackgroundWorker, безусловно, является вариантом для рассмотрения. Но что, если я хочу справиться с задачей в своем собственном потоке? Как тогда поступить? - person sandyiscool; 02.04.2011
comment
Это все равно что услышать, как кто-то говорит, что позвонить в пожарную службу, безусловно, стоит рассмотреть. Но что, если я хочу потушить свой горящий дом в одиночку? Я предлагаю поработать с этими руководствами: msdn. microsoft.com/en-us/library/aa645740(v=vs.71).aspx - person MusiGenesis; 02.04.2011
comment
LOL .. Ну, вы, конечно, не будете вызывать пожарных, чтобы потушить пожар на кухне :-) Кроме шуток, я люблю обрабатывать потоки самостоятельно, если только это не очень сложный многопоточный сценарий. Это простой случай запуска › проверки состояния › остановки потока. Он даже не включает какой-либо общий ресурс, к которому обращаются потоки. Все, что ему нужно, это способ проверить флаг, не заходя в тупик. - person sandyiscool; 02.04.2011
comment
Я думаю, что ваше приложение блокируется, когда вы нажимаете кнопку «Стоп», потому что вы присоединяетесь к workerThread, потоку, который вы поддерживаете на неопределенный срок, потому что вы поддерживаете его как переменную уровня класса. doWork() никогда не вызывается (если только он не вызывается в одной из отрезанных частей), и это не имело бы значения, если бы это было так. Опять же, я бы посоветовал поработать с учебными пособиями (фактически запустить код) и лучше понять, как все должно работать. - person MusiGenesis; 02.04.2011
comment
Вот еще один: msdn.microsoft.com /en-us/library/7a2f3ay4(v=vs.80).aspx#Y170 Кажется, это более или менее точно то, что вы делаете. Особый совет — использовать ключевое слово volatile для вашей переменной stopWork. volatile — это простой и дешевый способ синхронизации (работает только для логического значения). - person MusiGenesis; 02.04.2011
comment
Еще проще, теперь, когда я смотрю на это: просто избавьтесь от строки workerThread.Join() при нажатии кнопки остановки и сделайте stopWork изменчивой. - person MusiGenesis; 02.04.2011
comment
@MusiGenesis: Спасибо за помощь. Предоставленные вами ссылки отлично подходят для углубленного изучения многопоточности. - person sandyiscool; 03.04.2011