Assist the UI Dispatcher to handle a flood of method invocations

[亡魂溺海] 提交于 2019-12-02 01:29:05

问题


The following post has become a bit *longer* than expected I apologize for that but maybe you'll find it interesting to read and maybe you have an idea to help me :)

I am developing a small application whose GUI consists of a number of List controls. Each List control has a thread associated with which is permanently producing strings that are being added to the list.

To allow the List controls being updated by different threads I built an extended ObservableCollection that asynchronously invokes all its operations to the UI dispatcher which works pretty fine. Here is a code-snippet of that class to examplify the insert-operation:

public class ThreadSaveObservableCollection<T> : ObservableCollection<T> {

    private int _index;

    private Dispatcher _uiDispatcher;
    private ReaderWriterLock _rwLock;

    // ...

    private bool _insertRegFlag;

    new public void Insert (int index, T item) {

        if (Thread.CurrentThread == _uiDispatcher.Thread) {

            insert_(index, item);
        } else {

            if (_insertRegFlag) { }
            else {

                BufferedInvoker.RegisterMethod(_index + "." + (int)Methods.Insert);
                _insertRegFlag = true;
            }

            BufferedInvoker.AddInvocation(new Invocation<int, T> { Ident = _index + "." + (int)Methods.Insert, Dispatcher = _uiDispatcher, Priority = DispatcherPriority.Normal, Param1 = index, Param2 = item, Method = new Action<int, T>(insert_) });
        }
    }

    private void insert_ (int index, T item) {

        _rwLock.AcquireWriterLock(Timeout.Infinite);

        DateTime timeStampA = DateTime.Now;

        base.Insert(index, item);

        DateTime timeStampB = DateTime.Now;

        BufferedInvoker.Returned(_index + "." + (int)Methods.Insert, timeStampB.Subtract(timeStampA).TotalMilliseconds);

        _rwLock.ReleaseWriterLock();
    }

    // ...
}

To model the invocation in form of a kind of pending invocation-task I built the following:

public interface IInvocation {

    string Ident { get; set; }
    void Invoke ();
}

public struct Invocation : IInvocation {

    public string Ident { get; set; }
    public Dispatcher Dispatcher { get; set; }
    public DispatcherPriority Priority { get; set; }
    public Delegate Method { get; set; }

    public void Invoke () {

        Dispatcher.BeginInvoke(Method, Priority, new object[] { });
    }
}

My problem now is that because of the enormous amount of method calls I am invoking onto the UI Dispatcher (I have aprox. 8 to 10 threads which are permanently producing strings which they add to their Lists) my UI looses the ability to respond to user I/O (e.g. with the mouse) after aprox. 30 seconds until it doesn't accept any user interaction at all after about a minute.

To face this problem I wrote some kind of a buffered invoker that is responsible for buffering all method calls I want to invoke onto the UI dispatcher to then invoke them in a controlled way e.g. with some delay between the invocations to avoid flooding the UI dispatcher.

Here is some code to illustrate what I am doing (please see the description after the code-segment):

public static class BufferedInvoker {

    private static long _invoked;
    private static long _returned;
    private static long _pending;
    private static bool _isInbalanced;

    private static List<IInvocation> _workLoad;
    private static Queue<IInvocation> _queue;

    private static Thread _enqueuingThread;
    private static Thread _dequeuingThread;
    private static ManualResetEvent _terminateSignal;
    private static ManualResetEvent _enqueuSignal;
    private static ManualResetEvent _dequeueSignal;

    public static void AddInvocation (IInvocation invocation) {

        lock (_workLoad) {

            _workLoad.Add(invocation);
            _enqueuSignal.Set();
        }
    }

    private static void _enqueuing () {

        while (!_terminateSignal.WaitOne(0, false)) {

            if (_enqueuSignal.WaitOne()) {

                lock (_workLoad) {

                    lock (_queue) {

                        if (_workLoad.Count == 0 || _queue.Count == 20) {

                            _enqueuSignal.Reset();
                            continue;
                        }

                        IInvocation item = _workLoad[0];
                        _workLoad.RemoveAt(0);
                        _queue.Enqueue(item);

                        if (_queue.Count == 1) _dequeueSignal.Set();
                    }
                }
            }
        }
    }

    private static void _dequeuing () {

        while (!_terminateSignal.WaitOne(0, false)) {

            if (_dequeueSignal.WaitOne()) {

                lock (_queue) {

                    if (_queue.Count == 0) {

                        _dequeueSignal.Reset();
                        continue;
                    }

                    Thread.Sleep(delay);

                    IInvocation i = _queue.Dequeue();
                    i.Invoke();

                    _invoked++;
                    _waiting = _triggered - _invoked;
                }
            }
        }
    }

    public static void Returned (string ident, double duration) {

        _returned++;

        // ...
    }
}

The idea behind this BufferedInvoker is that the ObservableCollections don't invoke the operations on their own but instead call the AddInvocation method of the BufferedInvoker which puts the invocation-task into its _workload list. The BufferedInvoker then maintains two 'internal' threads that operate on a _queue - one thread takes invocations from the _workload list and puts them into the _queue and the other thread puts the invocations out of the _queue and finally invokes them one after the other.

So that's nothing else than two buffers to store pending invocation-tasks in order to delay their actual invocation. I am further counting the number of invocation-tasks that have been actually invoked by the _dequeuing thread (i.e. long _invoked) and the number of methods that have been returned from their execution (every method inside the ObservableCollection calls the Returned() method of the BufferedInvoker when it completes its execution - a number that is stored inside the _returned variable.

My idea was to get the number of pending invocations with (_invoked - _returned) to get a feeling of the workload of the UI dispatcher - but surprisingly _pending is always below 1 or 2.

So my problem now is that although I am delaying the invocations of methods to the UI dispatcher (with Thread.Sleep(delay)) the application starts to lag after some time which reflects the fact that the UI has too much to do as to handle user I/O.

But - and that is what I am really wondering - the _pending counter never reaches a high value, most of the time it's 0 even if the UI has already been frozen.

So I now have to find

(1) a way to measure the workload of the UI dispatcher to determine the point where the UI dispatcher is over-worked and

(2) do something against it.

So now Thank you very much for reading until this point and I hope you have any ideas how to invoke an arbitrary high number of methods onto the UI dispatcher without overwhelming it..

Thank you in advance ...emphasized text*emphasized text*


回答1:


Having taken a quick look I notice that you sleep with the lock held. This means that while sleeping no one can enqueue, rendering the queue useless.

The application does not lag because the queue is busy, but because the lock is held almost always.

I think you'll be better off removing all your manually implemented queues and locks and monitors and just use the built-in ConcurrentQueue. One queue per UI control and thread, and one timer per queue.

Anyway, here is what I propose:

ConcurrentQueue<Item> queue = new ...;

//timer pulls every 100ms or so
var timer = new Timer(_ => {
 var localItems = new List<Item>();
 while(queue.TryDequeue(...)) { localItems.Add(...); }
 if(localItems.Count != 0) { pushToUI(localItems); }
});

//producer pushes unlimited amounts
new Thread(() => { while(true) queue.Enqueue(...); });

Simple.



来源:https://stackoverflow.com/questions/11250947/assist-the-ui-dispatcher-to-handle-a-flood-of-method-invocations

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!