XPCOM:nsIThreadManager

From MozillaWiki
Jump to: navigation, search

In a world without nested event queues, the concept of a thread and an event queue merge...

Interfaces

nsIThreadManager

[scriptable, uuid(...)]
interface nsIThreadManager : nsISupports {
  /**
   * Create a new thread (a global, user PRThread).  Currently, flags is
   * an unused parameter, that must be 0.
   */
  nsIThread newThread(in unsigned long flags);
   
  /**
   * Get the nsIThread object (if any) corresponding to the given PRThread.
   * This method returns null if there is no corresponding nsIThread.
   */
  [noscript] nsIThread getThreadFromPRThread(in PRThread thread);
   
  /**
   * Get the main thread.
   */
  readonly attribute nsIThread mainThread;
   
  /**
   * Get the current thread.
   */
  readonly attribute nsIThread currentThread;
   
  /**
   * This attribute is true if the calling thread is the main thread of the
   * application process.
   */
  readonly attribute boolean isMainThread;
};

nsIEventTarget

[scriptable, uuid(...)]
interface nsIEventTarget : nsISupports {
  /**
   * Dispatch an event to the target.  This function may be called from any
   * thread.  If flags specifies DISPATCH_SYNC, then the dispatch method
   * will not return until the event has been processed.  NOTE: Calling 
   * dispatch with DISPATCH_SYNC, may have the side-effect of running other 
   * events on the current thread while waiting for the given event to run
   * to completion.  This function is thread-safe and re-entrant.
   */
  void dispatch(in nsIRunnable event, in unsigned long flags);
  const unsigned long DISPATCH_NORMAL = 0;
  const unsigned long DISPATCH_SYNC   = 1;
   
  /**
   * Returns true if events dispatched to this target will run on the
   * current thread (i.e., the thread calling this method).
   */
  boolean isOnCurrentThread();
};
 
%{C++
// convenient aliases:
#define NS_DISPATCH_NORMAL nsIEventTarget::DISPATCH_NORMAL
#define NS_DISPATCH_SYNC   nsIEventTarget::DISPATCH_SYNC
%}

nsIThread

[scriptable, uuid(...)]
interface nsIThread : nsIEventTarget {
  /**
   * Returns the PRThread object corresponding to this nsIThread.
   */
  [noscript] readonly attribute PRThread PRThread;
   
  /**
   * Shutdown the thread.  This method may not be executed from the thread
   * itself.  Instead, it is meant to be executed from another thread (usually
   * the thread that created this thread).  When this function returns, the
   * thread will be shutdown, and it will no longer be possible to dispatch
   * events to the thread.
   */
  void shutdown();
   
  /**
   * Returns true if this thread has one or more pending events.
   */
  boolean hasPendingEvents();
   
  /**
   * Process the next event.  If there are no pending events, then this
   * method may wait -- provided mayWait is true -- until an event is 
   * dispatched to this thread.  This method is re-entrant but may only be
   * called if this thread is the current thread.
   *
   * @return A boolean value that is "true" if an event was processed.
   */
  boolean processNextEvent(in boolean mayWait);
};

nsIThreadInternal

[scriptable, uuid(...)]
interface nsIThreadInternal : nsIThread {
  /**
   * Get/set an observer for this thread (may be null).  This attribute may
   * be read from any thread, but must only be set on the thread corresponding
   * to this thread object.
   */
  attribute nsIThreadObserver observer;
   
  /**
   * This method causes any events currently enqueued on the thread to be
   * suppressed until PopEventQueue is called.  Additionally, any new events
   * dispatched to the thread will only be processed if they are accepted by
   * the given filter.  If the filter is null, then new events are accepted.
   * Calls to PushEventQueue may be nested and must each be paired with a call
   * to PopEventQueue in order to restore the original state of the thread.
   */
  void pushEventQueue(in nsIThreadEventFilter filter);
   
  /**
   * Revert a call to PushEventQueue.  When an event queue is popped, any
   * events remaining in the queue are appended to the elder queue.
   */
  void popEventQueue();
};

nsIThreadObserver

