3 live555 source code analysis (3) - live555 task scheduling

1. Task Scheduler

The TaskScheduler class is the basic abstract class of task scheduling. live555 divides tasks into three types, namely delayed tasks, event tasks and background IO tasks. The scheduling functions of the three tasks are defined in this class.
The TaskScheduler class is defined in the same file as the UsageEnvironment UsageEnvironment/include/UsageEnvironment.h:

class TaskScheduler {
    
    
public:
  virtual ~TaskScheduler();

  virtual TaskToken scheduleDelayedTask(int64_t microseconds, TaskFunc* proc,
					void* clientData) = 0;
	// Schedules a task to occur (after a delay) when we next
	// reach a scheduling point.
	// (Does not delay if "microseconds" <= 0)
	// Returns a token that can be used in a subsequent call to
	// unscheduleDelayedTask() or rescheduleDelayedTask()
        // (but only if the task has not yet occurred).

  virtual void unscheduleDelayedTask(TaskToken& prevTask) = 0;
	// (Has no effect if "prevTask" == NULL)
        // Sets "prevTask" to NULL afterwards.
        // Note: This MUST NOT be called if the scheduled task has already occurred.

  virtual void rescheduleDelayedTask(TaskToken& task,
				     int64_t microseconds, TaskFunc* proc,
				     void* clientData);
        // Combines "unscheduleDelayedTask()" with "scheduleDelayedTask()"
        // (setting "task" to the new task token).
        // Note: This MUST NOT be called if the scheduled task has already occurred.

  // For handling socket operations in the background (from the event loop):
  typedef void BackgroundHandlerProc(void* clientData, int mask);
    // Possible bits to set in "mask".  (These are deliberately defined
    // the same as those in Tcl, to make a Tcl-based subclass easy.)
    #define SOCKET_READABLE    (1<<1)
    #define SOCKET_WRITABLE    (1<<2)
    #define SOCKET_EXCEPTION   (1<<3)
  virtual void setBackgroundHandling(int socketNum, int conditionSet, BackgroundHandlerProc* handlerProc, void* clientData) = 0;
  void disableBackgroundHandling(int socketNum) {
    
     setBackgroundHandling(socketNum, 0, NULL, NULL); }
  virtual void moveSocketHandling(int oldSocketNum, int newSocketNum) = 0;
        // Changes any socket handling for "oldSocketNum" so that occurs with "newSocketNum" instead.

  virtual void doEventLoop(char volatile* watchVariable = NULL) = 0;
      // Causes further execution to take place within the event loop.
      // Delayed tasks, background I/O handling, and other events are handled, sequentially (as a single thread of control).
      // (If "watchVariable" is not NULL, then we return from this routine when *watchVariable != 0)

  virtual EventTriggerId createEventTrigger(TaskFunc* eventHandlerProc) = 0;
      // Creates a 'trigger' for an event, which - if it occurs - will be handled (from the event loop) using "eventHandlerProc".
      // (Returns 0 iff no such trigger can be created (e.g., because of implementation limits on the number of triggers).)
  virtual void deleteEventTrigger(EventTriggerId eventTriggerId) = 0;

  virtual void triggerEvent(EventTriggerId eventTriggerId, void* clientData = NULL) = 0;
      // Causes the (previously-registered) handler function for the specified event to be handled (from the event loop).
      // The handler function is called with "clientData" as parameter.
      // Note: This function (unlike other library functions) may be called from an external thread
      // - to signal an external event.  (However, "triggerEvent()" should not be called with the
      // same 'event trigger id' from different threads.)

  // The following two functions are deprecated, and are provided for backwards-compatibility only:
  void turnOnBackgroundReadHandling(int socketNum, BackgroundHandlerProc* handlerProc, void* clientData) {
    
    
    setBackgroundHandling(socketNum, SOCKET_READABLE, handlerProc, clientData);
  }
  void turnOffBackgroundReadHandling(int socketNum) {
    
     disableBackgroundHandling(socketNum); }

  virtual void internalError(); // used to 'handle' a 'should not occur'-type error condition within the library.

protected:
  TaskScheduler(); // abstract base class
};

二、BasicTaskScheduler0

The BasicTaskScheduler0 class inherits from TaskScheduler, which is the preliminary implementation of TaskScheduler, but it is also an abstract class, which defines the member variables corresponding to the three tasks.

