3 live555のソースコード解析(3) - live555のタスクスケジューリング

1. タスクスケジューラ

TaskScheduler クラスはタスクスケジューリングの基本的な抽象クラスで、live555 ではタスクを遅延タスク、イベントタスク、バックグラウンド IO タスクの 3 種類に分類し、それぞれのタスクのスケジューリング機能を定義しています。
TaskScheduler クラスは、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

BasicTaskScheduler0 クラスは、TaskScheduler の暫定実装である TaskScheduler を継承していますが、3 つのタスクに対応するメンバー変数を定義する抽象クラスでもあります。

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 は遅延タスクの管理に使用され、
fHandlers と fLastHandledSocketNum はバックグラウンド IO タスクの管理に使用され、
その他のいくつかのメンバー変数はイベント タスクの管理に使用されます。

1.シングルステップ

SingleStep 関数は、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 はタスクをループで実行することです。
watchVariable はタスク実行時の合図であり、変数が NULL または指す内容が 0 の場合、タスクは停止します。
つまり、SingleStep はタスクを 1 つのステップで実行するもので、BasicTaskScheduler に実装されています。

2. イベントタスクのスケジュール設定

このセクションでは、最初にイベント タスクがどのようにスケジュールされるかを分析し、DelayQueue が導入された場合は遅延タスクのスケジューリングが分析され、BasicTaskScheduler が導入された場合はバックグラウンド IO タスクが分析されます。
まず、これらのメンバー変数の定義を見てください
fTriggersAwaitingHandling これは 32 ビットのビットマップであり、この変数の各ビットはイベント スロットを表します。ビットが 1 の場合、対応するタイム スロットの準備ができており、実行できることを意味します。
fLast UsedTriggerMask これも 32 ビットのビットマップで、最後に実行されたイベント タスクを記録します。fLast UsedTriggerMask の 1 ビットだけが 1 で、これはイベントが最後に実行されたスロットです。
fTriggeredEventHandlers は、特定のイベントを格納するために使用される配列であり、実際には関数ポインターです。最大 32 個のイベントを保存できます。
fTriggeredEventClientDatas も配列であり、fTriggeredEventHandlers の各要素に対応するユーザー データを格納するために使用されます。実際には、対応する関数を実行するときに必要なパラメータです。
fLast UsedTriggerNum は、最後に実行されたイベント スロットを保持します。

1) イベントタスクを作成する

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;
}

fLast UsedTriggerNum は 31 に初期化され、fLast UsedTriggerMask は 1 に初期化されます。このようにして、最初のタスクを作成するとき、最初のタスクに対応する TriggerNum が であり(31+1)%32=0、最初のタスクに対応するマスクの最初のビットが 1 であることが保証されます。
少しわかりにくいかもしれませんが、MAX_NUM_EVENT_TRIGGERS が 4 であると仮定して理解すると、32 ビットにまとめることができます。
MAX_NUM_EVENT_TRIGGERS=4 の場合、fLast UsedTriggerNum は 3 に初期化され、fLast UsedTriggerMask は 1 に初期化されます。
次に、最初のタスク i=0 を作成するときに、マスクは 0 だけ右にシフトされますが、マスク = 0 の場合、最初の位置は 1、つまり 1000 に設定されます。
2 番目のタスクでは、i=1 で、マスクは 0100 だけ右にシフトされます。
3 番目のタスクでは、i=2 となり、マスクは 0010 だけ右にシフトされます。
4 番目のタスクでは、i=3 であり、マスクは 0001 だけ右にシフトされます。
5 番目のタスクでは、1 番目のタスクに対応するフラグ ビットと同じです。したがって、実際には、タスクの数に対応するマスクの数は 1 です。MAX_NUM_EVENT_TRIGGERS=32 にするのもわかりやすいので、詳細は省略します。

作成中には対応する fTriggeredEventHandler のみが保存され、fTriggeredEventClientDatas に対応するスロットは NULL のままであることに注意してください。