[scriptable, uuid(...)]
interface nsIThreadObserver : nsISupports {
  /**
   * This method is called after an event has been dispatched to the thread.
   * This method may be called from any thread.
   */
  void onDispatchedEvent(in nsIThreadInternal thread);
   
  /**
   * This method is called (from nsIThread::ProcessNextEvent) before an event
   * is processed.  This method is only called on the target thread.
   *
   * @param thread
   *        The thread being asked to process another event.
   * @param mayWait
   *        Indicates whether or not the method is allowed to block the calling
   *        thread.  For example, this parameter is false during thread shutdown.
   * @param recursionDepth
   *        Indicates the number of calls to ProcessNextEvent on the call stack
   *        in addition to the current call.
   */
  void onProcessNextEvent(in nsIThreadInternal thread, in boolean mayWait,
                          in unsigned long recursionDepth);
};

nsIThreadEventFilter

[scriptable, uuid(...)]
interface nsIThreadEventFilter : nsISupports {
  /**
   * This method is called to determine whether or not an event may be accepted
   * by a "nested" event queue (see nsIThreadInternal::PushEventQueue).
   *
   * WARNING: This method must not make any calls on the thread object.
   */
  [notxpcom] boolean AcceptEvent(in nsIRunnable event);
};

nsIThreadPool

[scriptable, uuid(...)]
interface nsIThreadPool : nsIEventTarget
{
  /**
   * Shutdown the thread pool.  This method may not be executed from any
   * thread in the thread pool.  Instead, it is meant to be executed from
   * another thread (usually the thread that created this thread pool).
   * When this function returns, the thread pool and all of its threads will
   * be shutdown, and it will no longer be possible to dispatch tasks to the
   * thread pool.
   */
  void shutdown()
   
  /**
   * Get/set the maximum number of threads allowed at one time in this pool.
   */
  attribute unsigned long threadLimit;
   
  /**
   * Get/set the maximum number of idle threads kept alive.
   */
  attribute unsigned long idleThreadLimit;
   
  /**
   * Get/set the amount of time in milliseconds before an idle thread is
   * destroyed.
   */
  attribute unsigned long idleThreadTimeout;
};

C++ Utilities

Functions

nsThreadUtils.h defines the following helper functions:

/**
 * Create a new thread.
 *
 * @param result
 *        The resulting nsIThread object.
 * @param event
 *        The initial event to run on this thread.  This can be null.
 */
NS_METHOD NS_NewThread(nsIThread **result, nsIRunnable *event);
 
/**
 * Get a reference to the current thread.
 *
 * @param result
 *        The resulting nsIThread object.
 */
NS_METHOD NS_GetCurrentThread(nsIThread **result);
 
/**
 * Get a reference to the main thread.
 *
 * @param result
 *        The resulting nsIThread object.
 */
NS_METHOD NS_GetMainThread(nsIThread **result);
 
/**
 * Test to see if the current thread is the main thread.
 *
 * @returns PR_TRUE if the current thread is the main thread, and PR_FALSE
 * otherwise.
 */
PRBool NS_IsMainThread();
 
/**
 * Dispatch the given event to the current thread.
 *
 * @param event
 *        The event to dispatch.
 */
NS_METHOD NS_DispatchToCurrentThread(nsIRunnable *event);
 
/**
 * Dispatch the given event to the main thread.
 *
 * @param event
 *        The event to dispatch.
 * @param dispatchFlags
 *        The flags to pass to the main thread's dispatch method.
 */
NS_METHOD NS_DispatchToMainThread(nsIRunnable *event,
                                  PRUint32 dispatchFlags = NS_DISPATCH_NORMAL);
 
/**
 * Process all pending events for the given thread before returning.  This
 * method simply calls ProcessNextEvent on the thread while HasPendingEvents
 * continues to return true and the time spent in NS_ProcessPendingEvents
 * does not exceed the given timeout value.
 *
 * @param thread
 *        The thread object for which to process pending events.  If null,
 *        then events will be processed for the current thread.
 * @param timeout
 *        The maximum number of milliseconds to spend processing pending
 *        events.  Events are not pre-empted to honor this timeout.  Rather,
 *        the timeout value is simply used to determine whether or not to
 *        process another event.  Pass PR_INTERVAL_NO_TIMEOUT to specify no
 *        timeout.
 */