protected:
  // To implement delayed operations:
  DelayQueue fDelayQueue;

  // To implement background reads:
  HandlerSet* fHandlers;
  int fLastHandledSocketNum;

  // To implement event triggers:
  EventTriggerId volatile fTriggersAwaitingHandling; // implemented as a 32-bit bitmap
  EventTriggerId fLastUsedTriggerMask; // implemented as a 32-bit bitmap
  TaskFunc* fTriggeredEventHandlers[MAX_NUM_EVENT_TRIGGERS];
  void* fTriggeredEventClientDatas[MAX_NUM_EVENT_TRIGGERS];
  unsigned fLastUsedTriggerNum; // in the range [0,MAX_NUM_EVENT_TRIGGERS)

fDelayQueue is used to manage delayed tasks,
fHandlers and fLastHandledSocketNum are used to manage background IO tasks,
and several other member variables are used to manage event tasks.

1. SingleStep

The SingleStep function is a pure virtual function that is called during doEventLoop.

void BasicTaskScheduler0::doEventLoop(char volatile* watchVariable) {
    
    
  // Repeatedly loop, handling readble sockets and timed events:
  while (1) {
    
    
    if (watchVariable != NULL && *watchVariable != 0) break;
    SingleStep();
  }
}

doEventLoop is to perform tasks in a loop.
watchVariable is a sign when the task is executed, and when the variable is NULL or the content it points to is 0, the task is stopped.
So SingleStep is to execute tasks in a single step, which is implemented in BasicTaskScheduler.

2. Event task scheduling

This section first analyzes how event tasks are scheduled, delayed task scheduling is analyzed when DelayQueue is introduced, and background IO tasks are analyzed when BasicTaskScheduler is introduced.
First look at the definitions of these member variables
fTriggersAwaitingHandling This is a 32-bit bitmap, and each bit of this variable represents an event slot. If the bit is 1, it means that the corresponding time slot is ready and can be executed.
fLastUsedTriggerMask This is also a 32-bit bitmap, which records the event task executed last time. Only one bit of fLastUsedTriggerMask is 1, which is the slot where the event was executed last time.
fTriggeredEventHandlers is an array used to store specific events, which are actually function pointers. Up to 32 events can be stored.
fTriggeredEventClientDatas is also an array, used to store the user data corresponding to each element of fTriggeredEventHandlers. In fact, it is the parameter required when the corresponding function is executed.
fLastUsedTriggerNum holds the last executed event slot.

1) Create an event task

EventTriggerId BasicTaskScheduler0::createEventTrigger(TaskFunc* eventHandlerProc) {
    
    
  unsigned i = fLastUsedTriggerNum;
  EventTriggerId mask = fLastUsedTriggerMask;

  do {
    
    
    i = (i+1)%MAX_NUM_EVENT_TRIGGERS;
    mask >>= 1;
    if (mask == 0) mask = 0x80000000;

    if (fTriggeredEventHandlers[i] == NULL) {
    
    
      // This trigger number is free; use it:
      fTriggeredEventHandlers[i] = eventHandlerProc;
      fTriggeredEventClientDatas[i] = NULL; // sanity

      fLastUsedTriggerMask = mask;
      fLastUsedTriggerNum = i;

      return mask;
    }
  } while (i != fLastUsedTriggerNum);

  // All available event triggers are allocated; return 0 instead:
  return 0;
}

fLastUsedTriggerNum is initialized to 31, and fLastUsedTriggerMask is initialized to 1. In this way, when creating the first task, it is guaranteed that the TriggerNum corresponding to the first task is (31+1)%32=0, and the first bit of the mask corresponding to the first task is 1.
It may be a little difficult to understand. We can assume that MAX_NUM_EVENT_TRIGGERS is 4 to understand. After understanding, it can be brought into 32 bits.
If MAX_NUM_EVENT_TRIGGERS=4, then fLastUsedTriggerNum is initialized to 3, and fLastUsedTriggerMask is initialized to 1.
Then when creating the first task, i=0, the mask is shifted to the right by 0, but if mask=0, the first position is set to 1, which is 1000.
In the second task, i=1, and the mask is shifted to the right by 0100.
For the third task, i=2, and the mask is shifted to the right by 0010.
For the fourth task, i=3, and the mask is shifted to the right by 0001.
For the fifth task, it is the same as the flag bit corresponding to the first task. So in fact, the number of the mask corresponding to the number of tasks is 1. Bringing it into MAX_NUM_EVENT_TRIGGERS=32 is also easy to understand, so I won’t go into details.

It is worth noting that only the corresponding fTriggeredEventHandlers are saved during creation, and the slot corresponding to fTriggeredEventClientDatas is still NULL.

2) Trigger event task

void BasicTaskScheduler0::triggerEvent(EventTriggerId eventTriggerId, void* clientData) {
    
    
  // First, record the "clientData".  (Note that we allow "eventTriggerId" to be a combination of bits for multiple events.)
  EventTriggerId mask = 0x80000000;
  for (unsigned i = 0; i < MAX_NUM_EVENT_TRIGGERS; ++i) {
    
    
    if ((eventTriggerId&mask) != 0) {
    
    
      fTriggeredEventClientDatas[i] = clientData;
    }
    mask >>= 1;
  }

  // Then, note this event as being ready to be handled.
  // (Note that because this function (unlike others in the library) can be called from an external thread, we do this last, to
  //  reduce the risk of a race condition.)
  fTriggersAwaitingHandling |= eventTriggerId;
}

To trigger a task is to find the corresponding slot according to the eventTriggerId, which is the mask corresponding to the task when creating the task, and fill the corresponding user data in the slot.
It is worth noting that triggerEvent is not a real execution event, but sets the bit corresponding to fTriggersAwaitingHandling to 1, indicating that the event corresponding to this slot can be executed, and it will be executed when it is scheduled.

3) Delete event task

void BasicTaskScheduler0::deleteEventTrigger(EventTriggerId eventTriggerId) {
    
    
  fTriggersAwaitingHandling &=~ eventTriggerId;

  if (eventTriggerId == fLastUsedTriggerMask) {
    
     // common-case optimization:
    fTriggeredEventHandlers[fLastUsedTriggerNum] = NULL;
    fTriggeredEventClientDatas[fLastUsedTriggerNum] = NULL;
  } else {
    
    
    // "eventTriggerId" should have just one bit set.
    // However, we do the reasonable thing if the user happened to 'or' together two or more "EventTriggerId"s:
    EventTriggerId mask = 0x80000000;
    for (unsigned i = 0; i < MAX_NUM_EVENT_TRIGGERS; ++i) {
    
    
      if ((eventTriggerId&mask) != 0) {
    
    
        fTriggeredEventHandlers[i] = NULL;
        fTriggeredEventClientDatas[i] = NULL;
      }
      mask >>= 1;
    }
  }
}

The deletion event is the same as the trigger event, and the slot corresponding to the event is found through eventTriggerId. When deleting, first set the position corresponding to fTriggersAwaitingHandling to 0. Here is the advantage of using a bitmap, and the marking of the corresponding event can be completed directly by bit operation.
After finding the corresponding slot, set the corresponding slot data of fTriggeredEventHandlers and fTriggeredEventClientDatas to NULL.
In the code, first judge whether the eventTriggerId is fLastUsedTriggerMask, in fact, it is to reduce the search process, because generally it is deleted after executing a task. This design is based on people's habits in handling tasks, which can improve retrieval efficiency.

3. DelayQueue

The DelayQueue class defines the queue of delayed tasks, including functions such as addition, deletion, and deletion of delayed tasks.
DelayQueue inherits from DelayQueueEntry, and DelayQueueEntry is an element of DelayQueue.

1、DelayQueueEntry

DelayQueueEntry is an element of a doubly linked list, and records the time that needs to be delayed. Note that this delay time is not the time the task really needs to be delayed, but the deta delay time. What do you mean, is the relative time.
insert image description here
Take the above picture as an example, assuming that the first task is ready to be executed, that is to say, the current delay of the first task is 0ms; the second task needs to wait for 5ms after the execution of the first task time-consuming), then the fDeltaTimeRemaining of the second task is 5ms. And the third task actually has to wait 8ms before it can be executed, then his fDeltaTimeRemaining is 8-5=3ms.

Why store the deta delay instead of storing the actual delay on a first come, first served basis. It can be imagined that if the delay time of the second task is 50ms, and the delay time of the third task is 3ms, according to the order of FIFO, the third task must wait for the execution of the second task before it can be executed, then the second task The three tasks have to wait for 53ms to be executed. Therefore, live555 forms a delayed task execution linked list according to the delayed time. It is more convenient to use deta delay to calculate.

2. Add delayed tasks

void DelayQueue::addEntry(DelayQueueEntry* newEntry) {
    
    
  synchronize();

  DelayQueueEntry* cur = head();
  while (newEntry->fDeltaTimeRemaining >= cur->fDeltaTimeRemaining) {
    
    
    newEntry->fDeltaTimeRemaining -= cur->fDeltaTimeRemaining;
    cur = cur->fNext;
  }

  cur->fDeltaTimeRemaining -= newEntry->fDeltaTimeRemaining;

  // Add "newEntry" to the queue, just before "cur":
  newEntry->fNext = cur;
  newEntry->fPrev = cur->fPrev;
  cur->fPrev = newEntry->fPrev->fNext = newEntry;
}

Because the delay time of newly added elements is based on the current one, synchronization is performed before each element is added. The so-called synchronization is to update the deta delay according to the current time.
After synchronization, find where the newly added element should be inserted into the queue, and calculate the deta delay of the new element relative to the previous task and insert it into the DelayQueue.
Let's see how to sync

void DelayQueue::synchronize() {
    
    
  // First, figure out how much time has elapsed since the last sync:
  _EventTime timeNow = TimeNow();
  if (timeNow < fLastSyncTime) {
    
    
    // The system clock has apparently gone back in time; reset our sync time and return:
    fLastSyncTime  = timeNow;
    return;
  }
  DelayInterval timeSinceLastSync = timeNow - fLastSyncTime;
  fLastSyncTime = timeNow;

  // Then, adjust the delay queue for any entries whose time is up:
  DelayQueueEntry* curEntry = head();
  while (timeSinceLastSync >= curEntry->fDeltaTimeRemaining) {
    
    
    timeSinceLastSync -= curEntry->fDeltaTimeRemaining;
    curEntry->fDeltaTimeRemaining = DELAY_ZERO;
    curEntry = curEntry->fNext;
  }
  curEntry->fDeltaTimeRemaining -= timeSinceLastSync;
}

First get the current time, and compare it with the time of the last synchronization. If the current time is earlier than the time of the last synchronization, it means that the clock has turned around, reset the time of the last synchronization and return.
Otherwise, calculate how long it has been since the last synchronization to this synchronization, and then update the deta time in the DelayQueue according to the elapsed time. It is worth noting that many tasks may have timed out during this synchronization, so the deta delay of those tasks should be set to 0. Then find the first task that has not timed out, and subtract the elapsed time from the deta delay of this task (this is also the advantage of using the deta task, that is, only update to the first task that has not timed out each time. Because it is a relative delay time, there is no need to update it later).

3. Update delayed tasks

Some delayed tasks may update their own delay time, the function of updating delayed tasks is this.
There are two update methods, one based on token and one based on delayed task pointer.

void DelayQueue::updateEntry(DelayQueueEntry* entry, DelayInterval newDelay) {
    
    
  if (entry == NULL) return;

  removeEntry(entry);
  entry->fDeltaTimeRemaining = newDelay;
  addEntry(entry);
}

void DelayQueue::updateEntry(intptr_t tokenToFind, DelayInterval newDelay) {
    
    
  DelayQueueEntry* entry = findEntryByToken(tokenToFind);
  updateEntry(entry, newDelay);
}

The token-based method is to find the corresponding delayed task pointer according to the token, and update it according to the method based on the delayed task pointer.
The update is relatively simple and rude, just delete the task first and then add it again.

4. Delete delayed tasks

void DelayQueue::removeEntry(DelayQueueEntry* entry) {
    
    
  if (entry == NULL || entry->fNext == NULL) return;

  entry->fNext->fDeltaTimeRemaining += entry->fDeltaTimeRemaining;
  entry->fPrev->fNext = entry->fNext;
  entry->fNext->fPrev = entry->fPrev;
  entry->fNext = entry->fPrev = NULL;
  // in case we should try to remove it again
}

DelayQueueEntry* DelayQueue::removeEntry(intptr_t tokenToFind) {
    
    
  DelayQueueEntry* entry = findEntryByToken(tokenToFind);
  removeEntry(entry);
  return entry;
}

Deleting delayed tasks is the same as updating delayed tasks. There are two ways to delete. The deta delay to update the next one after deletion. (again demonstrating the benefits of using relative latency)

5. Scheduling delayed tasks

DelayInterval const& DelayQueue::timeToNextAlarm() {
    
    
  if (head()->fDeltaTimeRemaining == DELAY_ZERO) return DELAY_ZERO; // a common case

  synchronize();
  return head()->fDeltaTimeRemaining;
}

This function returns the waiting time for the next task, which is used for task scheduling.

Handle delayed tasks in the function handleAlarm

void DelayQueue::handleAlarm() {
    
    
  if (head()->fDeltaTimeRemaining != DELAY_ZERO) synchronize();

  if (head()->fDeltaTimeRemaining == DELAY_ZERO) {
    
    
    // This event is due to be handled:
    DelayQueueEntry* toRemove = head();
    removeEntry(toRemove); // do this first, in case handler accesses queue

    toRemove->handleTimeout();
  }
}

This is the scheduling function for delayed tasks. First judge whether the task at the head of the linked list can be executed at that time, if not, perform a synchronization, and then wait for the next scheduling.
If it's time, remove the task from the queue. And process the task, execute handleTimeout.
Because in fact the delayed task object used in BasicTaskScheduler0 is of type AlarmHandler.
AlarmHandler inherits from DelayQueueEntry and overloads the handleTimeout function

  virtual void handleTimeout() {
    
    
    (*fProc)(fClientData);
    DelayQueueEntry::handleTimeout();
  }

That is to say, in fact, processing the delayed task is to execute the function of the registered delayed task, and delete the resource of the delayed task after execution.

四、BasicTaskScheduler

BasicTaskScheduler is a real task scheduling class, inherited from BasicTaskScheduler0, and implements the scheduling of background IO tasks.

1. Generate a BasicTaskScheduler object

The constructor of BasicTaskScheduler is protected, which means that an object cannot be directly created outside the class, so if you want to create a task scheduling object, you must create it through createNew.

BasicTaskScheduler* BasicTaskScheduler::createNew(unsigned maxSchedulerGranularity) {
    
    
	return new BasicTaskScheduler(maxSchedulerGranularity);
}

maxSchedulerGranularity is the maximum scheduling interval, indicating that at least one scheduling is completed within the specified time.
createNew returns a BasicTaskScheduler object.

BasicTaskScheduler::BasicTaskScheduler(unsigned maxSchedulerGranularity)
  : fMaxSchedulerGranularity(maxSchedulerGranularity), fMaxNumSockets(0)
#if defined(__WIN32__) || defined(_WIN32)
  , fDummySocketNum(-1)
#endif
{
    
    
  FD_ZERO(&fReadSet);
  FD_ZERO(&fWriteSet);
  FD_ZERO(&fExceptionSet);

  if (maxSchedulerGranularity > 0) schedulerTickTask(); // ensures that we handle events frequently
}

Initialized fReadSet, fWriteSet and fExceptionSet during construction. At the same time, the schedulerTickTask delay task is generated.
Let's take a look at what the schedulerTickTask implements:

void BasicTaskScheduler::schedulerTickTask(void* clientData) {
    
    
  ((BasicTaskScheduler*)clientData)->schedulerTickTask();
}

void BasicTaskScheduler::schedulerTickTask() {
    
    
  scheduleDelayedTask(fMaxSchedulerGranularity, schedulerTickTask, this);
}

In fact, schedulerTickTask is put into the delay queue as a delayed task. When the delayed task is scheduled to schedulerTickTask, it is put into the delay queue as a delayed task. In short, schedulerTickTask is executed in a certain period of time.
It seems meaningless, just execute a meaningless function regularly, this function does not do any actual work. What effect? You can't see it just by looking at it, you can only know it when you are in the SingleStep function.

2. Background IO task scheduling

The BasicTaskScheduler of live555 implements IO multiplexing by means of select.

1) Set background IO tasks