2) イベントタスクのトリガー

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;
}

タスクをトリガーするには、タスク作成時にタスクに対応するマスクであるeventTriggerIdに従って対応するスロットを見つけ、対応するユーザーデータをスロットに埋めます。
triggersAwaitingHandling に対応するビットを 1 に設定して、このスロットに対応するイベントが実行可能であることを示し、スケジュールされたときに実行されるのを待機します。

3) イベントタスクの削除

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;
    }
  }
}

削除イベントはトリガー イベントと同じであり、イベントに対応するスロットはeventTriggerIdによって見つかります。削除する場合は、まず fTriggersAwaitingHandling に対応する位置を 0 に設定します。ここにビットマップを使用する利点があり、対応するイベントのマーキングをビット操作で直接完了できます。
対応するスロットを見つけたら、fTriggeredEventHandlers と fTriggeredEventClientDatas の対応するスロット データを NULL に設定します。
コードではまず、eventTriggerId が fLastusedTriggerMask であるかどうかを判断します。これは、一般にタスクの実行後に削除されるため、実際には検索処理を軽減するためです。この設計は、タスクを処理する際の人々の習慣に基づいており、検索効率を向上させることができます。

3. 遅延キュー

DelayQueue クラスは、遅延タスクの追加、削除、削除などの機能を含む、遅延タスクのキューを定義します。
DelayQueue は DelayQueueEntry を継承しており、DelayQueueEntry は DelayQueue の要素です。

1、遅延キューエントリ

DelayQueueEntry は二重リンク リストの要素であり、遅延する必要がある時間を記録します。この遅延時間は、タスクが実際に遅延する必要がある時間ではなく、データ遅延時間であることに注意してください。つまり、相対的な時間のことです。
ここに画像の説明を挿入
上の図を例として、最初のタスクを実行する準備ができている、つまり、最初のタスクの現在の遅延が 0 ミリ秒であると仮定し、2 番目のタスクは最初のタスク時間の実行後に 5 ミリ秒待つ必要があるとします。 -消費)、2 番目のタスクの fDeltaTimeRemaining は 5ms です。そして、3 番目のタスクは実際に実行されるまでに 8 ミリ秒待つ必要があり、その場合の fDeltaTimeRemaining は 8-5=3 ミリ秒になります。

実際の遅延を先着順で保存するのではなく、データ遅延を保存する理由。2 番目のタスクの遅延時間が 50ms、3 番目のタスクの遅延時間が 3ms の場合、FIFO の順序に従って、3 番目のタスクは 2 番目のタスクの実行を待たなければ実行できないことが想像できます。 3 つのタスクが実行されるまで 53 ミリ秒待つ必要があります。したがって、live555 は遅延時間に応じて遅延タスク実行リンク リストを作成します。データ遅延を使用して計算する方が便利です。

2. 遅延タスクを追加する

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;
}

新しく追加された要素の遅延時間は現在の遅延時間に基づいているため、各要素を追加する前に同期が実行され、現在の時刻に応じてデータ遅延が更新されます。
同期後、新しく追加された要素をキューに挿入する場所を見つけ、前のタスクに対する新しい要素のデータ遅延を計算して DelayQueue に挿入します。
同期する方法を見てみましょう

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;
}

まず現在時刻を取得し、前回の同期時刻と比較し、現在時刻が前回の同期時刻より早い場合は時計が回ったことを意味し、前回の同期時刻をリセットしてリターンします。
それ以外の場合は、前回の同期から今回の同期までの経過時間を計算し、経過時間に応じて DelayQueue のデータ時間を更新します。この同期中に多くのタスクがタイムアウトした可能性があるため、それらのタスクのデータ遅延を 0 に設定する必要があることに注意してください。次に、タイムアウトしていない最初のタスクを見つけて、このタスクのデータ遅延から経過時間を減算します (これはデータ タスクを使用する利点でもあります。つまり、タイムアウトしていない最初のタスクのみを更新するということです)相対的な遅延時間であるため、後で更新する必要はありません)。

