简体   繁体   中英

How to synchronize event handlers

Multithreading is still on my to-do list, so the title could be totally wrong :)

My object is listening to serial port, like this:

class MyClass
{
    MyOpticalScanner _scanner;

    public MyClass()
    {
        _scanner = new MyOpticalScanner();
        _scanner.CodeScanned += CodeScannedEventHandler;
    }

    private void CodeScannedEventHandler(object sender, CodeScannedEventArgs e)
    {
        Debug.WriteLine("ThreadID: " + Thread.CurrentThread.ManagedThreadId + " ||| " + e.ScannedCode);
        ....
        // Some code, query the database, etc...
    }
}

The scanner is sending ordered data: 001 , 002 , 003 , 004 , 005 , ... But if the code in the CodeScannedEventHandler takes too long to process, then another event is raised and I got inconsistent ordering. The Debug.WriteLine in event handler could give me this:

ThreadID: 8 ||| 001
ThreadID: 9 ||| 002
ThreadID: 10 ||| 003
ThreadID: 10 ||| 006
ThreadID: 8 ||| 004
ThreadID: 8 ||| 008
ThreadID: 8 ||| 009
ThreadID: 8 ||| 010
ThreadID: 10 ||| 007
ThreadID: 9 ||| 005

How can I assure, that each new event starts to process only when the old one is finished?

Edit 1 - I didn't told you everything, actualy I am not listening to COM port during the testing, instead I created my own mock object. This object (ScannerMock) uses an internal System.Timer and raises the CodeScanned event on Timer.OnTick event. Could the problem be here?

From the comment in Hans Passant's answer : SerialPort has an internal lock that ensures that the DataReceived event cannot be called again while it is running. Should I include similar lock in my mocking scanner and how?

Edit 2: I added a lock to my scanner object and put the code, that raises my event, into it. It looks like it works :)

You've got a pretty big problem if CodeScannedEventHandler runs again before the previous one has finished running. SerialPort does not do this btw, its DataReceived event is serialized. A lock cannot reliably solve this problem, the order in which threads acquire the lock isn't guaranteed. Which is what you see happening, there's a lock inside Debug.WriteLine().

If the sequence is really important then all you can do is keep the event handler as short and snappy as possible so it always takes less time than the rate at which the events run. Quickly store the scan result in a thread-safe queue and get out. You need another thread that empties the queue. It is still not a 100% guarantee, you'll need help from whomever wrote MyOpticalScanner to get that guarantee.

How can I assure, that each new event starts to process only when the old one is finished?

If you do that, you risk loosing incoming data, and/or overflowing receive buffers.

One approach might be to push incoming data into a Queue (quickly), and process that queue in another thread. If there is some time between the Codes that will work.

But if the consumer can't keep up you're still in trouble.

I would suggest that you use a thread-safe queue class which reports, when adding an item, whether another item had been added since the queue last reported that it was empty (wrapping an ordinary queue in a lock and adding an 'empty' flag--within the lock--should suffice). Any time a communications record arrives, add it to the queue and, if nothing had been added since the last 'empty' report, dispatch a MethodInvoker to read out and process everything in the queue and exit once the queue is empty (that method could repeatedly call a 'one record received' event).

If an record is being processed while another one arrives, the queue will report that at least one record had been added since the last time the queue reported itself as empty, so no new MethodInvoker will be dispatched. Any record which gets enqueued before the queue reports itself empty will be handled by the earlier MethodInvoker; any record which gets enqueued after the MethodInvoker finds the queue is empty will not get handled by that MethodInvoker, and will require the launch of another one.

How can I assure, that each new event starts to process only when the old one is finished?

For example, by using the lock statement:

class MyClass {
    ...
    Object myLock = new Object();
    ...

    private void CodeScannedEventHandler(object sender, CodeScannedEventArgs e)
    {
        lock(myLock) {
            ...
        }
    }
}

Note, however, that this might not solve your problem: This solution will prevent two instances of your event handler running in parallel. It will not guarantee that the threads which are currently waiting get released in the "correct" order.

If your scanner component supports this, then the easiest solution would be to configure that component to always call the event handler on the same thread.

This is caused because

If you're scanning in more then one thread this will happen of course.

You should synchronize the CodeScannedEventHandler method.

You can use lock for example.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM