change the structure a bit
This commit is contained in:
17
01-tasks/01-tasks-solutions/main-01.cpp
Normal file
17
01-tasks/01-tasks-solutions/main-01.cpp
Normal file
@ -0,0 +1,17 @@
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
using namespace std;
|
||||
|
||||
void mySimpleTask() {
|
||||
using namespace std::chrono_literals;
|
||||
while(true) {
|
||||
cout << "Hello World" << endl;
|
||||
this_thread::sleep_for(1000ms);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::thread thread(mySimpleTask);
|
||||
thread.join();
|
||||
}
|
31
01-tasks/01-tasks-solutions/main-02.cpp
Normal file
31
01-tasks/01-tasks-solutions/main-02.cpp
Normal file
@ -0,0 +1,31 @@
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
using namespace std;
|
||||
|
||||
class MyExecutableObject {
|
||||
public:
|
||||
MyExecutableObject(uint32_t delayMs): delayMs(delayMs) {}
|
||||
|
||||
static void executeTask(MyExecutableObject& self) {
|
||||
while(true) {
|
||||
self.performOperation();
|
||||
this_thread::sleep_for(std::chrono::milliseconds(self.delayMs));
|
||||
}
|
||||
}
|
||||
|
||||
void performOperation() {
|
||||
cout << "Hello World" << endl;
|
||||
}
|
||||
private:
|
||||
uint32_t delayMs;
|
||||
};
|
||||
|
||||
int main() {
|
||||
MyExecutableObject myExecutableObject(1000);
|
||||
std::thread thread(
|
||||
MyExecutableObject::executeTask,
|
||||
std::reference_wrapper(myExecutableObject));
|
||||
thread.join();
|
||||
return 0;
|
||||
}
|
77
01-tasks/01-tasks-solutions/main-03.cpp
Normal file
77
01-tasks/01-tasks-solutions/main-03.cpp
Normal file
@ -0,0 +1,77 @@
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
using namespace std;
|
||||
|
||||
class MyExecutableObjectIF {
|
||||
public:
|
||||
virtual ~MyExecutableObjectIF() = default;
|
||||
virtual void performOperation() = 0;
|
||||
};
|
||||
|
||||
class MyExecutableObject0: public MyExecutableObjectIF {
|
||||
public:
|
||||
MyExecutableObject0() = default;
|
||||
|
||||
void performOperation() override {
|
||||
cout << "Task 0" << endl;
|
||||
}
|
||||
private:
|
||||
};
|
||||
|
||||
class MyExecutableObject1: public MyExecutableObjectIF {
|
||||
public:
|
||||
MyExecutableObject1() = default;
|
||||
|
||||
void performOperation() override {
|
||||
cout << "Task 1" << endl;
|
||||
}
|
||||
private:
|
||||
};
|
||||
|
||||
class MyExecutableObject2: public MyExecutableObjectIF {
|
||||
public:
|
||||
MyExecutableObject2() = default;
|
||||
|
||||
void performOperation() override {
|
||||
cout << "Task 2" << endl;
|
||||
}
|
||||
private:
|
||||
};
|
||||
|
||||
class MyPeriodicTask {
|
||||
public:
|
||||
MyPeriodicTask(MyExecutableObjectIF& executable, uint32_t taskFreqMs)
|
||||
: executable(executable), taskFreqMs(taskFreqMs) {}
|
||||
|
||||
std::thread start() {
|
||||
return std::thread(
|
||||
MyPeriodicTask::executeTask,
|
||||
std::reference_wrapper(*this));
|
||||
}
|
||||
private:
|
||||
static void executeTask(MyPeriodicTask& self) {
|
||||
while(true) {
|
||||
self.executable.performOperation();
|
||||
this_thread::sleep_for(std::chrono::milliseconds(self.taskFreqMs));
|
||||
}
|
||||
}
|
||||
MyExecutableObjectIF& executable;
|
||||
uint32_t taskFreqMs;
|
||||
};
|
||||
|
||||
int main() {
|
||||
MyExecutableObject0 myExecutableObject0;
|
||||
MyExecutableObject1 myExecutableObject1;
|
||||
MyExecutableObject2 myExecutableObject2;
|
||||
MyPeriodicTask task0(myExecutableObject0, 1000);
|
||||
MyPeriodicTask task1(myExecutableObject1, 2000);
|
||||
MyPeriodicTask task2(myExecutableObject2, 5000);
|
||||
auto thread0 = task0.start();
|
||||
auto thread1 = task1.start();
|
||||
auto thread2 = task2.start();
|
||||
thread0.join();
|
||||
thread1.join();
|
||||
thread2.join();
|
||||
return 0;
|
||||
}
|
85
01-tasks/01-tasks-solutions/main-04.cpp
Normal file
85
01-tasks/01-tasks-solutions/main-04.cpp
Normal file
@ -0,0 +1,85 @@
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
|
||||
#include "fsfw/tasks/ExecutableObjectIF.h"
|
||||
#include "fsfw/tasks/PeriodicTaskIF.h"
|
||||
#include "fsfw/tasks/TaskFactory.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
class MyExecutableObjectIF {
|
||||
public:
|
||||
virtual ~MyExecutableObjectIF() = default;
|
||||
virtual void performOperation() = 0;
|
||||
};
|
||||
|
||||
class MyExecutableObject0: public ExecutableObjectIF {
|
||||
public:
|
||||
MyExecutableObject0() = default;
|
||||
|
||||
ReturnValue_t performOperation(uint8_t opCode) override {
|
||||
cout << "Task 0" << endl;
|
||||
return returnvalue::OK;
|
||||
}
|
||||
private:
|
||||
};
|
||||
|
||||
class MyExecutableObject1: public ExecutableObjectIF {
|
||||
public:
|
||||
MyExecutableObject1() = default;
|
||||
|
||||
ReturnValue_t performOperation(uint8_t opCode) override {
|
||||
cout << "Task 1" << endl;
|
||||
return returnvalue::OK;
|
||||
}
|
||||
private:
|
||||
};
|
||||
|
||||
class MyExecutableObject2: public ExecutableObjectIF {
|
||||
public:
|
||||
MyExecutableObject2() = default;
|
||||
|
||||
ReturnValue_t performOperation(uint8_t opCode) override {
|
||||
cout << "Task 2" << endl;
|
||||
return returnvalue::OK;
|
||||
}
|
||||
private:
|
||||
};
|
||||
|
||||
class MyPeriodicTask {
|
||||
public:
|
||||
MyPeriodicTask(MyExecutableObjectIF& executable, uint32_t taskFreqMs)
|
||||
: executable(executable), taskFreqMs(taskFreqMs) {}
|
||||
|
||||
std::thread start() {
|
||||
return std::thread(
|
||||
MyPeriodicTask::executeTask,
|
||||
std::reference_wrapper(*this));
|
||||
}
|
||||
private:
|
||||
static void executeTask(MyPeriodicTask& self) {
|
||||
while(true) {
|
||||
self.executable.performOperation();
|
||||
this_thread::sleep_for(std::chrono::milliseconds(self.taskFreqMs));
|
||||
}
|
||||
}
|
||||
MyExecutableObjectIF& executable;
|
||||
uint32_t taskFreqMs;
|
||||
};
|
||||
|
||||
int main() {
|
||||
MyExecutableObject0 myExecutableObject0;
|
||||
MyExecutableObject1 myExecutableObject1;
|
||||
MyExecutableObject2 myExecutableObject2;
|
||||
auto* factory = TaskFactory::instance();
|
||||
auto* periodicTask0 = factory->createPeriodicTask("TASK_0", 0, PeriodicTaskIF::MINIMUM_STACK_SIZE, 0.5, nullptr);
|
||||
auto* periodicTask1 = factory->createPeriodicTask("TASK_1", 0, PeriodicTaskIF::MINIMUM_STACK_SIZE, 1.0, nullptr);
|
||||
periodicTask0->addComponent(&myExecutableObject0);
|
||||
periodicTask0->addComponent(&myExecutableObject1);
|
||||
periodicTask1->addComponent(&myExecutableObject2);
|
||||
periodicTask0->startTask();
|
||||
periodicTask1->startTask();
|
||||
while(true) {
|
||||
this_thread::sleep_for(5000ms);
|
||||
}
|
||||
}
|
234
01-tasks/01-tasks.md
Normal file
234
01-tasks/01-tasks.md
Normal file
@ -0,0 +1,234 @@
|
||||
# Threads and Tasks
|
||||
|
||||
A satellite is a complex system which usually has a lot of tasks which need to be done
|
||||
simulatenously by a dedicated On-Board Computer (OBC). This can include for example:
|
||||
|
||||
- TMTC handling. This includes Telecommand (TC) reception and execution, and the (autonomous)
|
||||
generation of Telemetry (TM)
|
||||
- Control Operations, for example execution of the Attitue Control System (ACS) loop
|
||||
- Handling of connected physical devices like sensors or payloads
|
||||
|
||||
Usually, these systems oftentimes have soft and even hard real-time requirements where longer delays
|
||||
are not allowed and the system has an upper bound for response times.
|
||||
|
||||
This basically means that any software which does multiple non-trivial tasks needs a
|
||||
(real-time) operating system to perform multiple tasks consecutively, with deterministisc
|
||||
guarantees that these tasks are performed within a certain temporal bound.
|
||||
|
||||
Some common operating system in the Space domain able to do this:
|
||||
|
||||
- FreeRTOS for smaller MCUs (e.g. SOURCE CubeSat project)
|
||||
- Embedded Linux (EIVE CubeSat project)
|
||||
- RTEMS (FLP satellite project)
|
||||
|
||||
All these operating system use threads or tasks as the basic worker unit which is executing code.
|
||||
This chapter first introduces threads as they are exposed by the C++ standard library.
|
||||
After that, the code is transitioned to use the abstraction provided by the framework.
|
||||
|
||||
## 1. Scheduling a basic task using the C++ `std::thread` API
|
||||
|
||||
The goal of this task is to set up a basic thread which prints the following
|
||||
string every second: "Hello World".
|
||||
|
||||
- [std::thread API](https://en.cppreference.com/w/cpp/thread/thread)
|
||||
- [Delaying a thread](https://en.cppreference.com/w/cpp/thread/sleep_for)
|
||||
|
||||
## 2. Changing to the concept of executable objects
|
||||
|
||||
The goal of this task is to convert the code from task 1 so the `std::thread` API takes an
|
||||
executable object to move to a more object oriented task approach. The printout of the thread
|
||||
should remain the same. The executable objects should be named `MyExecutableObject`. It contains
|
||||
one function called `periodicOperation` which performs the printout, and a static function which
|
||||
takes the `MyExecutableObject` itself by reference and executes it in a permanent loop.
|
||||
|
||||
The executable object should be passed into the `std::thread` directly.
|
||||
|
||||
### Hints
|
||||
|
||||
- [std::reference_wrapper](https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper)
|
||||
to pass references to the [std::thread] API
|
||||
- [std::chrono::milliseconds](https://en.cppreference.com/w/cpp/chrono/duration) has a constructor
|
||||
where an `uint32_t` can be used to create the duration from a custon number.
|
||||
|
||||
### Subtasks
|
||||
|
||||
1. Create a class called `MyExecutableObject` with a `public` block.
|
||||
2. Add a static function called `executeTask` which expects itself (`MyExecutableObject& self`) as
|
||||
a parameter with an empty implementation
|
||||
3. Add a regular method called `performOperation` which performs the printout
|
||||
4. Implement `executeTask`. This function uses the passed object and performs the scheduling
|
||||
specific part by calling `self.performOperation` in a permanent loop with a delay between
|
||||
calls. You can hardcode the delay to 1000ms for the first implementation.
|
||||
5. Add a constructor to `MyExecutableObject` which expects a millisecond delay
|
||||
as an `uint32_t` and cache it as a member variable. Then use this member
|
||||
variable in the `executeTask` implementation to make the task frequency configurable via the
|
||||
constructor (ctor) parameter.
|
||||
|
||||
With the conversion to executable object, we have reached a useful goal in object-oriented
|
||||
programming (OOP) in general: The application logic inside `performOperation` is now decoupled
|
||||
from the scheduling logic inside `executeTask`. This is also called seperation of concerns.
|
||||
|
||||
## 3. Making the executable objects generic
|
||||
|
||||
Threads generally expect a function which is then directly executed.
|
||||
Sometimes, the execution of threads needs to be deferred. For example, this can be useful
|
||||
if the execution of tasks should only start after a certain condition.
|
||||
|
||||
Also, it might become useful to model any task in form of a class. An instantiation
|
||||
of that class would then be an executable object. Another point is that even though
|
||||
we have seperated the scheduling specific part from the application logic, they are still
|
||||
part of the same class. It would be nice to have two separate classes for this.
|
||||
|
||||
C++ as an OOP language provides abstraction in form of interfaces, which can be used to have
|
||||
different types of generic executable objects. Interfaces usually do not have a lot of source code
|
||||
on their own. They describe a design contract a class should have which implements the interface.
|
||||
In general, the FSFW relies heavily on subclassing and inheritance to provide adaptions point to users.
|
||||
|
||||
We are going to refactor our `MyExecutableObject` by introducing an interface for any executable
|
||||
object. We are then going to add a generic class which expects an object fulfilling this design
|
||||
contract and then executes that object.
|
||||
|
||||
Interfaces in C++ are implemented using
|
||||
[abstract classes](https://en.cppreference.com/w/cpp/language/abstract_class) which only contains
|
||||
pure virtual functions.
|
||||
|
||||
### Subtasks
|
||||
|
||||
1. Create an interface called `MyExecutableObjectIF`. You can create this like a regular class.
|
||||
As opposed to Java the differences between interfaces and classes are only by convention.
|
||||
2. In general, it is recommended to add a virtual destructor to an interface. It looks like this:
|
||||
```cpp
|
||||
virtual ~<Class>() = default;
|
||||
```
|
||||
3. Add a abstract virtual function `performOperation`.
|
||||
Abstract virtual functions look like this in general
|
||||
|
||||
```cpp
|
||||
virtual <functionName>(...) = 0;
|
||||
```
|
||||
4. Implement you custom interface for `MyExecutableObject` by re-using the exsiting
|
||||
`performOperation` function. In general, when implementing
|
||||
an interface or overriding a virtual function, it is recommended to add the `override` keyword
|
||||
to the function delaration. We do not have seperation between source and header files for
|
||||
our class yet, so you can add the `override` keyword after the function arguments and before
|
||||
the implementation block. The compiler will throw a compile error if a function is declared
|
||||
override but no base object function was actually overriden. This can prevent subtle bugs.
|
||||
Please note that `MyExecutableObject` is actually now forced to implement the
|
||||
`performOperation` function because that function is pure. The compiler makes sure we fulfill
|
||||
the design contract specified by the interface
|
||||
5. Add a new class called `MyPeriodicTask`. Our executed object and the task abstraction
|
||||
are now explicitely decoupled by using composition. Composition means that we have
|
||||
a "has-a" relationship instead of a "is-a" relationship. In general, composition is preferable
|
||||
to inheritance for flexible software designs. The new `MyPeriodicTask` class should
|
||||
have a ctor which expects a `MyExecutableObjectIF` by reference and the task frequency in
|
||||
milliseconds as an `uint32_t`. It caches both the executbale object and the task frequency
|
||||
as private member variables. Also add a `std::thread` member variable.
|
||||
6. Add a public `start` function and leave it empty for now.
|
||||
7. Add a private static `executeTask` method which expects `MyPeriodicTask` by reference.
|
||||
Its implementation is similar to the `executeTask` method of `MyExecutableObject`.
|
||||
Remove the `executeTask` implementation from `MyExecutableObject`.
|
||||
8. In the start method, use `std::thread` API with `MyPeriodicTask::executeTask` as the
|
||||
executed function. Pass the task itself by reference similarly to how it was done in task 2.
|
||||
Cache the created thread into the thread member variable.
|
||||
|
||||
We now have two separate classes where one class only contains application logic and the other
|
||||
one only contains scheduling logic. The `MyPeriodicTask` is also able to schedule arbitrary
|
||||
types which implement `MyExecutableObjectIF`. Finish this task
|
||||
by crating 3 different executable objects where each object does or prints our something
|
||||
different. Then pass all of those three different objects to a `MyPeriodicTask` and start
|
||||
all three periodic tasks which three different frequencies.
|
||||
|
||||
You now should have code which looks something like this:
|
||||
|
||||
```cpp
|
||||
int main() {
|
||||
MyExecutableObject0 myExecutableObject0;
|
||||
MyExecutableObject1 myExecutableObject1;
|
||||
MyExecutableObject2 myExecutableObject2;
|
||||
MyPeriodicTask task0(myExecutableObject0, 1000);
|
||||
MyPeriodicTask task1(myExecutableObject1, 2000);
|
||||
MyPeriodicTask task2(myExecutableObject2, 5000);
|
||||
auto thread0 = task0.start();
|
||||
auto thread1 = task1.start();
|
||||
auto thread2 = task2.start();
|
||||
thread0.join();
|
||||
thread1.join();
|
||||
thread2.join();
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
Where the three tasks do their tasks with different frequencies.
|
||||
|
||||
## 4. Using the framework abstractions
|
||||
|
||||
We now use framework components to perform the tasks shown above. The framework
|
||||
exposes an abstractions for executable tasks called [`ExecutableObjectIF`](https://documentation.irs.uni-stuttgart.de/fsfw/development/api/task.html).
|
||||
|
||||
It also offers a unform API to execute periodic tasks in form of the
|
||||
[`PeriodicTaskIF`](https://egit.irs.uni-stuttgart.de/fsfw/fsfw/src/branch/master/src/fsfw/tasks/PeriodicTaskIF.h).
|
||||
|
||||
These tasks can then be created using the
|
||||
[`TaskFactory`](https://egit.irs.uni-stuttgart.de/fsfw/fsfw/src/branch/master/src/fsfw/tasks/TaskFactory.h) singleton.
|
||||
|
||||
An arbitrary number of executable objects can then be passed to a periodic task. These objects
|
||||
are then executed sequentially. This allows a granular design of executable tasks.
|
||||
For example, important tasks get an own dedicated thread while other low priority objects are
|
||||
scheduled consecutively in another thread.
|
||||
|
||||
In summary, task abstractions have the following advantages:
|
||||
|
||||
- Task execution can be deferred until an explicit `start` method is called
|
||||
- Same uniform API across multiple operating systems
|
||||
|
||||
### Subtasks
|
||||
|
||||
1. Load the required interfaces:
|
||||
- `#include "fsfw/tasks/ExecutableObjectIF.h"`
|
||||
- `#include "fsfw/tasks/PeriodicTaskIF.h`
|
||||
- `#include "fsfw/tasks/TaskFactory.h`
|
||||
2. For your three custom objects, implement the executable object IF provided by the framework
|
||||
instead of your custom interface.
|
||||
3. In your main function, create an instance of the `TaskFactory`. `TaskFactory`
|
||||
is implemented as a singleton: The object will create itself when using
|
||||
`TaskFactory::instance`. All subsequent calls will return the same intance.
|
||||
There are other similar singletons to create other objects like mutexes or message queues.
|
||||
3. Create two periodic tasks using the `TaskFactory::createPeriodicTask` function.
|
||||
Some notes on the expected arguments:
|
||||
- Each task has a name. This is useful for debugging, especially because the framework
|
||||
abstractons can detect missed deadlines.
|
||||
- The task priority parameter is OS dependent. This parameter is currently ignored for the
|
||||
Linux OSAL so you can pass 0 here.
|
||||
On Windows, you can retrieve the priority by using `tasks::makeWinPriority`
|
||||
which can be loaded by including `#include "fsfw/osal/windows/winTaskHelpers.h"`
|
||||
- The stack space parameter is generally ignored or unimportant for host systems.
|
||||
You can simply pass `PeriodicTaskIF::MINIMUM_STACK_SIZE` here. This parameter becomes
|
||||
important on resource constrained OSes and systems, for example FreeRTOS.
|
||||
- The frequency is expected as floating point seconds
|
||||
- You can pass a function which will be called when a deadline is missed. This is the case
|
||||
when a tasks took longer than its designated slot frequency. This is useful to detect
|
||||
bugs in the software or generally detect when tasks require a large amount of time.
|
||||
You can also pass `nullptr` here if you do not want any function to be called.
|
||||
4. Add the first two of your custom exec objects to the first periodic task. Those tasks
|
||||
will be executed in the same thread consecutively. You can use the `PeriodicTaskIF::addComponent`
|
||||
method to do this.
|
||||
5. Add the third custom exec object to the second periodic task. The third object
|
||||
gets an own thread
|
||||
6. Start both periodic tasks and add a permanent loop at the end of the main
|
||||
method which puts the main thread into sleep.
|
||||
|
||||
You successfully scheduled some objects using the framework!
|
||||
The general concept of executble objects is used heavily throughout the framework.
|
||||
For example, each device handler or controller is an executable object, as the bases
|
||||
classes exposed by the framework implement `ExecutableObjectIF`.
|
||||
|
||||
There is also another type of periodic task handler called `FixedTimeslotTask`. Here,
|
||||
you can explicitely specify (multiple) execution slots with a specified relative time within
|
||||
the execution slot. This is useful for objects where there are multiple processing steps
|
||||
but the steps take different amount of times.
|
||||
|
||||
In examples or other OBSW implementations using the framework, you will often
|
||||
see the distrinction between an `ObjectFactory.cpp` and an `InitMission.cpp`.
|
||||
In the first file, all global (executable) objects will be created. In the second file,
|
||||
all of these objects will be scheduled. Another chapter will introduce the Object Manager
|
||||
to show what exactly is happening here.
|
Reference in New Issue
Block a user