Індикація ходу виконання тривалих операцій

Хоча в більшості додатків нема чого обчислювати pi, багато хто з них
виконують тривалі операції, наприклад друк, виклик Web-сервісу або
підрахунок процентних доходів за якимсь багатомільйонного внеску в банку
Pacific Northwest. Зазвичай користувачі готові почекати завершення
такого роду операцій, часто займаючись в цей час чимось іншим, якщо
можуть спостерігати за ходом виконання операції. Тому навіть у моєму
маленькому додатку є індикатор прогресу (progress bar). Мій
алгоритм обчислює 9 знаків числа pi за один прохід. Як тільки
з'являється новий набір цифр, програма оновлює і змінює
індикатор прогресу. Наприклад, рис. 2 ілюструє хід обчислення 1000
знаків pi (21 знак – добре, а 1000 знаків – краще).

Window message queue – Черга віконних повідомлень
Request update – Запит на оновлення
Dequeue – Витяг з черги
Owning thread – Потік-власник
Update – Оновлення
Window controls – Віконні елементи управління
Other thread – Інший потік
Window with controls – Вікно з елементами управління
Рис. 5. Безпечна багатопоточність

Спрощена багатопоточність

Виклик Invoke не дуже зручний, а оскільки він двічі зустрічається в
функції CalcPi, ми можемо полегшити собі життя і змінити ShowProgress,
щоб вона сама виконувала асинхронний виклик. Якщо ShowProgress викликається
з коректного потоку, вона оновлює елементи управління, в іншому випадку
вона використовує Invoke для виклику самої себе в потрібному потоці. Повернемося до
попередньої, більш простої версії CalcPi:

void ShowProgress(string pi, int totalDigits, int digitsSoFar) {
/ / Переконаємося, що ми в коректному потоці
  if( _pi.InvokeRequired == false ) {
    _pi.Text = pi;
    _piProgress.Maximum = totalDigits;
    _piProgress.Value = digitsSoFar;
  }
  else {
/ / Показувати хід виконання операції асинхронно
    ShowProgressDelegate showProgress =
      new ShowProgressDelegate(ShowProgress);
    this.Invoke(showProgress,
      new object[] { pi, totalDigits, digitsSoFar});
  }
}

void CalcPi(int digits) {
StringBuilder pi = new StringBuilder ("3", digits + 2);

/ / Показати хід виконання
  ShowProgress(pi.ToString(), digits, 0);

  if( digits > 0 ) {
    pi.Append(".");

    for( int i = 0; i < digits; i += 9 ) {
      ...
/ / Показати хід виконання
      ShowProgress(pi.ToString(), digits, i + digitCount);
    }
  }
}

Так як виклик Invoke – синхронний і нам не потрібно його повертається
значення (адже ShowProgress не повертає значення), тут краще
використовувати BeginInvoke, щоб робочий потік не завис:

BeginInvoke (showProgress, new object [] {pi, totalDigits, digitsSoFar});

BeginInvoke завжди краще, якщо повертається призначення не
потрібно, оскільки при використанні цього методу робочий потік відразу
ж повертається до своєї роботи, що виключає ймовірність взаємної
блокування.

Висновок

У цьому короткому прикладі я показав, як виконувати тривалі операції
так, щоб можна було відображати хід їх виконання, а UI продовжував
реагувати на дії користувача. З цією метою я використовував
асинхронний делегат для запуску робочого потоку і метод Invoke для
головної форми. При цьому ще один делегат виконувався в UI-потоці.

Я стежив, щоб до даних не було одночасного доступу з робочого і
UI-потоків. Замість цього я передавав кожному потоку копію потрібних йому
даних (в робочий потік – число знаків, а в UI-потік – кількість
знаків, обчислених на даний момент). У підсумку я ніколи не передавав
посилання на об'єкти, що розділяються двома потоками, наприклад на поточний
StringBuilder. Якщо б я передавав посилання, мені довелося б задіяти
. NET-засоби синхронізації, щоб виключити ймовірність звернення двох
потоків до одного об'єкту одночасно, а це вимагало б
додаткових зусиль. Мені й без того довелося проробити масу роботи,
щоб два потоки могли викликати один одного.

Звичайно, якщо ви маєте справу з великими наборами даних, ви навряд чи
захочете їх копіювати. Проте скрізь, де це можливо, я раджу для
реалізації тривалих операцій в додатках Windows Forms поєднувати
асинхронні делегати і передачу повідомлень між робочим і UI-потоками.

Подяки

Хотів би подякувати Саймона Робінсона (Simon Robinson) за його
повідомлення в списку розсилки DevelopMentor. NET, надихнув мене на
написання цієї статті, Йена Гріффітса (Ian Griffiths) за його початкові
напрацювання у цій галузі і Майка Вудрінг (Mike Woodring) за знамениті
картинки зі схемами підтримки декількох потоків, які я без докорів
совісті поцупив у нього для своєї статті.

Посилання

Кріс Селлз (Chris Sells)
– Незалежний консультант і викладач в DevelopMentor.
Спеціалізується на розподілених додатках ст. NET і COM. Автор
кількох книг, у тому числі "ATL Internals", яка в даний час
переробляється для обліку змін, що з'явилися в ATL7. Крім того,
працює над книгами "Essential Windows Forms" (Addison-Wesley) і
"Mastering Visual Studio. NET" (O? Reilly). У вільний час Кріс
підтримує Web-сервіси DevCon і керує Genghis – проектом з
відкритим вихідним кодом. Більш докладну інформацію про нього і про його
численних проектах див. на сайті
http://www.sellsbrothers.com.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*