Azure IoT C SDK
Threading model and optimizing for resource constrained devices

IoTHub Device Client and the _LL_ (lower layer) API

The lower layer

There are two distinct but closely related sets of APIs for the IoT Hub client. Applications that want to run on a single thread should use the _LL_ (aka lower level) API. Sometimes the hardware/OS can only support one thread at a time or else spinning additional threads is expensive. Applications on more capable hardware may also use the _LL_ layer for greater control over scheduling.

Calls to the IoT Hub API's ending in Async do not cause the operation to execute immediately. Instead, the work - both for sending data across the network and receiving data - is queued. The application must manually schedule the work by calling IoTHubDeviceClient_LL_DoWork (when using a device client).

This is typically done in some sort of simple loop, e.g.

while (appIsRunning) {
SendTelemetryIfThereIsAnyToSend(iotHubLLHandle);
Sleep(1000);
}
void IoTHubDeviceClient_LL_DoWork(IOTHUB_DEVICE_CLIENT_LL_HANDLE iotHubClientHandle)
This function MUST be called by the user so work (sending/receiving data on the network,...
struct IOTHUB_CLIENT_CORE_LL_HANDLE_DATA_TAG * IOTHUB_DEVICE_CLIENT_LL_HANDLE
Handle corresponding to a lower layer (LL) device client instance.
Definition: iothub_device_client_ll.h:49

CRITICAL(!) notes for _LL_ application development
NOT FOLLOWING THESE STEPS WILL POTENTIALLY RESULT IN VERY HARD TO DIAGNOSE BUGS

  • You MUST NOT use an _LL_ handle on more than one thread (unless you provide all your own locking). Part of what makes the _LL_ layer use fewer resources is that it does not use locks. It assumes a single threaded environment where locks are not needed.
  • Even if you are not actively sending data while using _LL_ layer, you still need to periodically invoke the appropriate DoWork to make the client accept data from the network. This periodic DoWork call is also required as otherwise the underlying network transport and/or service may timeout the service due to inactivity.

The convenience layer

Applications may also use the non _LL_ API set, which is also known as the "convenience layer."

Just like the _LL_ layer, IoT Hub API's ending in Async queue work to be performed later and do not block waiting for the service accepting or rejecting the request. The difference is that the convenience layer itself automatically takes care of sending the data. In other words, there is no DoWork. The convenience layer does this for you automatically by spinning a worker thread to implicitly DoWork for your application. The convenience layer also performs locking, allowing a given IOTHUB_DEVICE_CLIENT_HANDLE to be safely used by different threads.

How to specify between the _LL_ and convenience layers

  • Applications using the _LL_ layer should #include iothub_device_client_ll.h and use its API's and IOTHUB_DEVICE_CLIENT_LL_HANDLE.
  • Applications using the convenience layer should #include iothub_device_client.h and use its API's and IOTHUB_DEVICE_CLIENT_HANDLE.

The _LL_ | convenience layer paradigm is used in many places throughout the SDK. The module client follows this pattern with IOTHUB_MODULE_CLIENT_LL_HANDLE and IOTHUB_MODULE_CLIENT_HANDLE. The provisioning client also follows this pattern with PROV_DEVICE_LL_HANDLE and PROV_DEVICE_HANDLE.

Avoid long-running callbacks

Regardless of whether you are using the _LL_ or convenience layer, application callbacks that you implement - such as processing a twin update - must NOT block for extended periods of time.

The IoT Hub SDK uses a single dispatcher thread to handle all callbacks to user code. This same thread handles all network I/O. This is true whether the _LL_ or convenience layer is used. The only difference is that in the _LL_ layer, your thread is the dispatcher when it calls into the appropriate DoWork() call.

An application callback that takes a long time to run is problematic. The SDK will not be able to call other pending callbacks as it is blocked on the long-running one. If the call back code takes long enough (minutes) there is the risk that the SDK will not be able to fire its periodic network keep-alive and that the entire connection will be dropped.

Your callback should in general limit itself to quick, CPU bound operations.

Reentrancy and API calls

When processing a callback, your application code must not destroy the underlying handle (via IoTHubDeviceClient_LL_Destroy, e.g.). If your application is using the _LL_ layer, it must also not invoke IoTHubDeviceClient_LL_DoWork or IoTHubModuleClient_LL_DoWork.

Your code can invoke other IoT Hub SDK calls. A common pattern for instance is that while processing a callback for an updated device twin properties for the application to invoke IoTHubDeviceClient_LL_SendReportedState.