FLTK logo

STR #1536

FLTK matrix user chat room
(using Element browser app)   FLTK gitter user chat room   GitHub FLTK Project   FLTK News RSS Feed  
  FLTK Apps      FLTK Library      Forums      Links     Login 
 Home  |  Articles & FAQs  |  Bugs & Features  |  Documentation  |  Download  |  Screenshots  ]
 

Return to Bugs & Features | Roadmap 1.1 | SVN ⇄ GIT ]

STR #1536

Application:FLTK Library
Status:1 - Closed w/Resolution
Priority:2 - Low, e.g. a documentation error or undocumented side-effect
Scope:3 - Applies to all machines and operating systems
Subsystem:Core Library
Summary:a thread-related helper
Version:1.1-current
Created By:wavexx
Assigned To:mike
Fix Version:1.1-current (SVN: v5683)
Update Notification:

Receive EMails Don't Receive EMails

Trouble Report Files:


Name/Time/Date Filename/Size  
 
#1 wavexx
14:17 Dec 12, 2006
recall.patch
3k
 
 
#2 wavexx
11:41 Feb 11, 2007
awake.patch
2k
 
     

Trouble Report Comments:


Name/Time/Date Text  
 
#1 wavexx
14:17 Dec 12, 2006
I've been using a continuation-like mechanism to ease asynchronous message dispatching to the main thread to circumvent FLTK threading limitations (and more) for some time.

After #1532 I think this could be a very important addition to the FLTK awake() for more complicated code.

Attached patch adds a Fl::recall() method and documentation, which allows any thread to schedule for execution the specified handler in the main thread.

I took the time to simplify my original implementation (originally based on pipes) and integrate it into the current threading implementation. Incidentally, that's how I discovered STR #1534.
 
 
#2 matt
07:25 Jan 21, 2007
I like the idea and the implementation sems good enough. It's missing the rc_chek() newr the main look somewhere, but that's easy enough.

My suggestion would be to change the name though. I have a different association with "recall" (apart from Schwarzenegger in a female fat suit in "Total Recall"). Instead of "recall(my_function, 10)", how about "delegate()", "pass()", "bequeath()", or "relay()", or simply "call()"? I feel that the fact the task will be done later by someone else should somehow b reflected.


Mike? Anyone who speaks better English than I do?
 
 
#3 wavexx
07:47 Jan 21, 2007
I don't understand the first sentence.
If it's about rc_check execution, it's added as a "check" (see the add_check line). Apart from the STR 1532 fix, it also requires the changes I proposed in STR 1537.

I already tested this implementation on OSX, WIN32 and Linux.

About the name, I used "recall" as in "call again". To extend the current naming for main-loop helpers, maybe "add_call" or "add_handler", or "call_handler" or "awake_handler" (which renders the purpose of the action).
 
 
#4 matt
08:00 Jan 21, 2007
Damn, I hate when the mouspad unintentionally moves the text cursor and scrambles my words ;-) . But you guessed it right. Thanks.

How about "awake_and_call(some_function)"?
 
 
#5 wavexx
08:25 Jan 21, 2007
Overlong :). I still prefer awake_handler.  
 
#6 mike
12:22 Jan 28, 2007
Hmm, my inclination is to not provide a full RPC mechanism in FLTK, but perhaps we can just add a callback hook for the awake() message processing.

Right now you have to poll Fl::thread_message(), but if we added a Fl::awake_cb(Fl_Timeout_Handler) method then you can do:

    Fl::lock();
    Fl::awake_cb(my_function);
    Fl::awake(my_data);
    Fl::unlock();

to do simple RPC stuff with FLTK.  Anything more complicated will just need to either a) use your current implementation or b) pass a structure containing a message type or function pointer that your callback handles. On the thread side you'll probably want to allocate the data you pass, and delete it from the callback.
 
 
#7 wavexx
13:32 Jan 28, 2007
Consider:

 - fltk unlocks
 - worker 1: lock, awake, unlock
 - worker 2: lock, awake, unlock
 - fltk lock

worker 1 loses it's data. That's the main issue with the current awake() being limited to just one slot.

Since you need both 1) an action to perform, and 2) the data associated with it, I consider the proposal the bare minimum that a threaded application needs to implement _anyway_ to perform the example described.

Adding the possibility to just call an handler, IMHO, is almost useless. If we go that way I'm against it: better leave the current API unchanged.
 
 
#8 mike
14:25 Jan 28, 2007
I think you are missing a key part of the pipe-based implementation - the main thread can support multiple messages, but the issue is that the current functionality doesn't guarantee you will be able to process each message - it really depends on how you use Fl::wait() or Fl::check().

By adding a callback, the main thread will run the callback function for every message (pointer) you send from a worker thread via Fl::awake(). The pipe will act as the queue, and the callback will make sure that the main thread processes all of the messages.

(default pipe buffer is 16k on most OS's, for 2-4k of queued messages depending on the size of a pointer)
 
 
#9 wavexx
02:03 Jan 29, 2007
I only rely on guaranteed behavior (STR 1576 is deeply connected).  
 
#10 mike
07:13 Jan 29, 2007
Your proposed changes do not provide guaranteed delivery - without a callback and without queuing you only get the most recent message pointer.

Using an unbounded array instead of the pipe to transport the messages could lead to runaway memory use, and if you limit the number of messages that can be queued you have the same issues as the pipe.  The in-memory message queue also requires a mutex (more overhead).
 
 
#11 wavexx
16:40 Jan 29, 2007
I'm replying here for both this and STR 1576, since they correlate.

So we agree on the docs, that's why changing awake() is necessary.

Some thoughts:

The minimum necessary to allow guaranteed delivery is just a guaranteed way to break the execution loop. awake() doesn't really need to carry a message because a message can be stored externally. lock()/unlock(), IMHO, can be eliminated completely. Allowing just _some_ functions to work correctly in threads creates more confusion. With a minimal RPC system, simple application can be kept reasonably simple, and complex application can arrange for better synchronization. It would also remove the burden of exposing mutexes and entering in discussions like "Multithreading advice (lock/unlock suggestions)" in fltk.general.

The patch in STR 1576 changes awake() (**on posix) to guarantee to break the execution loop and store at least the last message pointer. It is guaranteed to _break_ because the write will always notify the main thread (note that we only write if necessary). The write/pipe here is just done to break the main loop: there are probably much better ways to achieve this on OSX. I don't know on WIN32 (which we know is broken since it isn't guaranteed, but I don't know about WIN32 to suggest a fix there). The last "message" is stored just to be backward compatible according to the docs.

The patch in this STR indeed guarantees the delivery as long as awake() is guaranteed to break the main loop, because all queued events are then processed one by one in the main thread with a "check". As you see in the patch, the awake message is not used at all.

You are right about the memory limitation, however the function could just return a boolean to indicate the status. In the patch I cloned the semantics of add_check/add_handler which just abort when they fail. Questionable ok, but not really the point.

The overhead of the mutex is insignificant and much more lightweight than using a pipe as a notification mechanism. The pipe instead could be removed entirely if a better way to break the execution loop is found.
 
 
#12 mike
16:47 Jan 31, 2007
The only way to interrupt select() on X11 is to send a signal (poor performance and a bad thing to use in a library) or trigger it via a write.

On OSX, we may be able to send an event to the first shown window, but I'm not sure whether that is allowed from a client thread without further initialization in each thread - I need to do more research/testing on that.

Your "only write if needed" implementation still needs a mutex, adding overhead.  If you provide queuing of messages (required IMHO), you can reuse the queue mutex, but you are also using more memory (queue + memory used for pipe and mutex) and incurring more overhead than a simple write-based implementation (lock, manipulate queue and optionally write to pipe, unlock).

My basic premise is this: the OS already provides us with a low-overhead way to do atomic message queuing/IPC via pipes and FLTK already includes support for efficiently processing these messages in the main thread, why invent another more complicated method?
 
 
#13 wavexx
14:05 Feb 01, 2007
The problem I see is that awake() always carry a message, and queues it. There's the need for a simpler signaling construct only. If many threads are involved (so that more than one thread awake()s before FLTK takes control) the superfluous I/O and kernel times become noticeable. I'd like a pthread_cond_signal equivalent so to speak.

Adding another pointer to the buffer will also double I/O activity.

Unfortunately, apart from signals, I can't suggest a better way to break the select. However I'm curious to test wether a synchronous signal is actually slower than a full pipe (apart from the library implications).
 
 
#14 mike
09:43 Feb 02, 2007
I think other signaling mechanisms need to happen outside of FLTK.

Regular signals are very inefficient (lots of overhead to deliver and process) and only allow very limited actions in the signal handler. They are also process-based and so only would be useful in signaling the main thread of a process.

The pthread_cond_foo() APIs would require all of the FD monitoring to be done in separate thread(s) (the main thread would wait on the condition to occur and the monitoring thread(s) would set/signal the condition as needed).  The biggest issue with this approach is that we'd still need to do the FD callbacks from the main thread (otherwise they can't pop up windows, etc.), requiring global fd_set/pollfd data with some sort of synchronization mechanism between the main and FD monitoring threads or new monitoring threads on each call which has even more overhead...