NS_METHOD NS_ProcessPendingEvents(nsIThread *thread = nsnull,
                                  PRIntervalTime timeout = PR_INTERVAL_NO_TIMEOUT);

nsCOMPtr helpers

The following nsCOMPtr helpers are defined to simplify things further:

already_AddRefed<nsIThread> do_GetCurrentThread();
 
already_AddRefed<nsIThread> do_GetMainThread();

nsRunnable

The class nsRunnable is also defined to simplify the implementation of new event types. For example, to define a new event all you need to do is write code like this:

class MyEvent : public nsRunnable {
public:
  MyEvent(... params ...);
   
  NS_IMETHOD Run() {
    // do stuff
    return NS_OK;
  }
};

Dispatching MyEvent is then as simple as:

nsCOMPtr<nsIRunnable> event = new MyEvent();
rv = NS_DispatchToCurrentThread(event);
NS_ENSURE_SUCCESS(rv, rv);

From JS

Event Dispatch

Threads and Thread Manager implement nsIClassInfo:

function MyEvent() {
}
MyEvent.prototype = {
  QueryInterface: function(iid) {
    if (iid.equals(Components.interfaces.nsIRunnable) ||
        iid.equals(Components.interfaces.nsISupports))
      return this;
    throw Components.results.NS_ERROR_NO_INTERFACE;
  },
  run: function() {
    // do stuff
  }
};
 
function DispatchMyEvent() {
  var target = 
      Components.classes["@mozilla.org/thread-manager;1"].
      getService().currentThread;
   
  target.dispatch(new MyEvent(), target.DISPATCH_NORMAL);
}

Remarks

  • Thread priority for native threads can be exposed via nsISupportsPriority.
  • Thread names must be unique.
  • The Necko I/O thread pool will be replaced by a generic thread pool implementation provided by XPCOM. The thread pool will implement nsIEventTarget, allowing events to be dispatched to any thread in the thread pool. We may even wish to define a nsIThreadPool interface and have it implement that as well. That interface would define methods to adjust the limits on the number of threads in the thread pool and so on.
  • We will need an alternative for nsIEventQueue::RevokeEvents. There are several possibilities: (1) make consumers hold references to their nsIRunnable's so they can "disconnect" them manually, or (2) expose an enumeration/visitor API that allows consumers to walk the list of pending tasks and remove arbitrary tasks. I'm not sure what is best yet. The first option or something similar based on weak references is probably best as it avoids the costly O(n) RevokeEvents call.
  • The following interfaces/classes would go away or be modified heavily: nsIEventQueue, nsIEventQueueService, nsIEventTarget, nsIThread, PLEvent.
  • Issue: Perhaps we should support calling nsIThread::Shutdown from the thread itself. That would probably have to be implemented by posting an event to the main thread, and having that thread call shutdown. However, Shutdown also has the property that it does not return until the thread is gone. That obvious can't happen if Shutdown is called on the thread itself.

What about "native" event queues?

The main thread where UI events are processed and the socket transport thread will use the observer interface defined by nsIThreadInternal. The main thread will interact with the "native" event system during calls to ProcessNextEvent and Dispatch. Likewise, the socket transport thread will similarly need to poll sockets and perform I/O operations. By observing calls to ProcessNextEvent, observers can wait on a native event queue (or poll on a set of file descriptors) instead of waiting on the thread's internal event queue monitor. When they observe a call to Dispatch, they can use whatever mechanism is appropriate to unblock the thread that is waiting on the native event queue.

How do synchronous events work?

When Dispatch(event, DISPATCH_SYNC) is called on a nsIEventTarget, it first checks to see if the current thread is the same as the thread where the event would be run. If it is, then it just runs the event directly without queuing it up. However, if not, then it must queue the event to be run when the dispatch target next processes its events. To avoid returning to the caller immediately, the method gets the current thread and calls its ProcessNextEvent method in a loop until it receives acknowledgement that the queued event has been run.

