Долгосрочная задача в потоке пользовательского интерфейса - PullRequest
1 голос
/ 28 января 2020

У меня есть длительная задача, которая манипулирует NSMutableAttributedString. Эта манипуляция включает в себя проверку шрифтов, таких как:

    bold = (pfont.FontDescriptor.SymbolicTraits & NSFontSymbolicTraits.BoldTrait) != 0;

и создание шрифтов для настройки текста, таких как:

    var fontmgr = NSFontManager.SharedFontManager;
    font = fontmgr.FontWithFamily(fontname,
                                          (bold ? NSFontTraitMask.Bold : 0) |
                                          (italic ? NSFontTraitMask.Italic : 0),
                                          5, size);

Я не хочу останавливать поток пользовательского интерфейса, пока это происходит, поэтому я надеюсь выполнить эту манипуляцию в фоновом потоке. Обратите внимание, что я не касаюсь любых элементов пользовательского интерфейса во время этой обработки - просто изменяю NSAttributedString.

Вы можете сделать некоторые манипуляции с NSAttributedString в фоновом потоке, но это кажется, что касание шрифтов является верботеном, так как вызывает AppKitThreadAccessException.

Поэтому мне нужно запустить мои манипуляции в потоке пользовательского интерфейса, и я ищу способ получить куски процессорного времени. В идеале был бы способ периодически возвращать sh контроль, как в старые добрые времена совместной многозадачности - что-то вроде этого:

    var paragraphs = myBigLongAttributedString.BreakIntoParagraphs(); // this is fast.
    foreach (var paragraph in paragraphs) {
        // do some work on the paragraph
        relinquish control
    }

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

Большое спасибо за любые идеи.

Приветствия.

Пол

Ответы [ 3 ]

2 голосов
/ 28 января 2020

Есть много способов, которые могут это реализовать, например NSThread , NSOperation / NSOperationQueue классы или GCD .

Например (используя GCD)

using CoreFoundation;
DispatchQueue.GetGlobalQueue(DispatchQueuePriority.Default).DispatchAsync(() =>
{
  DispatchQueue.MainQueue.DispatchAsync(() => { 

     //do something you want

  });
});
0 голосов
/ 31 января 2020

ОК - еще одна версия кода. На этот раз с правильной блокировкой и использованием ManualResetEvents, а не Task.Delay (тестирование показывает, что это на 20-40% точнее).

/// <summary>
    /// Dispatch work items in the main UI thread while ensuring that the UI
    /// is not starved of processing time.
    /// </summary>
    public static class MainDispatcher
    {
        static ConcurrentQueue<Action> workList = new ConcurrentQueue<Action>();
        static bool active = false;
        static object activeLock = new object();

        public static void RunAsync(Action action)
        {
            workList.Enqueue(() =>
            {
                //  Run the requested code, then request the next one (in a task, so the UI thread is not used)
                action(); _ = Task.Run(Next);
            });

            if (!active)
                Activate();
        }
        static void Activate()
        {
            //  We need a separate flag for starting the runner if active was false. We CANT use
            //  active, because it could change between the end of the lock and the Task.Run.
            //  We can't extend the lock because we cannot be locked when we start the task runner
            //  (that could cause a deadlock)
            bool start_runner = false;
            lock (activeLock) 
            {
                if (!active)
                {
                    active = true;
                    start_runner = true;   
                }
            }

            if (start_runner)
                Task.Run(Next);
        }
        public static void Clear()
        {
            workList.Clear();
        }

        static void Next()
        {
            Action p = null;
            //  Delay to ensure UI thread can process UI events
            //  Note that WaitOne on a manualresetevent is more accurate than Task.Delay(1) by about 20%-40%.
            new System.Threading.ManualResetEvent(false).WaitOne(1);
            //Task.Delay(1).Wait();   
            lock(activeLock)
            {
                active = workList.TryDequeue(out p);
                if (!active)
                    p = null;
            }
            try
            {
                if (p != null)
                    DispatchQueue.MainQueue.DispatchAsync(p);
            }
            catch { }
        }
    }
0 голосов
/ 30 января 2020

Как я уже говорил выше ... использование DispatchQueue для выполнения какой-либо обработки на самом деле не работает, потому что DispatchQueue, по-видимому, лишает UI возможности, если вы потратите на него много работы.

Более подробно: я могу разбить мою обработку до нескольких тысяч очень маленьких рабочих элементов (каждый занимает 1-2 мс) Выполнение их через:

DispatchQueue.MainQueue.DispatchAsync(() => ProcessParagraph(para));

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

Основа c Идея состоит в том, чтобы обеспечить паузу в 1 мс между рабочими элементами. .

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

    /// <summary>
    /// Dispatch work items in the main UI thread while ensuring that the UI
    /// is not starved of processing time.
    /// </summary>
    public static class MainDispatcher
    {
        static ConcurrentQueue<Action> workList = new ConcurrentQueue<Action>();
        static bool active = false;

        public static void RunAsync(Action action)
        {
            workList.Enqueue(async () =>
            {
                action(); await Task.Delay(1); Task.Run(Next);
            });
            if (!active)
                Activate();
        }
        static void Activate()
        {
            active = true;
            Task.Run(Next());
        }
        public static void Clear()
        {
            workList.Clear();
        }

        static void Next()
        {
            Action p;
            if (workList.TryDequeue(out p))
                try
                {
                    DispatchQueue.MainQueue.DispatchSync(p);
                }
                catch { }
            else
                active = false;
        }
    }

Пара замечаний:

  • Я ценю, что должен пометить тест и установить флаг active - но для меня это не требуется, так как я прокачиваю только рабочие элементы в очередь из одного потока.
  • Я использую DispatchSyn c, поэтому я могу гарантировать задержку не менее 1 мс между выполнением рабочих элементов.
  • Обработка в целом медленнее, но пользовательский интерфейс остается жидким, так что это достойный компромисс. Я, вероятно, мог бы поэкспериментировать с уменьшенными задержками, но сейчас это не нужно.

Спасибо Лукасу за указатели, которые помогли мне.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...