My proposed changes would not add another pointer to the buffered messages; instead, a single pointer is passed (as now) to an (optional) awake message callback in the main thread.  Any more complex dispatch mechanism will need to be handled by that callback...

As for performance, since we always use select() or poll() on at least one file descriptor (the X display for X11 or the Apple event queue on OSX), using a file descriptor for Fl::awake() messages is the lowest overhead method of delivery - read() and write() are highly optimized, thread-safe, and atomic. No external synchronization is required, no forced context changes occur (as happens for signals), and no complex queuing/memory management is required.

Related to performance, it is important that the swake message processing does not have higher priority than the GUI event processing, otherwise the UI may become non-responsive...
 
 
#15 wavexx
14:57 Feb 02, 2007
I mostly agree, yet, for all threaded applications I've done, I needed a simple RPC system to do pretty trivial stuff like showing a window and updating views instead of locking directly. It feels really dirty to have partial thread safety and no simple RPC. If we implement it on top of FLTK, it should not increase the executable payload when not used.

I didn't mean to implement pthread_cond stuff, just make awake() behave like pthread_cond_signal as closely as possible: the signal is one thing (that you can't implement outside), and the data is your business only.

The write and fd can't be avoided (sadly). Changing awake would not fix anything. I'm convinced. At that point:

- I'd add a note in the docs to mention that awakes guarantees to break at least once if called from any thread, guarantees (relative) order, but does not guarantee delivery.
- I'd add an awake_foo() function that returns a boolean, indicating the write() status, so one can eventually take advantage of the system queue without resorting to extra code if this behavior is sufficient.

Your awake_cb proposal makes sense (I was fooled by the example), but I would change thread_awake_cb to drain the queue immediately so that all events are caught in a single Fl::wait() event. thread_message would then really return just the last message. To process all messages you would install an awake_cb handler. That would be a performance boost. Otherwise it's the same as adding a 'check'.
 
 
#16 wavexx
15:05 Feb 02, 2007
I forgot about WIN32....

awake() _really_ needs to send at least one event to break the loop here even when the system queue is already full. We should find a way to ensure that.

MSDN says that any event is just discarded when the input queue is full (so.. lame..), so, maybe, somehow, there's a way to ensure there's always space for at least one more event reserved for awake().
 
 
#17 mike
15:05 Feb 08, 2007
If the system queue (or pipe) is full, the message sent via awake() will be lost but the main loop will still break to process the (previous) messages...

Anyways, I've implemented the Fl::set_awake_cb() API so that thread messages can be handled, and documented that Fl::awake() queues "at least several thousand" messages.  Let me know if you run into any problems with it...
 
 
#18 wavexx
11:41 Feb 11, 2007
Yes, because on posix the queue is independent and >0.
I would not mention the queue size at all in the docs, but that's ok.
As I said in the 17:57 Feb 2 message, the return status should be exposed to the user and thread_awake_cb should drain the queue immediately to avoid useless round-trips.

Attaching the changes I'm using now.

WIN32 still needs to be fixed though.
 
 
#19 mike
12:12 Feb 11, 2007
Since there isn't anything we can do about a failed send of an awake() message, and since we advertise that Fl::awake() is not a guaranteed delivery method, I'm not sure what good adding a new function will do.

As for WIN32 being "broken", it is no more broken than the UNIX pipe stuff - the WIN32 path is just using the Windows event queue to multiplex awake messages with UI events.

As for making the recieve side of the pipe non-blocking, that is not necessary since we only read from the pipe when select() says we have data, and we will *not* be processing all thread messages in one go for reasons I've said before.

If you want a guaranteed, real-time messaging API, you'll need to layer that on top of FLTK - we will not be including it in the core library.
 
 
#20 mike
12:12 Feb 11, 2007
Fixed in Subversion repository.  
     

Return to Bugs & Features ]

 
 

Comments are owned by the poster. All other content is copyright 1998-2024 by Bill Spitzak and others. This project is hosted by The FLTK Team. Please report site problems to 'erco@seriss.com'.