3. 遅延したタスクを更新する

一部の遅延タスクは独自の遅延時間を更新する場合があります。遅延タスクを更新する機能はこれです。
更新方法には 2 つあり、1 つはトークンに基づくもの、もう 1 つは遅延タスク ポインターに基づくものです。

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);
}

トークンベースの方法は、トークンに従って対応する遅延タスクポインタを見つけ、遅延タスクポインタに基づく方法に従って更新することである。
更新は比較的単純かつ失礼で、最初にタスクを削除してから再度追加するだけです。

4. 遅れたタスクを削除する

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;
}

遅延タスクの削除は、遅延タスクの更新と同じです。削除には 2 つの方法があります。削除後に次のデータを更新するまでのデータ遅延。(相対レイテンシーを使用する利点を再度示します)

5. 遅延タスクのスケジュール設定

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

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

この関数は、タスクのスケジューリングに使用される次のタスクの待ち時間を返します。

関数 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();
  }
}

遅延タスクのスケジューリング機能です。まず、リンクリストの先頭のタスクがその時点で実行可能かどうかを判断し、実行可能でない場合は同期を実行し、次のスケジューリングを待ちます。
時間になったら、タスクをキューから削除します。そしてタスクを処理し、handleTimeoutを実行します。
実際、BasicTaskScheduler0 で使用される遅延タスク オブジェクトのタイプは AlarmHandler であるためです。
AlarmHandler は DelayQueueEntry を継承し、handleTimeout 関数をオーバーロードします。

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

つまり、遅延タスクの処理とは、実際には、登録されている遅延タスクの機能を実行し、実行後に遅延タスクのリソースを削除することである。

四、BasicTaskScheduler

BasicTaskScheduler は、BasicTaskScheduler0 から継承された実際のタスク スケジューリング クラスで、バックグラウンド IO タスクのスケジューリングを実装します。

1. BasicTaskScheduler オブジェクトを生成する

BasicTaskScheduler のコンストラクターは保護されています。つまり、オブジェクトをクラスの外部に直接作成することはできないため、タスク スケジュール オブジェクトを作成する場合は、createNew を使用して作成する必要があります。

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

maxSchedulerGranularity は最大スケジューリング間隔で、指定された時間内に少なくとも 1 つのスケジューリングが完了することを示します。
createNew は BasicTaskScheduler オブジェクトを返します。

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
}

構築中に fReadSet、fWriteSet、および fExceptionSet が初期化されました。同時に、schedulerTickTask遅延タスクが生成されます。
schedulerTickTask の実装を見てみましょう。

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

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

実際には、schedulerTickTask は遅延タスクとして遅延キューに入れられ、遅延タスクがSchedulerTickTask にスケジュールされると、遅延タスクとして遅延キューに入れられます。つまり、schedulerTickTaskは一定時間ごとに実行されます。
意味がないようですが、意味のない関数を定期的に実行するだけで、この関数は実際の作業を行いません。どのような効果がありますか? これは見ただけではわかりません。SingleStep 関数を使用している場合にのみわかります。

2. バックグラウンド IO タスクのスケジューリング

live555のBasicTaskSchedulerはselectによるIO多重化を実装しています。

1) バックグラウンド IO タスクを設定する

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);
  }
}

このバックグラウンドタスクの設定には、バックグラウンドタスクの新規作成とバックグラウンドタスクの削除の2つの機能が含まれる。
conditionSetが0の場合、バックグラウンドタスクは削除され、バックグラウンドIOタスクはHandlerSetクラスのclearHandler関数によって削除されます。
ConditionSet が 0 以外の場合、HandlerSet クラスの assignHandler 関数を通じてバックグラウンド IO タスクが生成されます。

