Virtual Packs, Part 2

This will build on the first article Taming Qt. So, if you haven't read that one yet you're advised to do so.

Previously, we conveniently used virtual packs for simple GUI callbacks. Can we do more with it?

Virtual packs happen to have traits. These are traits that can be used on virtual pack:

  • Counted trait
  • Synchronized trait
  • Waitable trait
  • On ready trait

Counted trait simply keeps track of how many times pack was expanded into function and used. This is default trait added to packs created on stack with SF::vpack or packs created on heap with SF::vpackPtr. However, this is not atomic count, but simple integer count, so, using same pack from multiple threads can result in incorrect count of uses due to data races. User can call .useCount method to get number of times pack was used. If this trait is not present useCount returns -1.

Synchronized trait allows to synchronize packs, even if they're used concurrently. Since the only way for third party to get to the contents of the pack is to expand them into function, virtual pack can ensure that this function is always synchronized with std::mutex. Synchronizing pack also synchronizes counted trait, and counted will return accurate count of usage.

Waitable trait allows to block and wait until the pack was used at least once. Implementation uses std::promise and std::future to achieve this.

On ready trait calls specific function always after pack was used.

Traits usually only matter when pack is created on the heap, since most of the traits have to do with concurrency.

Let's learn by example. First off, let's create a pack with synchronized and countable traits:

auto vpack = SF::vpackPtrCustom<
    templatious::VPACK_COUNT | // pack bitmask
    templatious::VPACK_SYNCED,
    int // the signature
>(0);

const int ROUNDS = 100000;

auto h1 = std::async(std::launch::async,
    [=]() {
        TEMPLATIOUS_REPEAT( ROUNDS ) {
            vpack->tryCallFunction<int>(
                [](int& i) {
                    ++i;
                }
            );
        }
    });

auto h2 = std::async(std::launch::async,
    [=]() {
        TEMPLATIOUS_REPEAT( ROUNDS ) {
            vpack->tryCallFunction<int>(
                [](int& i) {
                    i += 2;
                }
            );
        }
    });

h1.wait();
h2.wait();

int outResult = vpack->fGet<0>();
int useCount = vpack->useCount();
assert( outResult == ROUNDS * (1 + 2) );
assert( useCount == ROUNDS * 2 );

Here, we create pack with two traits - count trait and synchronized trait. We call SF::vpackPtrCustom and first template argument is a bitmask - traits we want to use. Available enums are:

  • VPACK_COUNT
  • VPACK_WAIT
  • VPACK_SYNCED
  • VPACK_WCALLBACK

for the appropriate traits. We used VPACK_COUNT and VPACK_SYNCED traits here. Next, we specify signature of the pack - one int and we pass an argument for construction of that int. Then, we launch two async functions which concurrently increment the value in the pack. And, asserts make sure that indeed, the result of the sum and usage counts of pack are what we expect if pack is synchronized.

Now, let's say we send a pack across threads to who knows what to do who knows what. Let's say we want to query a database and download a webpage at the same time, without blocking, and only proceed when both queries are finished.

typedef std::shared_ptr<
    templatious::VirtualPack> PackPtr;
void downloadWebpage(PackPtr p);
void queryVisitorCount(PackPtr p);

...

auto pagePack = SF::vpackPtrCustom<
    templatious::VPACK_WAIT,
    const char*,
    std::vector<char>
>("http://www.youtube.com",std::vector<char>());

auto visitorPack = SF::vpackPtrCustom<
    templatious::VPACK_WAIT,
    int
>(-1);

downloadWebpage(pagePack);
queryVisitorCount(visitorPack);

pagePack->wait();
visitorPack->wait();

int count = visitorPack->fGet<0>();
std::vector< char > page =
    std::move(pagePack->fGet<1>());

// do something with the variables

We construct two virtual packs as queries, send them to two methods which, we assume, enqueue and complete those requests in other threads and we simply wait until at least someone used our packs in the functions, assuming they left a response in our packs (of course, wait will return only after someone is done dealing with the pack).

Last but not least, with callback trait. In here, we had to block and wait until our requests were done. What if we already know what to do with the data, and we'd like to take over as soon as someone used our pack, in the same thread that it was used in for extra efficiency?

No problem, consider this example, which reuses the downloadWebpage function from previously:

auto callback =
    [](const TEMPLATIOUS_VPCORE<
    const char*,std::vector<char>
    >& pack)
    {
    auto& vect = pack.fGet<1>();
    // do something with vect
    };

auto vpack = SF::vpackPtrCustomWCallback<
    templatious::VPACK_SYNCED,
    const char*,
    std::vector<char>
>(callback,
  "http://www.youtube.com",
  std::vector<char>());

downloadWebpage(vpack);

Notice that our callback is not particularly pleasant. That is because we have to know explicit type signature and exact type name for the callback argument. This is to avoid performance overhead for unpacking templatious::VirtualPack when we already know it's true type. In C++14 this will be obsolete as TEMPLATIOUS_VPCORE will be eligible to replace with our beloved auto keyword. By default, user can only receive const reference to the pack during callback. Whether we can call the callback with the true type of virtual pack is checked at compile time, not at runtime.

Pack created has VPACK_SYNCED trait, which enforces mutex synchronization. When you are in the callback and you specified synced trait know that you, and only you have the access to this pack at the moment. If anyone else wants it they are waiting in the line after you are finished.

Note that we didn't specify templatious::VPACK_WCALLBACK flag as it is implicit when calling WCallback methods for virtual packs.

Our callback will happen right after someone used the pack in their thread.

Why do we need to specify compile time policies for virtual packs? Well, as it happens the exact memory layout of a pack depends on the policies we specify. If we add counted trait to a pack, there's a space reserved for one extra integer in virtual pack structure. If we don't add counted trait, space is preserved as that integer doesn't exist at all and pack simply returns -1. If we specify that we need synchronization in our pack, then pack has mutex in it, if we don't - it doesn't. Same goes for waitable, if we use waitable trait, pack contains promise and future of void in itself. Virtual pack with callback also reserves space in struct for callback only and only if we use callback.

This could be potential memory layout of a virtual pack for <int,char> signature on 32 bit machine:

[vtable pointer - 4 bytes]
[hash of virtual pack - 4 bytes]
[optional - callback for callback trait]
[optional - std::promise and std::future for waitable trait]
[optional - std::mutex for synchronized trait]
[optional - integer for countable trait]
[constness info, std::bitset<32> - 4 bytes]
[array of std::type_index with 2 elements - 8 bytes]
[our int - 4 bytes]
[our char - 1 byte (+3 for align)]

Everything with optional may exist or not exist and conserve memory depending on the virtual pack creation policies specified. So, our virtual pack can be as slim as 28 bytes or as big as 80 bytes with every trait (measured on g++-4.9 32 bit), but, nevertheless, it is always contiguous in memory and on the heap it will be allocated with one allocation and deallocated with one deallocation.

Happy packing!