void BasicTaskScheduler
  ::setBackgroundHandling(int socketNum, int conditionSet, BackgroundHandlerProc* handlerProc, void* clientData) {
    
    
  if (socketNum < 0) return;
#if !defined(__WIN32__) && !defined(_WIN32) && defined(FD_SETSIZE)
  if (socketNum >= (int)(FD_SETSIZE)) return;
#endif
  FD_CLR((unsigned)socketNum, &fReadSet);
  FD_CLR((unsigned)socketNum, &fWriteSet);
  FD_CLR((unsigned)socketNum, &fExceptionSet);
  if (conditionSet == 0) {
    
    
    fHandlers->clearHandler(socketNum);
    if (socketNum+1 == fMaxNumSockets) {
    
    
      --fMaxNumSockets;
    }
  } else {
    
    
    fHandlers->assignHandler(socketNum, conditionSet, handlerProc, clientData);
    if (socketNum+1 > fMaxNumSockets) {
    
    
      fMaxNumSockets = socketNum+1;
    }
    if (conditionSet&SOCKET_READABLE) FD_SET((unsigned)socketNum, &fReadSet);
    if (conditionSet&SOCKET_WRITABLE) FD_SET((unsigned)socketNum, &fWriteSet);
    if (conditionSet&SOCKET_EXCEPTION) FD_SET((unsigned)socketNum, &fExceptionSet);
  }
}

This setting background task includes two functions of creating a new background task and deleting a background task.
When the conditionSet is 0, the background task is deleted, and the background IO task is deleted through the clearHandler function of the HandlerSet class.
When the conditionSet is not 0, the background IO task is generated through the assignHandler function of the HandlerSet class.

A collection of background IO tasks of the HandlerSet class, which realizes the addition, deletion and movement of background IO tasks. The content of the class is relatively simple, so I won't do too much analysis.

2) Mobile Socket corresponds to the task

void BasicTaskScheduler::moveSocketHandling(int oldSocketNum, int newSocketNum) {
    
    
  if (oldSocketNum < 0 || newSocketNum < 0) return; // sanity check
#if !defined(__WIN32__) && !defined(_WIN32) && defined(FD_SETSIZE)
  if (oldSocketNum >= (int)(FD_SETSIZE) || newSocketNum >= (int)(FD_SETSIZE)) return; // sanity check
#endif
  if (FD_ISSET(oldSocketNum, &fReadSet)) {
    
    FD_CLR((unsigned)oldSocketNum, &fReadSet); FD_SET((unsigned)newSocketNum, &fReadSet);}
  if (FD_ISSET(oldSocketNum, &fWriteSet)) {
    
    FD_CLR((unsigned)oldSocketNum, &fWriteSet); FD_SET((unsigned)newSocketNum, &fWriteSet);}
  if (FD_ISSET(oldSocketNum, &fExceptionSet)) {
    
    FD_CLR((unsigned)oldSocketNum, &fExceptionSet); FD_SET((unsigned)newSocketNum, &fExceptionSet);}
  fHandlers->moveHandler(oldSocketNum, newSocketNum);

  if (oldSocketNum+1 == fMaxNumSockets) {
    
    
    --fMaxNumSockets;
  }
  if (newSocketNum+1 > fMaxNumSockets) {
    
    
    fMaxNumSockets = newSocketNum+1;
  }
}

This function implements the socket corresponding to the change task, and the flags (read, write, exception) that it cares about.

3) Background IO task scheduling

The task scheduling function of BasicTaskScheduler is in the SingleStep function. This function is called cyclically in the doEventLoop function of the parent class. That is the real scheduling function.

  DelayInterval const& timeToDelay = fDelayQueue.timeToNextAlarm();
  struct timeval tv_timeToDelay;
  tv_timeToDelay.tv_sec = timeToDelay.seconds();
  tv_timeToDelay.tv_usec = timeToDelay.useconds();
  // Very large "tv_sec" values cause select() to fail.
  // Don't make it any larger than 1 million seconds (11.5 days)
  const long MAX_TV_SEC = MILLION;
  if (tv_timeToDelay.tv_sec > MAX_TV_SEC) {
    
    
    tv_timeToDelay.tv_sec = MAX_TV_SEC;
  }
  // Also check our "maxDelayTime" parameter (if it's > 0):
  if (maxDelayTime > 0 &&
      (tv_timeToDelay.tv_sec > (long)maxDelayTime/MILLION ||
       (tv_timeToDelay.tv_sec == (long)maxDelayTime/MILLION &&
	tv_timeToDelay.tv_usec > (long)maxDelayTime%MILLION))) {
    
    
    tv_timeToDelay.tv_sec = maxDelayTime/MILLION;
    tv_timeToDelay.tv_usec = maxDelayTime%MILLION;
  }

First, get the delay time of the next delayed task, and judge whether the delay time exceeds the maximum limit delay time. If it exceeds, set the timeout time of this scheduling as the maximum limit delay time, otherwise the timeout time is the next delay task timeout period.