バックグラウンドIOタスクの追加、削除、移動を実現するHandlerSetクラスのバックグラウンドIOタスクのコレクション。授業内容は比較的シンプルなので、あまり分析はしません。

2) Mobile Socket がタスクに対応

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;
  }
}

この関数は、変更タスクに対応するソケットと、それが対象とするフラグ (読み取り、書き込み、例外) を実装します。

3) バックグラウンド IO タスクのスケジューリング

BasicTaskScheduler のタスクスケジューリング機能は SingleStep 関数内にあります。この関数は、親クラスの doEventLoop 関数内で周期的に呼び出されます。それが本当のスケジューリング機能です。

  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;
  }

まず、次に遅延しているタスクの遅延時間を取得し、その遅延時間が最大遅延時間を超えているかどうかを判定し、超えている場合はこのスケジューリングのタイムアウト時間を最大遅延時間として設定し、超えていない場合は次のタイムアウト時間を設定します。タスクのタイムアウト期間を遅らせます。

ここで、schedulerTickTask 関数の機能がわかりました。schedulerTickTask 関数が設定されておらず、他に遅延タスクがない場合、遅延タスク キューにタスクは存在しない場合、timeToNextAlarm() から取得される遅延時間はデフォルト値のみになり、ここでのスケジュール タイムアウトは次のとおりです。良くない コントロールしてください。

バックグラウンド IO タスクのスケジュールを引き続き確認します。

  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
  }

まず選択ブロックに入ります。タイムアウト期間は、前のステップで計算されたこのスケジューリングのタイムアウト期間です。
selectResult<0 がエラーを示している場合は、エラー メッセージを出力します。
次に、fHandlers 内のタスクの走査を開始し、実行条件を満たすタスクがあれば、そのタスクを実行します (スケジューリングごとに最大 1 つのバックグラウンド IO タスク、1 つのイベント タスク、および 1 つの遅延タスクのみが実行されます)。

各バックグラウンド IO タスクが実行される機会を確保するために、前回実行されたタスクからトラバースを開始し、条件を満たすタスクが見つからない場合は、最初から実行されることに注意してください。始まり。単純に毎回最初からたどる場合、チームの先頭にあるタスクがスケジュールされるたびに実行条件を満たしていれば、後続のタスクは実行される機会がなくなることを想像してください。

バックグラウンド IO タスクを実行した後、イベント タスクの実行を開始します。

  // 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);
    }
  }

イベント タスクが実行時にバックグラウンド IO タスクのステータスを変更しないようにするためのコメントが表示されます。そのため、イベント タスクはバックグラウンド IO タスクの背後にスケジュールする必要があります。
バックグラウンド IO タスクと同様に、各タスクに実行の機会があることが保証されるため、最後の実行位置からトラバースして実行されます。

最後に、遅延したタスクをスケジュールします。

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

5. 基本的なスケジュールのまとめ

live555 では、タスクを遅延タスク、イベント タスク、バックグラウンド IO タスクの 3 つのカテゴリに分類します。遅延タスクは DelayQueue を通じて管理およびスケジュールされ、イベント タスクはビットマップ構造を通じて管理およびスケジュールされ、バックグラウンド IO タスクは HandlerSet を通じてスケジュールされます。
DelayQueue は、遅延時間に従って遅延タスクをリンク リストに保存し、各要素は独自のタスク関数とユーザー データ、および前のタスクとの相対的な遅延時間を記録します。
イベント タスクは 32 ビット ビットマップで保存されます。各イベント タスクが実行される機会を確保するために、イベント タスクは最後に実行されたタスクを記録し、各スケジュールは最後に実行されたタスクからたどります。
バックグラウンド IO タスクは、選択メカニズムを使用して IO 多重化を実装し、各 IO タスクに実行の機会があることを保証するために、最後に実行されたイベントも記録し、各スケジュールは最後に実行されたタスクからたどります。

おすすめ

転載: blog.csdn.net/qq_36383272/article/details/120946172