Multithreading in ArmarX

Prerequisites: Write a Server and Client Communicating via RPC (C++)

If you want to check armarx threads documentation, you can find it here ArmarX Threads. This tutorial is self contained, which means that you don't need to go through the above component tutorial. But we suggest that you should read it to understand how components work in ArmarX.

After this tutorial you will understand how to write a multi-threaded program. We use a simple example which includes two clients and one server. The whole system contains a counter which is incremented continuously by one client called sender. The other client called receiver queries this value every few miliseconds. They communicate with each other through the server. In the meantime the server must accomplish a complicated task (we use sleep() to simulate the complicated task) and deliver the hash value of the current value given by the counter.

The idea is to firstly construct a client component and then instantiate two clients with different names: receiver and sender. The client component should be able to connect to the server component. Receiver and Sender can both manipulate the value of the counter with the methods implemented in server component. Hence, we should create an ice interface for server component to enable the communication.

Create a new package with components

Create a new package called MultiThreadingExample:

armarx-package init MultiThreadingExample
cd MultiThreadingExample
armarx-package add component MultiThreadingExampleServer
armarx-package add component MultiThreadingExampleClient
armarx-package add application MultiThreadingExampleServer
armarx-package add application MultiThreadingExampleClient

Write an ice interface for server component

Now we create an ice interface for server component and name it MultiThreadingExampleServerInterface.ice. The interface contains all functions which the client component can access. The receiver should be able to get the value from the sever and the sender can manipulate the value, hence we create one pair of getter-setter functions.

#ifndef _ARMARX_MultiThreadingExample_ServerInterface_SLICE
#define _ARMARX_MultiThreadingExample_ServerInterface_SLICE
module armarx
{
interface MultiThreadingExampleServerInterface
{
void setValue(string val);
string getValue();
};
};
#endif

Change the contents of CMakeLists to realize appropriate links

Now we need to change some CMakeLists in order to correctly build everything.

  • Add the interface directory into CMakeLists.txt under the folder "source/MultiThreadingExample/" using
    add_sub_directory(interface)
    if it's not already there. (Notice: we should put this command before all other commands in the current CMakeLists.txt)
  • Add server interfaces library into both CMakeLists of server and client components because we need to use the interface in both components.
    set(COMPONENT_LIBS ArmarXCoreInterfaces ArmarXCore MultiThreadingExampleInterfaces)
    (Notice: this cmake line should be added into both CMakeLists under separate folders for both server and client components.)
  • At the end, we need to edit the CMakeLists under the folder interface as following. (Notice: if you cannot see the interface folder in QTCreator, please run CMake on the project after finishing these first steps.)
    # List of slice files to include in the interface library
    set(SLICE_FILES
    MultiThreadingExampleServerInterface.ice
    )
    # generate the interface library
    armarx_interfaces_generate_library(MultiThreadingExample "${MultiThreadingExample_INTERFACE_DEPEND}")

Now we have set up all links. We should be able to build the whole project without errors. Build the project with 'make' under the 'build' folder. If you encounter any errors, ensure that you strictly follow the above steps.

Implement functions for server and client components

Until now, we have not implemented any functions yet. This is our next step. In this step, you have a lot of choices. Feel free to construct your own multi-threading program. The most interesting thing in ArmarX package is that we have already two different objects, runningtask (Using running tasks) and periodictask (Using periodic tasks), implemented. You can just use both tools to create your own thread. As the names show, the running task can only be executed one times, while the periodic task is always called after a certain time interval until you stop the tasks.

Remember to include the server interface into your header files. Ice will automatically generate a header file for our server interface. We need to add the following 'include' statements into both components' header files.

#include <MultiThreadingExample/interface/MultiThreadingExampleServerInterface.h>

Also, make sure the MultiThreadingExampleServerComponent inherits from the MultiThreadingExampleServerInterface:

class MultiThreadingExampleServer :
virtual public armarx::Component,
virtual public armarx::MultiThreadingExampleServerInterface

Also implement the getter and setter methods by adding two std::string member inputValue, outputValue to the header and by accessing getting/setting them with said methods.

If you want to use running task or periodic task tools, you also need to include them.

We implement a running task in the server component. Before that, we should set some private variable members for the server. The server should get input value (inputValue) from the sender and deliver the manipulated value (outputValue). At the same time it must have a 'complicated'(sleep()) running task (serverTask), in which the hash function of the input value is also calculated. You can initialize a running task using the following code:

RunningTask<MultiThreadingExampleServer>::pointer_type serverTask = new RunningTask<MultiThreadingExampleServer>(this, &MultiThreadingExampleServer::runningTask);