Here you know the function of the schedulerTickTask function. Try if the schedulerTickTask function is not set and there are no other delayed tasks, then there will be no tasks in the delayed task queue, then the delay time obtained from timeToNextAlarm() can only be the default value, and the scheduling timeout here is not good Take control.

Continue to look at the scheduling of background IO tasks

  int selectResult = select(fMaxNumSockets, &readSet, &writeSet, &exceptionSet, &tv_timeToDelay);
  if (selectResult < 0) {
    
    
#if defined(__WIN32__) || defined(_WIN32)
    int err = WSAGetLastError();
    // For some unknown reason, select() in Windoze sometimes fails with WSAEINVAL if
    // it was called with no entries set in "readSet".  If this happens, ignore it:
    if (err == WSAEINVAL && readSet.fd_count == 0) {
    
    
      err = EINTR;
      // To stop this from happening again, create a dummy socket:
      if (fDummySocketNum >= 0) closeSocket(fDummySocketNum);
      fDummySocketNum = socket(AF_INET, SOCK_DGRAM, 0);
      FD_SET((unsigned)fDummySocketNum, &fReadSet);
    }
    if (err != EINTR) {
    
    
#else
    if (errno != EINTR && errno != EAGAIN) {
    
    
#endif
	// Unexpected error - treat this as fatal:
#if !defined(_WIN32_WCE)
	perror("BasicTaskScheduler::SingleStep(): select() fails");
	// Because this failure is often "Bad file descriptor" - which is caused by an invalid socket number (i.e., a socket number
	// that had already been closed) being used in "select()" - we print out the sockets that were being used in "select()",
	// to assist in debugging:
	fprintf(stderr, "socket numbers used in the select() call:");
	for (int i = 0; i < 10000; ++i) {
    
    
	  if (FD_ISSET(i, &fReadSet) || FD_ISSET(i, &fWriteSet) || FD_ISSET(i, &fExceptionSet)) {
    
    
	    fprintf(stderr, " %d(", i);
	    if (FD_ISSET(i, &fReadSet)) fprintf(stderr, "r");
	    if (FD_ISSET(i, &fWriteSet)) fprintf(stderr, "w");
	    if (FD_ISSET(i, &fExceptionSet)) fprintf(stderr, "e");
	    fprintf(stderr, ")");
	  }
	}
	fprintf(stderr, "\n");
#endif
	internalError();
      }
  }

  // Call the handler function for one readable socket:
  HandlerIterator iter(*fHandlers);
  HandlerDescriptor* handler;
  // To ensure forward progress through the handlers, begin past the last
  // socket number that we handled:
  if (fLastHandledSocketNum >= 0) {
    
    
    while ((handler = iter.next()) != NULL) {
    
    
      if (handler->socketNum == fLastHandledSocketNum) break;
    }
    if (handler == NULL) {
    
    
      fLastHandledSocketNum = -1;
      iter.reset(); // start from the beginning instead
    }
  }
  while ((handler = iter.next()) != NULL) {
    
    
    int sock = handler->socketNum; // alias
    int resultConditionSet = 0;
    if (FD_ISSET(sock, &readSet) && FD_ISSET(sock, &fReadSet)/*sanity check*/) resultConditionSet |= SOCKET_READABLE;
    if (FD_ISSET(sock, &writeSet) && FD_ISSET(sock, &fWriteSet)/*sanity check*/) resultConditionSet |= SOCKET_WRITABLE;
    if (FD_ISSET(sock, &exceptionSet) && FD_ISSET(sock, &fExceptionSet)/*sanity check*/) resultConditionSet |= SOCKET_EXCEPTION;
    if ((resultConditionSet&handler->conditionSet) != 0 && handler->handlerProc != NULL) {
    
    
      fLastHandledSocketNum = sock;
          // Note: we set "fLastHandledSocketNum" before calling the handler,
          // in case the handler calls "doEventLoop()" reentrantly.
      (*handler->handlerProc)(handler->clientData, resultConditionSet);
      break;
    }
  }
  if (handler == NULL && fLastHandledSocketNum >= 0) {
    
    
    // We didn't call a handler, but we didn't get to check all of them,
    // so try again from the beginning:
    iter.reset();
    while ((handler = iter.next()) != NULL) {
    
    
      int sock = handler->socketNum; // alias
      int resultConditionSet = 0;
      if (FD_ISSET(sock, &readSet) && FD_ISSET(sock, &fReadSet)/*sanity check*/) resultConditionSet |= SOCKET_READABLE;
      if (FD_ISSET(sock, &writeSet) && FD_ISSET(sock, &fWriteSet)/*sanity check*/) resultConditionSet |= SOCKET_WRITABLE;
      if (FD_ISSET(sock, &exceptionSet) && FD_ISSET(sock, &fExceptionSet)/*sanity check*/) resultConditionSet |= SOCKET_EXCEPTION;
      if ((resultConditionSet&handler->conditionSet) != 0 && handler->handlerProc != NULL) {
    
    
	fLastHandledSocketNum = sock;
	    // Note: we set "fLastHandledSocketNum" before calling the handler,
            // in case the handler calls "doEventLoop()" reentrantly.
	(*handler->handlerProc)(handler->clientData, resultConditionSet);
	break;
      }
    }
    if (handler == NULL) fLastHandledSocketNum = -1;//because we didn't call a handler
  }

First enter the select block, and the timeout period is the timeout period of this scheduling calculated in the previous step.
If selectResult<0 indicates an error, print the error message.
Then start traversing the tasks in fHandlers, if any task meets the execution condition, execute the task (only one background IO task, one event task and one delay task are executed at most for each scheduling).

It is worth noting that, in order to ensure that each background IO task has a chance to be run, it starts to traverse from the task that was run last time, and if it cannot find a task that meets the conditions, it starts from the beginning. Just imagine that if you simply traverse from the beginning each time, then if the task at the head of the team meets the execution conditions every time it is scheduled, then the subsequent tasks will not have a chance to be executed.

After executing the background IO task, start executing the event task:

  // Also handle any newly-triggered event (Note that we do this *after* calling a socket handler,
  // in case the triggered event handler modifies The set of readable sockets.)
  if (fTriggersAwaitingHandling != 0) {
    
    
    if (fTriggersAwaitingHandling == fLastUsedTriggerMask) {
    
    
      // Common-case optimization for a single event trigger:
      fTriggersAwaitingHandling &=~ fLastUsedTriggerMask;
      if (fTriggeredEventHandlers[fLastUsedTriggerNum] != NULL) {
    
    
	(*fTriggeredEventHandlers[fLastUsedTriggerNum])(fTriggeredEventClientDatas[fLastUsedTriggerNum]);
      }
    } else {
    
    
      // Look for an event trigger that needs handling (making sure that we make forward progress through all possible triggers):
      unsigned i = fLastUsedTriggerNum;
      EventTriggerId mask = fLastUsedTriggerMask;

      do {
    
    
	i = (i+1)%MAX_NUM_EVENT_TRIGGERS;
	mask >>= 1;
	if (mask == 0) mask = 0x80000000;

	if ((fTriggersAwaitingHandling&mask) != 0) {
    
    
	  fTriggersAwaitingHandling &=~ mask;
	  if (fTriggeredEventHandlers[i] != NULL) {
    
    
	    (*fTriggeredEventHandlers[i])(fTriggeredEventClientDatas[i]);
	  }

	  fLastUsedTriggerMask = mask;
	  fLastUsedTriggerNum = i;
	  break;
	}
      } while (i != fLastUsedTriggerNum);
    }
  }

You can see the comment to prevent the event task from changing the status of the background IO task during runtime, so the event task should be scheduled behind the background IO task.
Like the background IO task, it ensures that each task has a chance to run, so it traverses and runs from the last execution position.

Finally schedule the delayed task.

  // Also handle any delayed event that may have come due.
  fDelayQueue.handleAlarm();

5. Summary of Basic Scheduling

live555 divides tasks into three categories, namely delayed tasks, event tasks and background IO tasks. Delay tasks are managed and scheduled through DelayQueue, event tasks are managed and scheduled through the bitmap structure, and background IO tasks are scheduled through HandlerSet.
DelayQueue stores delayed tasks in a linked list according to the delay time, and each element records its own task function and user data, as well as the relative delay time relative to the previous task.
The event task is saved by a 32-bit bitmap. To ensure that each event task has a chance to be executed, the event task will record the last executed task, and each schedule traverses from the last executed task.
The background IO task uses the select mechanism to implement IO multiplexing, and to ensure that each IO task has a chance to execute, it also records the last executed event, and each schedule traverses from the last executed task.

Guess you like

Origin blog.csdn.net/qq_36383272/article/details/120946172