Development

Development is occuring on the THREADS_20060307_BRANCH. See also bug 326273.

Comments

biesi:

  • What happens if shutdown is called while an event is being processed? I assume that this event will be processed to its end, no further events are accepted, and then the thread is shut down. right?
    • What happens to events that were already dispatched to this thread, but not run?
  • Is it a good idea to allow other events to run on the current thread while in dispatch?
  • Who is expected to call ProcessNextEvent?

darin:

  • When asked to shutdown, a thread would finish dispatching all queued events, and refuse to accept any new events. Once all queued events run to completion, the thread would terminate. This is consistent with how event queues work today.
  • If you are sync dispatching, then it is necessary to pump events on the calling thread or else you could end up in a dead-lock situation. For example, if thread A sync dispatches an event to thread B, which cannot complete its job without dispatching an event back to thread A, then the system will dead-lock unless thread A continues to dispatch pending events.
  • ProcessNextEvent may be called by anybody that wishes to implement the concept of a "modal event loop." For example, XMLHttpRequest (in sync configuration) would sit in a loop calling ProcessNextEvent while waiting for the underlying HTTP transaction to complete. The key difference between ProcessNextEvent and nsIEventQueue::ProcessPendingEvents is that ProcessNextEvent, when called on the UI thread, will process UI events as well. And, it will also wait (block the thread of execution) if there are no UI events or pending nsIRunnable events.

dbaron:

So mrbkap was telling me at lunch about what's essentially the third bullet point of your comment to biesi above -- that you're making the concept of "modal event loop" even more powerful. This actually scares me a good bit, because these modal event loops happen within another event -- an event that may have state on the stack (e.g., |this| parameters) that could be destroyed by the processing of further events, or may have posted events that rely on the event completing. I really don't like the mix of programming models here that makes it very hard to write robust code, and I wish we could avoid this whole concept altogether rather than making it even more powerful, and thus more dangerous. We already have crashes where frame trees get destroyed while processing an event that are related to the Windows API's way of doing the same thing, though (for, e.g., a DestroyWindow call or whatever it's called).

darin:

Yup, I'm aware of that issue. In fact, the problem already exists today. There are some places in the code where WaitPLEvent + HandleEvent are called in a loop waiting for an event to process. That bypasses PL_ProcessPendingEvents recursion restriction. PopThreadEventQueue does not behave as you would expect either. It can cause ProcessPendingEvents to run for all but the PLEventQueue that is the subject of the current call to PL_ProcessPendingEvents on the stack. That defeats the purpose of the nested event queue in the first place.

The problem you describe with layout events also occurs for nsIStreamListener implementations. It would be really wierd if OnDataAvailable were called recursively, or if OnDataAvailable were called while the consumer is calling nsIChannel::AsyncOpen. These specific problems are dealt with in nsBaseChannel.cpp by temporarily suspending the channel during callbacks. That suppresses recusive stream listener calls.

Do you have a suggestion that would allow us to prevent this problem? Is there a better way to make XMLHttpRequest.send, document.load, window.alert, etc. appear modal to the calling Javascript?

For C++ consumers, we'd have to basically eliminate modal dialogs altogether to avoid this problem. Today, we have bugs where downloads stall and get disconnected all because the browser popped up a modal dialog and the dialog was not closed promptly enough by the user. That's just unacceptable. And, nsIPromptService is frozen.

I think the answer is to be careful about making callbacks across XPCOM boundaries where event queues may be pumped. Maybe the layout code needs to unwind the stack more before allowing such callbacks to happen so that it can be sure that event processing won't be the end of the world.

dbaron: Also, with the new push/popEventQueue API defined on nsIThreadInternal, it is possible to suppress event dispatch for a particular scope. That is needed to implement synchronous XPCOM proxy calls. I'm assuming that any proper use of synchronous XPCOM proxy calls will be one that completes relatively quickly so as to negate the effects of locking up the browser. We should probably be very careful about calling PushEventQueue on the main thread, where it would potentially disrupt the browser UI. Native events would still be dispatched as always, but without application level events, the UI isn't very useful.