The second argument of the constructor defines a function which is called when the task starts running. Furthermore, we need two locks separately for setting and sending values.

boost::mutex inputMutex, outputMutex

Then we can implement our running task in server.

void MultiThreadingExampleServer::runningTask()
{
while(!serverTask->isStopped())
{
std::string tmp;
{
boost::mutex::scoped_lock lock(inputMutex);
tmp = inputValue; //keep lock time short
}
sleep(2); //sleep simulates long computation time
boost::hash<std::string> string_hash;
ARMARX_IMPORTANT << "The hash value of \"" + inputValue + "\" is " + ValueToString(string_hash(inputValue));
{
boost::mutex::scoped_lock lock(outputMutex);
outputValue = tmp; //keep lock time short
}
}
}

The above codes realize the functionality metioned before. Notice that we use boost scoped_lock to lock the resources we use. Boost scoped lock will release lock once it meets the end of the blocks. We avoid placing 'sleep' command into the scope of lock, because if it represents a very complicated task, we may never see any change of the values.

We can put the initialization of the running task together with the start of the task into the OnConnectComponent().

serverTask->start();

The client component must be able to connect the server component, which means that we should firstly define properties for server component.

defineRequiredProperty<std::string>("ClientType", "Sender or Receiver");
defineOptionalProperty<std::string>("ServerName", "MultiThreadingExampleServer", "Description");

We add another command line in order to tell the difference between sender and receiver. At the same time, we need to get access to these properties under the function onInitComponent():

usingProxy(getProperty<std::string>("ServerName").getValue());
isSender = getProperty<std::string>("ClientType").getValue() == "Sender";

where 'isSender' is a member of the client component. We should add the other member called 'counter' into the component, then the sender can increment the counter in its periodic task. The final step is to define a periodic task and create the corresponding function. We add the following codes into OnConnectComponent():

serverPrx = getProxy<MultiThreadingExampleServerInterfacePrx>(getProperty<std::string>("ServerName").getValue());
clientTask = new PeriodicTask<MultiThreadingExampleClientComponent>(this, &MultiThreadingExampleClient::periodicTaskCallback, 700);
clientTask->start();

where 'serverPrx' is used to receive the information from server component, hence you can use it to call all the functions defined in the ice interface (MultiThreadingExampleServerInterface). 'clientTask' is defined as a periodic task, which calles a callback function called 'periodicTaskCallback' every 700 miliseconds.

Our callback function looks like the following:

void MultiThreadingExampleClient::periodicTaskCallback()
{
if(isSender)
{
counter++;
ARMARX_IMPORTANT << "Setting the Counter " << counter;
serverPrx->setValue(ValueToString(counter));
}
else
{
ARMARX_IMPORTANT << "Getting the counter: "<< serverPrx->getValue();
}
}

If the client is the sender, then it will increment the counter and send some information to the server. If the client is the receiver, then it will get the information from the server.

Don't forget stopping both running task and periodic task when disconnecting the components.

Create a new scenario

At the end of the tutorial, we need to create a scenario and see the result of our multi-threading program. You can use the ArmarX GUI and the included scenario manager (Add Widget -> Meta -> ScenarioManager). First register the package MultiThreadingExample which we created earlier (Configure button). Then you should be able to create a scenario in this package.

After creating the scenario add our applications. We need one MultiThreadingExampleServerApp and two instances of the MultiThreadingExampleClientApp (sender and receiver).

Before running the scenario the clients need to be configured so that one acts as a sender and the other one as a receiver. Set the following properties for the sender using the scenario manager:

ArmarX.MultiThreadingExampleClient.ClientType = Sender
ArmarX.MultiThreadingExampleClient.ObjectName = MultiThreadingSenderClient

Set the corresponding for the receiver client:

ArmarX.MultiThreadingExampleClient.ClientType = Receiver
ArmarX.MultiThreadingExampleClient.ObjectName = MultiThreadingReceiverClient

Finally we can start the scenario and see if the results are correct by pressing the start button in the scenario manager. The log messages can be inspected using the LogViewer.

ARMARX_IMPORTANT
#define ARMARX_IMPORTANT
Definition: Logging.h:183
PeriodicTask.h
RunningTask.h
armarx::ValueToString
std::string ValueToString(const T &value)
Definition: StringHelpers.h:58
armarx::Component
Baseclass for all ArmarX ManagedIceObjects requiring properties.
Definition: Component.h:95
armarx::control::common::getValue
T getValue(nlohmann::json &userConfig, nlohmann::json &defaultConfig, const std::string &entryName)
Definition: utils.h:55
armarx
This file offers overloads of toIce() and fromIce() functions for STL container types.
Definition: ArmarXTimeserver.cpp:28