Synchronization with the Concurrency Runtime - Part 3

In my previous posts, I addressed the motivation behind using concurrency runtime’s synchronization primitives and also introduced Critical Section and reader writer lock. In this blog, I will cover concurrency runtime’s event.

Event

This is a bi-state type class which, unlike Critical Section or Reader Writer Lock, does not protect access to shared data. Events synchronize flow of execution and use concurrency runtime’s facilities to enable cooperative schedule of work. They behave similar to Win32 manual-reset event. The main difference between the concurrency runtime’s event and Win32 event is that the concurrency runtime’s event are designed to cooperatively yield to other cooperative tasks in the runtime when blocked in addition to preempting whereas Win32 events are, by design purely pre-emptive in nature.

 

Example:

 This example enables the scheduler to create two threads and then calls DemoEvent function that takes event class and a Win32 manual-reset event class as template parameters. The Demo function first creates several tasks that simulate some work and then wait for a shared event to become signaled.

 

Special thanks to Rick Molloy for sharing this example.

// event.cpp : Defines the entry point for the console application.

//

// compile with: /EHsc

#include <windows.h>

#include <concrt.h>

#include <concrtrm.h>

#include <ppl.h>

using namespace Concurrency;

using namespace std;

class WindowsEvent

{

    HANDLE m_event;

public:

    WindowsEvent()

        :m_event(CreateEvent(NULL,TRUE,FALSE,TEXT("WindowsEvent")))

    {

    }

    ~WindowsEvent()

    {

        CloseHandle(m_event);

    }

    void set()

    {

        SetEvent(m_event);

    }

    void wait(int count = INFINITE)

    {

        WaitForSingleObject(m_event,count);

    }

};

template<class EventClass>

void DemoEvent()

{

    EventClass e;

    LONG volatile taskCtr = 0;

    //create a taskgroup and schedule multiple copies of the task

    task_group tg;

    for(int i = 0;i < 8; ++i)

        tg.run([&e, &taskCtr]{

            //Simulate some work

            Sleep(100);

            printf_s("\tTask %d waiting for the event\n", InterlockedIncrement(&taskCtr));

            e.wait();

    });

    Sleep(1000);

    printf_s(" Setting the event\n");

    //Set the event

    e.set();

    //wait for the tasks

    tg.wait();

}

void main ()

{

    // Create a scheduler that uses two threads.

    CurrentScheduler::Create(SchedulerPolicy(2, MinConcurrency, 2, MaxConcurrency, 2));

    printf_s("Cooperative Event\n");

    DemoEvent<event>();

    printf_s("Windows Event\n");

    DemoEvent<WindowsEvent>();

}

 

Sample Output:

Cooperative Event

  Task 1 waiting for the event

        Task 2 waiting for the event

        Task 3 waiting for the event

        Task 4 waiting for the event

        Task 5 waiting for the event

  Setting the event

Windows Event

        Task 1 waiting for the event

  Task 2 waiting for the event

  Setting the event

        Task 3 waiting for the event

        Task 4 waiting for the event

        Task 5 waiting for the event

We observe that when using cooperative event, we execute all 5 tasks, each task when waiting on the event that is not set, cooperatively yields so that another task can be scheduled and run in the thread’s quantum. When using windows event, we observe that the 2 tasks scheduled block the thread until the event is set.