Designing an Asynchronous Solution

Test-driving your code will result in a design that usually differs from what you initially conceived. That’s not an excuse to abandon up-front design completely, however. You usually want to start with a road map that provides a reasonable direction.

Just don’t invest much time adding detail to the road map, because you’ll no doubt need to take a few detours as you travel the road ahead. The sections of the road map that detail the road ultimately not traveled represent a waste of your time.

A GeoServer client wants a simple interface—pass the server a user, a width, and a height, and receive a list of users in response. To support an asynchronous experience, however, the client will need to pass a callback function that handles receiving each in-bounds user.

We want to isolate the client from any details of threading. Here’s our proposed design:

  • For each incoming request, create a work item and add it to a single work queue. The work item contains all the information needed to determine whether a user lies within an area.

  • From the GeoServer, start one or more worker threads. When not processing work, each thread waits for an item to become available in the work queue. Once a thread pulls a work item, it tells it to execute.

We could code all of the logic in the GeoServer class directly, but we won’t. Conflating threading and application logic can lead to long, ugly debugging sessions when there’s a problem. And there’s almost always a problem.

Instead, we will separate the three concerns implied into three classes.

  • Work, which represents a work item to be queued and executed

  • ThreadPool, which creates worker threads to handle the work queue

  • GeoServer, which creates a Work object and sends it to the ThreadPool for execution

The Work class is probably the simplest place to start.

c9/5/WorkTest.cpp
 
#include "CppUTest/TestHarness.h"
 
#include "Work.h"
 
#include <functional>
 
#include <vector>
 
#include <sstream>
 
 
using​ ​namespace​ std;
 
 
TEST_GROUP(AWorkObject) {
 
};
 
 
TEST(AWorkObject, DefaultsFunctionToNullObject) {
 
Work work;
 
try​ {
 
work.execute();
 
}
 
catch​(...) {
 
FAIL(​"unable to execute function"​);
 
}
 
}
 
 
TEST(AWorkObject, DefaultsFunctionToNullObjectWhenConstructedWithId) {
 
Work work(1);
 
try​ {
 
work.execute();
 
}
 
catch​(...) {
 
FAIL(​"unable to execute function"​);
 
}
 
}
 
 
TEST(AWorkObject, CanBeConstructedWithAnId) {
 
Work work(1);
 
LONGS_EQUAL(1, work.id());
 
}
 
 
TEST(AWorkObject, DefaultsIdTo0) {
 
Work work;
 
LONGS_EQUAL(0, work.id());
 
}
 
TEST(AWorkObject, DefaultsIdTo0WhenFunctionSpecified) {
 
Work work{[]{}};
 
LONGS_EQUAL(0, work.id());
 
}
 
 
TEST(AWorkObject, CanBeConstructedWithAFunctionAndId) {
 
Work work{[]{}, 1};
 
LONGS_EQUAL(1, work.id());
 
}
 
 
TEST(AWorkObject, ExecutesFunctionStored) {
 
bool​ wasExecuted{false};
 
auto​ executeFunction = [&] () { wasExecuted = true; };
 
Work work(executeFunction);
 
work.execute();
 
CHECK_TRUE(wasExecuted);
 
}
 
 
TEST(AWorkObject, CanExecuteOnDataCapturedWithFunction) {
 
vector<​string​> data{​"a"​, ​"b"​};
 
string​ result;
 
auto​ callbackFunction = [&](​string​ s) {
 
result.append(s);
 
};
 
auto​ executeFunction = [&]() {
 
stringstream s;
 
s << data[0] << data[1];
 
callbackFunction(s.str());
 
};
 
Work work(executeFunction);
 
work.execute();
 
CHECK_EQUAL(​"ab"​, result);
 
}
c9/5/Work.h
 
#ifndef Work_h
 
#define Work_h
 
#include <functional>
 
 
class​ Work {
 
public​:
 
static​ ​const​ ​int​ DefaultId{0};
 
Work(​int​ id=DefaultId)
 
: id_{id}
 
, executeFunction_{[]{}} {}
 
Work(std::function<​void​()> executeFunction, ​int​ id=DefaultId)
 
: id_{id}
 
, executeFunction_{executeFunction}
 
{}
 
void​ execute() {
 
executeFunction_();
 
}
 
int​ id() ​const​ {
 
return​ id_;
 
}
 
private​:
 
int​ id_;
 
std::function<​void​()> executeFunction_;
 
};
 
#endif

The Work test CanExecuteOnDataCapturedWithFunction merely shows how lambdas work and is technically not necessary. It passes immediately. The test serves to reinforce our still-growing knowledge of how to use lambdas and demonstrates how client code can take advantage of a Work object. The function stored in executeFunction captures the locally defined data vector and subsequently demonstrates execution on that data. (The capture specification of [&] tells C++ to capture any referenced variable by reference.) The function also captures the locally defined callbackFunction, to which it then sends the result of concatenating elements from the data vector.

We’ll eventually delete the CanExecuteOnDataCapturedWithFunction test. When we get to the point of creating a Work object from the GeoServer code, it will provide us with an example of what we’ll want to do.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.224.56.29