Основная ветка: можно ли вытеснить runnables?

У меня есть исполняемый экземпляр, который снова самопланируется в конце своего метода run:

    private class MyRunnable implements Runnable {
        private volatile boolean cancelled = false;
        private Handler handler;

        public MyRunnable(Handler h){
            handler = h;
        }


        @Override
        public void run(){
            //Do stuff
            if(!cancelled){
                //Preemtion possible here?
                handler.postDelayed(this, 1000);
            }
        }

        public void selfStart(){
            cancelled = false;
            handler.removeCallbacks(this);
            handler.post(this);
        }

        public void selfCancel(){
            cancelled = true;
            handler.removeCallbacks(this);
        }
    }

Выполняемый сначала планируется в основном потоке, вызывая selfStart из onStart действия.

В то же время runnable может быть отменен извне (вызовом selfCancel) из действия onStop, а также из широковещательного приемника.

Насколько я знаю, Runnable.run, Activity.onStop и BroadcastReceiver.onReceive выполняются в одном и том же потоке (основном), поэтому на первый взгляд я подумал, что проблем с потокобезопасностью не будет.

Но иногда похоже, что исполняемый объект вытесняется в середине своего вызова run, затем он отменяется из активности или получателя, а затем возобновляется и снова перепланируется.

Это возможно?


ОБНОВЛЕНИЕ:
Я постараюсь лучше объяснить проблему. Показанный выше класс предназначен для периодического запуска задач в основном потоке. В комментарии «делай что-нибудь» на самом деле есть код, который обновляет TextView значением, переданным конструктору MyRunnable. Активность отменяет текущий исполняемый объект и запускает новый при получении определенных намерений. Несмотря на то, что текущий runnable всегда запрашивается для отмены перед созданием нового, иногда он остается запущенным вместе с новым, поэтому в текстовом представлении отображаются чередующиеся значения. Это не предполагаемое поведение.

Я думал, что если runnable в данный момент выполняется в основном потоке, он будет работать до завершения, а затем другие runnables или обработчики событий будут извлечены из очереди и выполнены при необходимости, но ни одно ожидающее событие или runnable не может быть «выполнено наполовину» .

В основном потоке выполняются два вида задач, связанных с проблемой:

  • R1: Самопланируемая задача MyRunnable. Запускается, а затем снова публикуется с задержкой в ​​1 с.
  • R2: обработчики событий, которые запрашивают отмену текущего экземпляра MyRunnable и создают новый R1'. Они происходят случайным образом и выполняются только один раз.

Я рассматривал два сценария. Первый:

  1. R1 уже работает в основном потоке.
  2. R2 прибывает и ставится в очередь в основном потоке.
  3. R1 завершает работу и снова публикует себя.
  4. R2 запускается и удаляет обратные вызовы для R1.
  5. R1 больше никогда не должен запускаться.

И второй:

  1. R1 не запущен, но запланирован.
  2. Приходит R2 и удаляет обратные вызовы для R1.
  3. R1 больше никогда не должен запускаться.

Теоретически, если приоритета нет и есть только один поток, почему иногда в основном потоке бывает два R1?


person Mister Smith    schedule 29.01.2014    source источник
comment
если вы публикуете свой Runnable только в одном Looper, то, что вы описываете, невозможно   -  person pskink    schedule 29.01.2014
comment
откуда вы знаете, что исполняемый файл предварительно выбран в отмеченной вами строке? Возможно ли, что он выполняет handler.post(this), а затем завершает текущий запуск?   -  person user3118604    schedule 30.01.2014
comment
@user3118604 user3118604 Я не думаю, что это вытеснено, это единственное возможное объяснение, которое я могу найти, когда есть один поток. Но звучит надуманно, и я не хочу верить, что это вообще возможно.   -  person Mister Smith    schedule 30.01.2014
comment
Трудно поверить в то, что метод, работающий в потоке, может быть вытеснен чем-то другим в этом потоке. Более правдоподобным может быть то, что каждый метод, однажды запущенный, выполняется до завершения перед чем-либо еще в этом потоке, но что очередь вызываемых методов управляется не так, как вы ожидаете. Регистрация входа и возврата каждого метода с его tid должна обеспечить понимание — вы никогда не должны видеть, как новый метод начинается в потоке до того, как текущий метод вернется.   -  person Chris Stratton    schedule 30.01.2014


Ответы (3)


Поскольку у вас нет синхронизации на selfStart или selfCancel, это вполне возможно.

Нерелевантное примечание, selfCancel может быть вызвано в отдельном потоке после того, как ваш оператор if в вашем методе запуска проверил значение cancelled. Затем MyRunnable получит еще один вызов, который сразу же завершится, поскольку он был отменен.

person William Morrison    schedule 29.01.2014
comment
Хотя это было бы верно, если бы было задействовано несколько потоков, на самом деле это не осмысленный ответ на заданный вопрос, если вы не объясните, почему часть этого кода будет выполняться в потоке, отличном от основного потока/потока пользовательского интерфейса. - person Chris Stratton; 29.01.2014
comment
Нет отдельной ветки. Все планирование или отмена вызываются в основном потоке. Синхронизация этих методов мало что изменит, потому что selfStart вызывается только один раз при запуске приложения. Проблемными методами являются selfCancel и run. Я думаю, что selfCancel вызывается, когда метод запуска находится внутри проверки флага. - person Mister Smith; 29.01.2014
comment
@MisterSmith - это невозможно, если задействован только один поток. Почему бы вам не добавить протоколирование потока, используемого для каждого метода? - person Chris Stratton; 29.01.2014
comment
Если бы ваш объект не управлялся более чем одним потоком, этого бы не произошло. Итак, попробуйте синхронизировать свой объект или отследить второй поток. - person William Morrison; 30.01.2014
comment
@ChrisStratton Согласен. Регистрация была добавлена ​​недавно, но безрезультатно. Эту проблему очень сложно воспроизвести, и, к сожалению, мне известны только версии без ведения журнала, поэтому мне придется выяснить это, просто прочитав код. В любом случае, я обновил вопрос с лучшим объяснением. - person Mister Smith; 30.01.2014

Мое предложение состояло бы в том, чтобы переместить //Do stuff внутрь аннулированного чека.

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

    @Override
    public void run(){
        if(!cancelled){
            //Do stuff
            handler.post(this);
        }
    }

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

person Nick Palmer    schedule 29.01.2014
comment
В моем конкретном случае не имеет большого значения, работает ли материал после отмены, но в любом случае спасибо за совет. И никто никогда не будет вызывать selfCancel из другого потока, это гарантировано :) Это явно указано в документах. - person Mister Smith; 30.01.2014
comment
РЖУ НЕ МОГУ. Потому что размещение его в документах означает, что будущие разработчики обязательно прочитают документы. - person Nick Palmer; 31.01.2014
comment
Вероятно, в будущем не будет другого разработчика, сейчас я единственный с этим. Но я передумал после публикации этого комментария, и теперь я выбрасываю исключения, если методы не вызываются из основного потока. - person Mister Smith; 31.01.2014
comment
Хороший. Всегда хорошо проверять свои предположения в коде. Даже если вы единственный разработчик, иногда, когда вы возвращаетесь к своему собственному коду, вы забываете предположения, которые вы сделали, когда писали его, а затем стреляете себе в ногу. - person Nick Palmer; 05.02.2014

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

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

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

person Mister Smith    schedule 31.01.2014