I'm often asked how to integrate the new parallel computing libraries in the Visual Studio 2010 Beta into existing C++ projects.
In this column, I'll explain a few of the ways you can use the APIs and classes that are part of the Parallel Pattern Library (PPL), Asynchronous Agents Library, and Concurrency Runtime in your existing projects.
I'll walk through four common scenarios developers face in multithreaded application development and describe how you can be productive right away, using the PPL and Agents Library to make your multithreaded program more efficient and more scalable.
One: Moving Work from a UI Thread to a Background Task
One of the first things you're told to avoid as a Windows developer is hanging the UI thread.
The end-user's experience of an unresponsive window is incredibly unpleasant regardless of whether developers provide their customers with a waiting mouse pointer or Windows provides them with a hung UI in the form of a frosted-glass window.
The guidance we're given is often rather terse: don't perform any blocking calls on the UI thread but instead move these calls to a background thread.
My experience is that this guidance isn't sufficient and that the coding work involved is tedious, error-prone, and rather unpleasant.
In this scenario, I'll provide a simple serial example that shows how to move work by using the existing threading APIs.
Then, I'll provide two approaches for moving work to a background thread using the PPL and Agents Library.
I'll wrap up this scenario by tying the examples back to the specifics of a UI thread.
Move a Long-Running Serial Operation to a Background Thread
So what does it mean to move work to a background thread?
If I have some function that is running a long or potentially blocking operation and I want to move that function to a background thread, a good deal of boilerplate code is involved in the mechanics of actually moving that work, even for something as simple as a single function call such as the one shown here:
void SomeFunction(int x, int y){
LongRunningOperation(x, y);
}
First, you need to package up any state that's going to be used.
Here I'm just packaging up a pair of integers, so I could use a built-in container like a std::vector, a std::pair, or a std::tuple, but more typically what I've seen folks do is package up the values in their own struct or class, like this:
struct LongRunningOperationParams{
LongRunningOperationParams(int x_, int y_):x(x_),y(y_){}
int x;
int y;
}
Then you need to create a global or static function that matches the threadpool or CreateThread signature, unpackages that state (typically by dereferencing a void * pointer), executes the function, and then deletes the data if appropriate.
Here's an example:
DWORD WINAPI LongRunningOperationThreadFunc(void* data){
LongRunningOperationParams* pData =
(LongRunningOperationParams*) data;
LongRunningOperation(pData->x,pData->y);
//delete the data if appropriate
delete pData;
}
Now you can finally get around to actually scheduling the thread with the data, which looks like this:
void SomeFunction(int x, int y){
//package up our thread state
//and store it on the heap
LongRunningOperationParams* threadData =
new LongRunningOperationParams(x,y);
//now schedule the thread with the data
CreateThread(NULL,NULL,&LongRunningOperationThreadFunc,
(void*) pData,NULL);
}
This might not seem like that much more code.
Technically, I've added only two lines of code to SomeFunction, four lines for our class and three lines for the thread function.
But that's actually four times as much code.
We went from three lines of code to 12 lines of code just to schedule a single function call with two parameters.
The last time I had to do something like this, I believe I had to capture approximately eight variables, and capturing and setting all this state becomes quite tedious and prone to error.
If I recall correctly, I found and fixed at least two bugs just in the process of capturing the state and building the constructor.
I also haven't touched on what it takes to wait for the thread to complete, which typically involves creating an event and a call to WaitForSingleObject to track that handle and, of course, cleaning up the handle when you're done with it.
That's at least three more lines of code, and that still leaves out handling exceptions and return codes.
An Alternative to CreateThread: The task_group Class
The first approach I'm going to describe is using the task_group class from the PPL.
If you're not familiar with the task_group class, it provides methods for spawning tasks asynchronously via task_group::run and waiting for its tasks to complete via task_group::wait.
It also provides cancellation of tasks that haven't been started yet and includes facilities for packaging an exception with std::exception_ptr and rethrowing it.
You'll see that significantly less code is involved here than with the CreateThread approach and that from a readability perspective, the code is much closer to the serial example.
The first step is to create a task_group object.
This object needs to be stored somewhere where its lifetime can be managed—for example, on the heap or as a member variable in a class.
Next you use task_group::run to schedule a task (not a thread) to do the work.
Task_group::run takes a functor as a parameter and manages the lifetime of that functor for you.
By using a C++0x lambda to package up the state, this is effectively a two-line change to the program.
Here's what the code looks like:
//a task_group member variable
task_group backgroundTasks;
void SomeFunction(int x, int y){
backgroundTasks.run([x,y](){LongRunningOperation(x, y);});
}
Making Work Asynchronous with the Agents Library
Another alternative is to use the Agents Library, which involves an approach based on message passing.
The amount of code change is about the same, but there's a key semantic difference worth pointing out with an agent-based approach.
Rather than scheduling a task, you build a message-passing pipeline and asynchronously send a message containing just the data, relying on the pipeline itself to process the message.
In the previous case, I'd send a message containing x and y.
The work still happens on another thread, but subsequent calls to the same pipeline are queued, and the messages are processed in order (in contrast to a task_group, which doesn't provide ordering guarantees).
First, you need a structure to contain the message.
You could, in fact, use the same structure as the earlier one, but I'll rename it as shown here:
struct LongRunningOperationMsg{
LongRunningOperationMsg (int x, int y):m_x(x),m_y(y){}
int m_x;
int m_y;
}
The next step is to declare a place to send the message to.
In the Agents Library, a message can be sent to any message interface that is a "target," but in this particular case the most suitable is call<T>.
A call<T> takes a message and is constructed with a functor that takes the message as a parameter.
The declaration and construction of the call might look like this (using lambdas):
call<LongRunningOperationMsg>* LongRunningOperationCall = new
call<LongRunningOperationMsg>([]( LongRunningOperationMsg msg)
{
LongRunningOperation(msg.x, msg.y);
})
The modification to SomeFunction is now slight.
The goal is to construct a message and send it to the call object asynchronously.
The call will be invoked on a separate thread when the message is received:
void SomeFunction(int x, int y){
asend(LongRunningOperationCall, LongRunningOperationMsg(x,y));
}
Getting Work Back onto the UI Thread
Getting work off the UI thread is only half the problem.
Presumably at the end of LongRunningOperation, you're going to get some meaningful result, and the next step is often getting work back onto the UI thread.
The approach to take varies based on your application, but the easiest way to achieve this in the libraries offered in Visual Studio 2010 is to use another pair of APIs and message blocks from the Agents Library: try_receive and unbounded_buffer<T>.
An unbounded_buffer<T> can be used to store a message containing the data and potentially the code that needs to be run on the UI thread.
Try_receive is a nonblocking API call that can be used to query whether there is data to display.
For example, if you were rendering images on your UI thread, you could use code like the following to get data back onto the UI thread after making a call to InvalidateRect:
unbounded_buffer<ImageClass>* ImageBuffer;
LONG APIENTRY MainWndProc(HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
RECT rcClient;
int i;
...
ImageClass image;
//check the buffer for images and if there is one there, display it.
if (try_receive(ImageBuffer,image))
DisplayImage(image);
...
}
Some details, like the implementation of the message loop, have been omitted here, but I hope this section was instructive enough to demonstrate the technique.
I encourage you to check the sample code for the article, which has a full working example of each of these approaches.
Figure 1 A Non-Thread-Safe Class
samclass
Widget{
size_t m_width;
size_t m_height;
public:
Widget(size_t w, size_t h):m_width(w),m_height(h){};
size_t GetWidth(){
return m_width;
}
size_t GetHeight(){
return m_height;
}
void SetWidth(size_t width){
m_width = width;
}
void SetHeight(size_t height){
m_height = height;
}
};
Two: Managing Shared State with Message Blocks and Agents
Another common situation in multithreaded application development is managing shared state.
Specifically, as soon as you try to communicate or share data between threads, managing shared state quickly becomes a problem you need to deal with.
The approach I've often seen is to simply add a critical section to an object to protect its data members and public interfaces, but this soon becomes a maintenance problem and sometimes can become a performance problem as well.
In this scenario, I'll walk through a serial and naïve example using locks, and then I'll show an alternative using the message blocks from the Agents Library.
Locking a Simple Widget Class
Figure 1 shows a non-thread-safe Widget class with width and height data members and simple methods that mutate its state.
The naïve approach to making the Widget class thread safe is to protect its methods with a critical section or reader-writer lock.
The PPL contains a reader_writer_lock, and Figure 2 offers a first look at the obvious solution to the naïve approach: using the reader_writer_lock in the PPL.
Figure 2 Using the reader_writer_lock from the Parallel Pattern Library
class LockedWidget{
size_t m_width;
size_t m_height;
reader_writer_lock lock;
public:
LockedWidget (size_t w, size_t h):m_width(w),m_height(h){};
size_t GetWidth(){
auto lockGuard = reader_writer::scoped_lock_read(lock);
return m_width;
}
size_t GetHeight(){
auto lockGuard = reader_writer::scoped_lock_read(lock);
return m_height;
}
void SetWidth(size_t width){
auto lockGuard = reader_writer::scoped_lock(lock);
m_width = width;
}
void SetHeight(size_t height){
auto lockGuard = reader_writer::scoped_lock(lock)
m_height = height;
}
};
What I've done here is add a read_writer_lock as a member variable and then decorate all appropriate methods with either the reader or the writer version of the lock.
I'm also using scoped_lock objects to ensure that the lock isn't left held in the midst of an exception.
All the Get methods now acquire the reader lock, and the Set methods acquire the write lock.
Technically, this approach looks like it is correct, but the design is actually incorrect and is fragile overall because its interfaces, when combined, are not thread safe.
Specifically, if I have the following code, I'm likely to have corrupt state:
Thread1{
SharedWidget.GetWidth();
SharedWidget.GetHeight();
}
Thread2{
SharedWidget.SetWidth();
SharedWidget.SetHeight();
}
Because the calls on Thread1 and Thread2 can be interleaved, Thread1 can acquire the read lock for GetWidth, and then before GetHeight is called, SetWidth and SetHeight could both execute.
So, in addition to protecting the data, you have to ensure that the interfaces to that data are also correct; this is one of the most insidious kinds of race conditions because the code looks correct and the errors are very difficult to track down.
Naïve solutions I've seen for this situation often involve introducing a lock method on the object itself—or worse, a lock stored somewhere else that developers need to remember to acquire when accessing that widget.
Sometimes both approaches are used.
An easier approach is to ensure that interfaces can be interleaved safely without exposing this ability to tear the state of the object between interleaved calls.
You might decide to evolve your interface as shown in Figure 3 to provide GetDimensions and UpdateDimensions methods.
This interface is now less likely to cause surprising behavior because the methods don't allow exposing unsafe interleavings.
Figure 3 A Version of the Interface with GetDimensions and UpdateDimensions Methods
struct WidgetDimensions
{
size_t width;
size_t height;
};
class LockedWidgetEx{
WidgetDimensions m_dimensions;
reader_writer_lock lock;
public:
LockedWidgetEx(size_t w, size_t h):
m_dimensions.width(w),m_dimensions.height(h){};
WidgetDimensions GetDimensions(){
auto lockGuard = reader_writer::scoped_lock_read(lock);
return m_dimensions;
}
void UpdateDimensions(size_t width, size_t height){
auto lockGuard = reader_writer::scoped_lock(lock);
m_dimensions.width = width;
m_dimensions.height = height;
}
};
Managing Shared State with Message Blocks
Now let's take a look at how the Agents Library can help make managing shared state easier and the code a little more robust.
The key classes from the Agents Library that are useful for managing shared variables are overwrite_buffer<T>, which stores a single updatable value and returns a copy of the latest value when receive is called; single_assignment<T>, which stores and returns a copy of a single value when receive is called but, like a constant, can be assigned only once; and unbounded_buffer<T>, which stores an unlimited number of items (memory permitting) and, like a FIFO queue, dequeues the oldest item when receive is called.
I'll start by using an overwrite_buffer<T>.
In the Widget class, I'll first replace the m_dimensions member variable with overwrite_buffer<WidgetDimensions>, and then I'll remove the explicit locks from the methods and replace them with the appropriate send and receive calls.
I still need to worry about our interface being safe, but I no longer have to remember to lock the data.
Here's how this looks in code.
It's actually slightly fewer lines of code than the locked version and the same number of lines as the serial version:
class AgentsWidget{
overwrite_buffer<WidgetDimensions> m_dimensionBuf;
public:
AgentsWidget(size_t w, size_t h){
send(&m_dimensionBuf,WidgetDimensions(w,h));
};
WidgetDimensions GetDimensions(){
return receive(&m_dimensionBuf);
}
void UpdateDimensions(size_t width, size_t height){
send(&m_dimensionBuf,WidgetDimensions(w,h));
}
};
There's a subtle semantic difference here from the reader_writer lock implementation.
The overwrite_buffer allows a call to UpdateDimensions to occur during a call to Dimensions.
This allows practically no blocking during these calls, but a call to GetDimensions may be slightly out of date.
It's worth pointing out that the problem existed in the locked version as well, because as soon as you get the dimensions, they have the potential to be out of date.
All I've done here is remove the blocking call.
An unbounded_buffer can also be useful for the Widget class.
Imagine that the subtle semantic difference I just described was incredibly important.
For example, if you have an instance of an object that you want to ensure is accessed by only one thread at a time, you can use unbounded_buffer as an object holder that manages access to that object.
To apply this to the Widget class, you can remove m_dimensions and replace it with unbounded_buffer<WidgetDimension> and use this buffer via the calls to GetDimensions and UpdateDimensions.
The challenge here is to ensure that no one can get a value from our widget while it is being updated.
This is achieved by emptying the unbounded_buffer so that calls to GetDimension will block waiting for the update to occur.
You can see this in Figure 4.
Both GetDimensions and UpdateDimensions block, waiting for exclusive access to the dimensions variable.
Figure 4 Emptying the Unbounded_Buffer
class AgentsWidget2{
unbounded_buffer<WidgetDimensions> m_dimensionBuf;
public:
AgentsWidget2(size_t w, size_t h){
send(&m_dimensionBuf,WidgetDimensions(w,h));
};
WidgetDimensions GetDimensions(){
//get the current value
WidgetDimensions value = receive(&m_dimensionBuf);
//and put a copy of it right back in the unbounded_buffer
send(&m_dimensionBuf,value);
//return a copy of the value
return WidgetDimensions(value);
}
void UpdateDimensions (size_t width, size_t height){
WidgetDimensions oldValue = receive(&m_dimensionBuf);
send(&m_dimensionBuf,WidgetDimensions(width,height));
}
};
It's Really About Coordinating Access to the Data
I want to stress one more thing about our Widget class: ensuring that methods and data that can be accessed concurrently work "safely" together is critical.
Often, this can be achieved by coordinating access to state rather than by locking methods or objects.
From a pure "lines of code" perspective, you won't see a big win over the locked example, and, in particular, the second example might even have a little more code.
What is gained, however, is a safer design, and with a little thought, you can often modify serial interfaces so that the internal state of the object isn't "torn." In the Widget example, I did this by using message blocks, and I was able to protect that state in such a way that it is safer.
Adding methods or functionality to the Widget class in the future is less likely to destroy the internal synchronization we've set up.
With a member lock, it's pretty easy to simply forget to lock the lock when a method is added on a class.
But moving operations to a message-passing model and using message blocks such as the overwrite buffer in their natural way can often keep data and classes synchronized.
Three: Using Combinable for Thread Local Accumulations and Initialization
The second scenario, in which we protected access to an object with locks or message blocks, works very well for heavier weight objects that are accessed infrequently.
If while reading that example you thought that there might be a performance problem if the synchronized widget were used in a tight (and parallel) loop, you're probably right.
That's because protecting shared state can be problematic, and for completely general purpose algorithms and objects that truly share state, there unfortunately aren't a lot of options other than to coordinate access or introduce a lock.
But you can almost always find a way to refactor code or an algorithm to relax the dependency on shared state, and once you've done this, a few specific but common patterns in which an object calls combinable<T> in the Parallel Pattern Library can really help out.
Combinable<T> is a concurrent container that offers support for three broad use cases: holding a thread-local variable or performing thread-local initialization, performing associative binary operations (like sum, min, and mix) on the thread-local variables and combining them, and visiting each thread-local copy with an operation (like splicing lists together).
In this section, I'll explain each of these cases and provide examples of how to use them.
Holding a Thread-Local Variable or Performing Thread-Local Initialization
The first use case I mentioned for combinable<T> was for holding a thread-local variable.
It is relatively common to store a thread-local copy of global state.
For example, in the colorized ray tracers applications like the one in our sample pack (code.msdn.microsoft.com/concrtextras) or in the samples for parallel development with .NET 4.0 (code.msdn.microsoft.com/ParExtSamples) there is an option to colorize each row by thread to visualize the parallelism.
In the native version of the demo, this is done by using a combinable object that holds the thread-local color.
You can hold a thread-local variable, of course, by using thread-local storage (TLS), but there are some disadvantages—most notably lifetime management and visibility, and these go hand in hand.
To use TLS, you first need to allocate an index with TlsAlloc, allocate your object, and then store a pointer to your object in the index with TlsSetValue.
Then, when your thread is exiting, you need to ensure that your object is deallocated.
(TlsFree is called automatically.) Doing this once or twice per thread and ensuring that there aren't any leaks because of early exits or exceptions isn't that challenging, but if your application needs dozens or hundreds of these items, a different approach is likely better.
Combinable<T> can be used to hold a thread-local value as well, but the lifetimes of the individual objects are tied to the lifetime of the combinable<T> item, and much of the initialization is automated.
You access the thread-local value simply by calling the combinable::local method, which returns a reference to the local object.
Here's an example using task_group, but this can be done with Win32 threads as well:
combinable<int> values;
auto task = [&](){
values.local() = GetCurrentThreadId();
printf("hello from thread: %d\n",values.local());
};
task_group tasks;
tasks.run(task);
//run a copy of the task on the main thread
task();
tasks.wait();
I mentioned that thread-local initialization can also be achieved with combinable.
If, for example, you need to initialize a library call on each thread on which it is used, you can create a class that performs the initialization in its constructor.
Then, on the first use per thread, the library call will be made, but it will be skipped on subsequent uses.
Here's an example:
class ThreadInitializationClass
{
public:
ThreadInitializationClass(){
ThreadInitializationRoutine();
};
};
...
//a combinable object will initialize these
combinable<ThreadInitializationClass> libraryInitializationToken;
...
//initialize the library if it hasn't been already on this thread
ThreadInitializationClass& threadInit = libraryInitalizationToken.local();
Performing Reductions in a Parallel Loop
Another major scenario for the combinable object is to perform thread-local reductions, or thread-local accumulations.
Specifically, you can avoid a particular type of race condition when parallelizing loops or in recursive parallel traversals with combinable.
Here's an incredibly naïve example that's not intended to show speed-ups.
The following code shows a simple loop that looks like it can be parallelized with parallel_for_each, except for access to the sum variable:
int sum = 0;
for (vector<int>::iterator it = myVec.begin(); it != myVec.end(); ++it) {
int element = *it;
SomeFineGrainComputation(element);
sum += element;
}
Now, rather than placing a lock in our parallel_for_each, which destroys any chance we had of speed-ups, we can use a combinable object to calculate the thread-local sums:
combinable<int> localSums;
parallel_for_each(myVec.begin(), myVec.end(), [&localSums] (int element) {
SomeFineGrainComputation(element);
localSums.local() += element;
});
We've now successfully avoided the race condition, but we have a collection of thread-local sums stored in the localSums object, and we still need to extract the final value.
We can do this with the combine method, which takes a binary functor like the following:
int sum = localSums.combine(std::plus<int>);
The third use case for combinable<T>, which involves using the combine_each method, is when you need to visit each of the thread-local copies and perform some operation on them (like cleanup or error checking).
Another, more interesting example is when your combinable object is a combinable<list<T>>, and in your threads you are building up std::lists or std::sets.
In the case of std::lists, they can easily be spliced together with list::splice; with std::sets, they can be inserted with set::insert.
Four: Converting an Existing Background Thread to an Agent or a Task
Suppose you already have a background or worker thread in your application.
There are some very good reasons why you might want to convert that background thread to a task from the PPL or to an agent, and doing so is relatively straightforward.
Some of the major advantages of doing this include the following:
Composability and performance. If your worker threads are compute intensive and you are considering using additional threads in the PPL or Agents Library, converting your background thread to a worker task allows it to cooperate with the other tasks in the runtime and avoid oversubscription on the system.
Cancellation and exception handling. If you want to be able to easily cancel work on a thread or have a well-described mechanism for handling exceptions, a task_group has these capabilities built in.
Control flow and state management. If you need to manage the state of your thread (started or completed, for example) or have an object whose state is effectively inseparable from the worker thread, implementing an agent might be useful.
Task_group Offers Cancellation and Exception Handling
In the first scenario, we explored what it takes to schedule work with a task_group: essentially packaging your work into a functor (using a lambda, an std::bind or a custom function object) and scheduling it with the task_group::run method.
What I didn't describe was the cancellation and exception-handling semantics, which are, in fact, related.
Figure 5 Implementation of MyAgentClass
class MyAgentClass : public agent{
public:
MyAgentClass (){
}
AgentsWidget widget;
void run(){
//run is started asynchronously when agent::start is called
//...
//set status to complete
agent::done();
}
};
First, I'll explain the straightforward semantics.
If your code makes a call to task_group::cancel or a task throws an uncaught exception, cancellation is in effect for that task_group.
When cancellation is in effect, tasks that haven't been started on that task_group won't be started, which allows scheduled work to easily and quickly be canceled on a task_group.
Cancellation doesn't interrupt tasks that are running or blocked, so a running task can query the cancellation status with the task_group::is_canceling method or by the helper function
is_current_task_group_canceling.
Here's a brief example:
task_group tasks;
tasks.run([](){
...
if(is_current_task_group_canceling())
{
//cleanup then return
...
return;
}
});
tasks.cancel();
tasks.wait();
Exception handling impacts cancellation because an uncaught exception in a task_group triggers cancellation on that task_group.
If there is an uncaught exception, the task_group will actually use std::exception_ptr to package up the exception on the thread it was thrown on.
Later, when task_group::wait is called, the exception is rethrown on the thread that called wait.
Implementing an Asynchronous Agent
The Agents Library offers an alternative to using a task_group: replacing a thread with the agent base class.
If your thread has a lot of thread-specific state and objects, an agent might be a better fit for the scenario.
The abstract agent class is an implementation of the actor pattern; the intended usage is to implement your own class derived from agent and then encapsulate any state that your actor (or thread) may have into that agent.
If there are fields that are intended to be publicly accessible, the guidance is to expose them as message blocks or sources and targets and use message passing to communicate with the agent.
Implementing an agent requires deriving a class from the agent base class and then overriding the virtual method run.
The agent can then be started by calling agent::start, which spawns the run method as a task, much like a thread.
The advantage is that thread-local state can now be stored in the class.
This allows for easier synchronization of state between threads, particularly if the state is stored in a message block.
Figure 5 shows an example of an implementation that has a publicly exposed member variable of type AgentsWidget.
Note that I've set the agent's status to done as the run method is exiting.
This allows the agent to not only be started but also be waited on.
Furthermore, the agent's current status can be queried by a call to agent::status.
Starting and waiting on our agent class is straightforward, as the following code shows:
MyAgentClass MyAgent;
//start the agent
MyAgent.start();
//do something else
...
//wait for the agent to finish
MyAgent.wait(&MyAgent);
Bonus Item: Sorting in Parallel with parallel_sort
Finally, I'd like to suggest another potentially easy point of parallelization, this time not from the PPL or the Agents Library but from our sample pack available at code.msdn.microsoft.com/concrtextras.
Parallel quicksort is one of the examples we use for explaining how to parallelize recursive divide-and-conquer algorithms with tasks, and the sample pack contains an implementation of parallel quicksort.
Parallel sort can show speed-ups if you're sorting a large number of items where the comparison operation is somewhat expensive, as with strings.
It probably won't show speed-ups for small numbers of items or when sorting built-in types like integers and doubles.
Here's an example of how it can be used:
//from the sample pack
#include "parallel_algorithms.h"
int main()
using namespace concurrency_extras;
{
vector<string> strings;
//populate the strings
...
parallel_sort(strings.begin(),strings.end());
}
Wrapping Up
I hope this column helps expand the horizons of how the parallel libraries in Visual Studio 2010 will apply to your projects, beyond simply using parallel_for or tasks to speed up compute-intensive loops.
You'll find many other instructive examples in our documentation on MSDN (msdn.microsoft.com/library/dd504870(VS.100).aspx) and in our sample pack (code.msdn.microsoft.com/concrtextras) that help illustrate the parallel libraries and how they can be used.
I encourage you to check them out.
Rick Molloy
is a program manager on the Parallel Computing Platform team at Microsoft.
|
Ich werde oft gefragt, wie Sie die neue Parallel computing Bibliotheken in Visual Studio 2010 Beta in vorhandene C++-Projekte integrieren.
In diesem Artikel werde ich erläutern nur einige der Verwendungsmöglichkeiten für die APIs und Klassen, die Teil der Parallel Pattern Library (PPL), asynchrone Agents Library und Parallelität Runtime in Ihre vorhandenen Projekten sind.
Ich werde durchlaufen vier allgemeine Szenarien Entwickler in Multithreadanwendung Entwicklung gegenüberstehen, und beschreiben, wie Sie produktiv sofort, sein können, mit dem PPL und Agents Bibliothek Ihre Multithreadprogramme effizienter und skalierbarer machen.
Eine: Verschieben von Arbeit von UI-Thread in ein Hintergrund-Task
Eines der ersten Dinge, die Sie angewiesen sind, als Windows-Entwickler zu vermeiden ist den Benutzeroberflächenthread hängende.
Kontakt des Endbenutzers eines nicht reagierenden Fensters ist unglaublich unangenehmen unabhängig davon, ob Entwickler ihre Kunden mit einem Mauszeiger warten bereitstellen oder Windows bietet Ihnen eine nicht reagierende Benutzeroberfläche in Form von einem frosted Glas-Fenster.
Die Anleitung, die wir angegeben haben, ist oft ziemlich kurz gefassten: keine blockierende Aufrufe auf dem Benutzeroberflächenthread ausführen, jedoch stattdessen diese Aufrufe an einen Hintergrundthread verschieben.
Meiner Erfahrung ist, dass dieser Anleitung ausreichend nicht und, dass die Codierung Arbeit beteiligten mühsam, fehleranfällig und ziemlich unangenehme.
In diesem Szenario werde ich ein einfaches Beispiel serielles bereitstellen, das zum Arbeiten mit der vorhandenen threading-APIs verschieben.
Anschließend werde ich zwei Ansätze zum Verschieben von Arbeit in einem Hintergrundthread mithilfe der PPL und Agents Bibliothek geben.
Ich werde in diesem Szenario umbrochen, durch Blockieren der Beispiele zurück zu den Besonderheiten in einem UI-Thread.
Verschieben Sie eine serielle lang andauernde Operation in einem Hintergrund-Thread
So was bedeutet es, arbeiten in einem Hintergrundthread zu verschieben?
Wenn eine Funktion, die eine lange ausgeführt oder potenziell blockierende Operation besteht und Sie möchten die Funktion in einem Hintergrundthread zu verschieben, ein gutes Angebot von Codebausteinen wird Code die Mechanismen der tatsächlich verschoben, dass die Arbeit beteiligt, auch für etwas so einfach wie eine einzelne Funktion aufrufen, wie die hier gezeigte:
void SomeFunction(int x, int y){
LongRunningOperation(x, y);
}
Zunächst müssen Sie einen beliebigen Zustand zu verpacken, die verwendet werden, geht.
Hier bin ich einfach Verpacken von ein Paar von ganzen Zahlen, so Verwendung konnte einen integrierten Container wie eine Std:: Vector, ein std::pair oder einer std::tuple, aber in der Regel was ich gesehen habe lediglich Leute führen Paket die Werte in Ihrer eigenen Struktur oder Klasse, wie folgt:
struct LongRunningOperationParams{
LongRunningOperationParams(int x_, int y_):x(x_),y(y_){}
int x;
int y;
}
Müssen Sie erstellen eine globale oder statische Funktion, die der Threadpool oder CreateThread Signatur entspricht, unpackages, Zustand (i. d. r. durch Dereferenzieren eines Void * Zeiger) führt die Funktion, und löscht dann die Daten bei Bedarf.
Beispiel:
DWORD WINAPI LongRunningOperationThreadFunc(void* data){
LongRunningOperationParams* pData =
(LongRunningOperationParams*) data;
LongRunningOperation(pData->x,pData->y);
//delete the data if appropriate
delete pData;
}
Jetzt können Sie schließlich abrufen um tatsächlich Planung den Thread mit der Daten wie folgt aussieht:
void SomeFunction(int x, int y){
//package up our thread state
//and store it on the heap
LongRunningOperationParams* threadData =
new LongRunningOperationParams(x,y);
//now schedule the thread with the data
CreateThread(NULL,NULL,&LongRunningOperationThreadFunc,
(void*) pData,NULL);
}
Dies mag nicht wie viel Code.
Technisch gesehen habe ich nur zwei Codezeilen SomeFunction, für unsere Klasse vier Zeilen und drei Zeilen für die Thread-Funktion hinzugefügt.
Aber eigentlich vier Mal so viel Code.
Wir wurden aus drei Zeilen von Code zu 12 Codezeilen genau so planen Sie einen einzigen Funktionsaufruf mit zwei Parametern.
Das letzte Mal, das musste ich etwas sieht, glauben ich musste ungefähr acht Variablen zu erfassen, und erfassen und Festlegen dieser Zustand wird recht mühsam und fehleranfällig.
Wenn ich korrekt erinnern, können Sie gefunden und behoben mindestens zwei Fehler nur im Prozess der Erfassen des Status und des Konstruktors erstellen.
Ich noch nicht auch betroffen, auf welche dauert der Thread abgeschlossen haben, warten, die in der Regel umfasst das Erstellen eines Ereignisses und ein Aufruf von WaitForSingleObject das Handle zu verfolgen und natürlich das Handle zu bereinigen, wenn Sie damit fertig sind.
Mindestens drei weitere Codezeilen ist, und bleiben weiterhin, Behandlung von Ausnahmen und Rückgabecodes.
Alternative zu CreateThread: Task_group-Klasse
Der erste Ansatz werde beschreiben verwendet die Task_group-Klasse von der PPL.
Wenn Sie nicht mit der Task_group-Klasse vertraut sind, stellt Methoden zum Starten von Aufgaben asynchron über task_group::run und Warten auf seine Aufgaben abgeschlossen über task_group::wait bereit.
Außerdem bietet Abbruch der Aufgaben, die noch noch nicht gestartet wurde und enthält Funktionen für eine Ausnahme mit std::exception_ptr Verpacken und erneute Auslösen es.
Sehen Sie, dass erheblich weniger Code hier beteiligt ist als mit der CreateThread Ansatz, die vom Standpunkt der Lesbarkeit, der Code seriellen Beispiel wesentlich ähnlicher ist.
Die erste Schritt besteht darin, ein Task_group-Objekt zu erstellen.
Dieses Objekt muss gespeichert, an die Stelle, wo seine Lebensdauer verwaltet werden können – z. B. auf dem Heap oder als eine Member-Variable in einer Klasse.
Als Nächstes verwenden Sie task_group::run zum Planen eines Tasks (nicht Thread), um die Arbeit zu erledigen.
Task_group::Run einer Functor als Parameter akzeptiert und verwaltet die Lebensdauer der Functor für Sie.
Mithilfe von C ++ 0 X Lambda, den Status zu verpacken Dies ist praktisch eine zweizeilige-Änderung an das Programm.
Hier ist wie der Code aussieht:
//a task_group member variable
task_group backgroundTasks;
void SomeFunction(int x, int y){
backgroundTasks.run([x,y](){LongRunningOperation(x, y);});
}
Arbeiten vornehmen mit der Bibliothek Agents asynchrone
Eine Alternative ist die Bibliothek Agents verwenden, die einen Ansatz auf Grundlage Nachrichtenübergabe umfasst.
Der Umfang der Codeänderung ungefähr gleich ist, aber es gibt eine semantische Hauptunterschied durch einen Agent basierenden Ansatz hingewiesen.
Anstatt Planen eines Tasks, Sie erstellen eine Pipeline Weiterleiten von Nachrichten und asynchron eine Nachricht zu senden, die nur die Daten enthält abhängig von der Pipeline selbst, um die Nachricht zu verarbeiten.
Im vorherigen Fall möchte ich eine Nachricht mit x und y senden.
Die Arbeit geschieht weiterhin auf einem anderen Thread aber nachfolgende Aufrufe an die gleiche Rohrleitung Warteschlange und die Nachrichten werden in Reihenfolge (im Gegensatz zu einer Task_group, die Reihenfolge garantiert nicht) verarbeitet.
Zunächst benötigen Sie eine Struktur, die Nachricht enthalten.
Sie könnten dieselbe Struktur tatsächlich wie die früheren verwenden, aber ich werde es umbenennen, wie hier gezeigt:
struct LongRunningOperationMsg{
LongRunningOperationMsg (int x, int y):m_x(x),m_y(y){}
int m_x;
int m_y;
}
Die nächste Schritt ist, einen Ort zum Senden der Nachricht zu deklarieren.
In der Bibliothek Agents kann eine Nachricht an jede Nachricht Schnittstelle gesendet werden, die eine "Ziel"in diesem speziellen Fall am besten geeigneten ist jedoch Aufruf < T >.
Ein Aufruf < T >akzeptiert eine Nachricht und wird mit einer Functor, die die Nachricht als Parameter annimmt.
Die Deklaration und Konstruktion des Aufrufs könnte folgendermaßen aussehen (mit Lambdas):
call<LongRunningOperationMsg>* LongRunningOperationCall = new
call<LongRunningOperationMsg>([]( LongRunningOperationMsg msg)
{
LongRunningOperation(msg.x, msg.y);
})
Die Änderung zu SomeFunction ist jetzt leichte.
Das Ziel ist eine Nachricht erstellen und senden es asynchron an Aufrufobjekts.
Der Aufruf wird auf einem separaten Thread aufgerufen werden, beim Empfang der Meldung:
void SomeFunction(int x, int y){
asend(LongRunningOperationCall, LongRunningOperationMsg(x,y));
}
Erste Aufgaben zurück auf das UI-Thread
Abrufen von Arbeit aus dem UI-Thread ist nur die Hälfte des Problems.
Vermutlich am Ende der LongRunningOperation Sie nun einige sinnvollen Ergebnis zu erzielen, und die nächste Schritt erhält häufig Arbeit wieder auf dem Benutzeroberflächenthread.
Das Verfahren zum Übernehmen hängt von der Anwendung, aber die einfachste Methode dafür in den Bibliotheken in Visual Studio 2010 angeboten ist ein weiteres Paar von APIs und Nachricht blockiert aus der Bibliothek für Agents verwenden: Try_receive und Unbounded_buffer < T >.
Ein Unbounded_buffer < T >kann verwendet werden, zum Speichern einer Nachricht mit den Daten und potenziell den Code, der auf dem Benutzeroberflächenthread ausgeführt werden muss.
Try_receive ist ein nicht blockierender API-Aufruf, der abzufragen, ob Daten zum Anzeigen verwendet werden können.
Wenn Sie auf dem Benutzeroberflächenthread Rendering Bilder, konnte Sie beispielsweise Code wie den folgenden verwenden, um Daten wieder auf dem Benutzeroberflächenthread erhalten, nachdem ein Aufruf an InvalidateRect:
unbounded_buffer<ImageClass>* ImageBuffer;
LONG APIENTRY MainWndProc(HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam)
{
RECT rcClient;
int i;
...
ImageClass image;
//check the buffer for images and if there is one there, display it.
if (try_receive(ImageBuffer,image))
DisplayImage(image);
...
}
Einige Details wie die Implementierung von der Meldungsschleife wurden hier weggelassen wurde, aber hoffe ich in diesem Abschnitt wurde hilfreich genug, um das Verfahren zu veranschaulichen.
Fangen Sie an den Beispielcode für den Artikel, überprüfen Sie über eine vollständige funktionsfähiges Beispiel der einzelnen Ansätze.
Abbildung 1 ein nicht-threadsichere Klasse
samclass
Widget{
size_t m_width;
size_t m_height;
public:
Widget(size_t w, size_t h):m_width(w),m_height(h){};
size_t GetWidth(){
return m_width;
}
size_t GetHeight(){
return m_height;
}
void SetWidth(size_t width){
m_width = width;
}
void SetHeight(size_t height){
m_height = height;
}
};
Verwalten von freigegebenen Status Message Blocks mit Agents
Eine weitere häufig auftretende Situation in Multithreadanwendung Entwicklung ist gemeinsam genutzten Zustand verwalten.
Insbesondere, sobald Sie versuchen, die Kommunikation oder Freigeben von Daten zwischen Threads wird schnell Verwalten von gemeinsam genutzten Zustands zu einem Problem, dem müssen Sie den Umgang mit.
Der Ansatz, den ich häufig gesehen habe, ist so einfach einen kritischen Abschnitt ein Objekt, seine Daten-Member und öffentliche Schnittstellen schützen hinzu, aber diese bald wird ein Problem Wartung und manchmal kann ein Leistungsproblem werden.
In diesem Szenario werde ich ein Serien- und vereinfachte Beispiel mithilfe von Sperren durchlaufen, und ich werde dann zeigen, dass Alternative mit Message Blocks aus der Bibliothek für Agents.
Sperren einer einfachen Widget-Klasse
Abbildung 1 zeigt eine nicht threadsichere Widget-Klasse mit Breite und Höhe die Datenmember und einfache Methoden, die den Zustand zu ändern.
Der naive Ansatz für das vornehmen des Widget-Klasse Threads abgesicherten besteht darin, seine Methoden mit einem kritischen Abschnitt oder die Reader / Writer-Sperre zu schützen.
Die PPL enthält eine Reader_writer_lock und Abbildung 2 bietet einen ersten Einblick in die offensichtliche Lösung der naive Ansatz: verwenden die Reader_writer_lock in die PPL.
Abbildung 2 mit Reader_writer_lock aus der Parallel Pattern Library
class LockedWidget{
size_t m_width;
size_t m_height;
reader_writer_lock lock;
public:
LockedWidget (size_t w, size_t h):m_width(w),m_height(h){};
size_t GetWidth(){
auto lockGuard = reader_writer::scoped_lock_read(lock);
return m_width;
}
size_t GetHeight(){
auto lockGuard = reader_writer::scoped_lock_read(lock);
return m_height;
}
void SetWidth(size_t width){
auto lockGuard = reader_writer::scoped_lock(lock);
m_width = width;
}
void SetHeight(size_t height){
auto lockGuard = reader_writer::scoped_lock(lock)
m_height = height;
}
};
Was ich hier getan haben ist ein Read_writer_lock als eine Membervariable hinzufügen und ergänzen alle entsprechende Methoden mit entweder dem Leser oder die Writer-Version der Sperre.
Ich bin auch sicherzustellen, dass die Sperre ist nicht mit Scoped_lock-Objekten mitten in eine Ausnahme gehalten.
Die Get-Methoden jetzt die Lesesperre erwerben und Set-Methoden die Schreibsperre erhalten.
Technisch gesehen anscheinend dieser Ansatz ist richtig, aber das Design ist tatsächlich falsch und zerbrechliche insgesamt da seiner Schnittstellen kombiniert, nicht threadsicher sind.
Insbesondere, wenn sich den folgenden Code bin ich wahrscheinlich fehlerhaft:
Thread1{
SharedWidget.GetWidth();
SharedWidget.GetHeight();
}
Thread2{
SharedWidget.SetWidth();
SharedWidget.SetHeight();
}
Da die Aufrufe von Thread1 und Thread2 verzahnt sein können, können Thread1 die Lesesperre für GetWidth erwerben und dann vor dem Aufruf von GetHeight SetWidth und SetHeight konnte beide ausführen.
Damit, zusätzlich zum Schutz der Daten, Sie sicherstellen, dass die Schnittstellen an die Daten auch; korrekt sindDies ist eine der heimtückischsten Arten von Racebedingungen, da der Code korrekt sieht und die Fehler sehr schwierig sind, aufspüren.
Naive Lösungen für diese Situation häufig angezeigt beinhalten eine Lock-Methode auf das Objekt selbst, oder schlimmer noch, eine Sperre gespeicherten woanders, die Entwickler daran denken, beim Zugriff auf das Widget erwerben zu müssen.
Manchmal werden beide Ansätze verwendet.
Ein einfacher Ansatz wird sichergestellt, dass Schnittstellen sicher verzahnt werden können, ohne die Möglichkeit, den Zustand des Objekts zwischen überlappende Aufrufe einzureißen auszusetzen.
Möglicherweise möchten Sie Ihre Schnittstelle weiterentwickelt, wie im Abbildung 3 Methoden GetDimensions und UpdateDimensions bereitzustellen.
Diese Schnittstelle ist jetzt überraschend Verhalten führen, da die Methoden ermöglichen nicht unsichere Interleavings Verfügbarmachen weniger wahrscheinlich.
Abbildung 3 eine Version der Schnittstelle mit GetDimensions und UpdateDimensions Methoden
struct WidgetDimensions
{
size_t width;
size_t height;
};
class LockedWidgetEx{
WidgetDimensions m_dimensions;
reader_writer_lock lock;
public:
LockedWidgetEx(size_t w, size_t h):
m_dimensions.width(w),m_dimensions.height(h){};
WidgetDimensions GetDimensions(){
auto lockGuard = reader_writer::scoped_lock_read(lock);
return m_dimensions;
}
void UpdateDimensions(size_t width, size_t height){
auto lockGuard = reader_writer::scoped_lock(lock);
m_dimensions.width = width;
m_dimensions.height = height;
}
};
Verwalten von freigegebenen Status mit Message Blocks
Nachdem wir nehmen einen Blick auf wie der Agents Library helfen kann stellen verwalten einfacher Zustand und der Code etwas robuster freigegeben.
Die wichtigsten Klassen aus der Bibliothek Agents, die eignen sich für Verwalten von freigegebene Variablen Overwrite_buffer < T >, der einen einzelnen aktualisierbaren Wert gespeichert und gibt eine Kopie der neuesten Wert beim empfangen wird, aufgerufen.Single_assignment < T >, welche Informationsspeicher und gibt eine Kopie einer einzelnen beim Wert erhalten wird aufgerufen, jedoch, wie eine Konstante zugewiesen werden kann nur einmal;und Unbounded_buffer < T >, der eine unbegrenzte Anzahl von Elementen (Arbeitsspeicher zulassen) und, wie eine FIFO-Warteschlange speichert dequeues das älteste Element Wenn wird aufgerufen.
Ich beginne mit einem Overwrite_buffer < T >.
In der Widget-Klasse ich werde zunächst die M_dimensions-Membervariable mit Overwrite_buffer < WidgetDimensions > ersetzen, und dann ich werde die Methoden die expliziten Sperren entfernen und Ersetzen Sie durch die entsprechenden Sende- und Anrufe.
Noch unsere Schnittstelle wird sicher kümmern muss, jedoch besteht nicht mehr müssen Sie die Daten zu sperren.
Hier ist, wie dies im Code aussieht.
Es ist tatsächlich etwas weniger Codezeilen als gesperrte Version und die gleiche Anzahl von Zeilen wie der seriellen Version:
class AgentsWidget{
overwrite_buffer<WidgetDimensions> m_dimensionBuf;
public:
AgentsWidget(size_t w, size_t h){
send(&m_dimensionBuf,WidgetDimensions(w,h));
};
WidgetDimensions GetDimensions(){
return receive(&m_dimensionBuf);
}
void UpdateDimensions(size_t width, size_t height){
send(&m_dimensionBuf,WidgetDimensions(w,h));
}
};
Es gibt ein feinen semantischer Unterschied hier aus der Reader_writer-Lock-Implementierung.
Die Overwrite_buffer ermöglicht einen Aufruf von UpdateDimensions, die während eines Aufrufs von Dimensionen auftreten.
Dadurch wird praktisch keine Blockierung während dieser Aufrufe, aber ein Aufruf von GetDimensions möglicherweise etwas veraltet.
Es ist erwähnenswert, dass das Problem in der gesperrten Version auch vorhanden da, sobald Sie die Dimensionen erhalten, das Potenzial, veraltet sein.
Ich hier getan haben lediglich den blockierenden Aufruf zu entfernen.
Ein Unbounded_buffer kann auch nützlich für die Widget-Klasse sein.
Genommen Sie an, dass der feine semantische Unterschied beschriebenen unglaublich wichtig war.
Wenn Sie eine Instanz eines Objekts, das sicherstellen sollen erfolgt durch nur einen Thread zu einem Zeitpunkt, können Unbounded_buffer als ein Objekt Inhaber, die Zugriff auf dieses Objekt verwaltet.
Dies die Widget-Klasse zuweisen möchten, können M_dimensions entfernen und ersetzen es mit Unbounded_buffer < WidgetDimension >und verwenden Sie diesen Puffer über die Aufrufe von GetDimensions und UpdateDimensions.
Die Herausforderung besteht hier darin um sicherzustellen, dass niemand können einen Wert aus unserer Widget während aktualisiert wird.
Dies wird erreicht, indem die Unbounded_buffer geleert, sodass Aufrufe von GetDimension Warten der Aktualisierung gesperrt werden.
Siehe Abbildung 4.
GetDimensions und UpdateDimensions Block exklusiven Zugriff auf die Variable Dimensionen warten.
Abbildung 4 leeren Unbounded_Buffer,
class AgentsWidget2{
unbounded_buffer<WidgetDimensions> m_dimensionBuf;
public:
AgentsWidget2(size_t w, size_t h){
send(&m_dimensionBuf,WidgetDimensions(w,h));
};
WidgetDimensions GetDimensions(){
//get the current value
WidgetDimensions value = receive(&m_dimensionBuf);
//and put a copy of it right back in the unbounded_buffer
send(&m_dimensionBuf,value);
//return a copy of the value
return WidgetDimensions(value);
}
void UpdateDimensions (size_t width, size_t height){
WidgetDimensions oldValue = receive(&m_dimensionBuf);
send(&m_dimensionBuf,WidgetDimensions(width,height));
}
};
Es ist tatsächlich zu Coordinating Zugriff auf die Daten
Betonen Sie eine weitere Sache über unsere Widget-Klasse soll: sicherstellen, dass Methoden und Daten, die gleichzeitig zugegriffen werden können "sicher" funktionierengemeinsam ist kritisch.
Dies kann häufig durch koordinierende Zugriff Zustand statt durch Sperren Methoden oder Objekten erreicht werden.
Eine reine "Zeilen des Codes"im Hinblick auf die Sie kein großer Gewinn über gesperrte Beispiel angezeigt, und insbesondere das zweite Beispiel möglicherweise noch ein wenig mehr Code.
Was erzielt wird, ist jedoch ein Design sicherer und mit ein wenig Gedanke häufig ändern serielle Schnittstellen so, dass der interne Zustand des Objekts ist nicht "unterbrochener." Im Beispiel Widget ich habe dies mithilfe der Nachricht blockiert, und ich konnte diesen Zustand in einer solchen Weise schützen, dass es sicherer ist.
Hinzufügen von Methoden oder Funktionen für die Widget-Klasse ist in der Zukunft weniger wahrscheinlich um die interne Synchronisierung zerstören, die wir eingerichtet haben.
Mit einer Sperre Member ist es ziemlich leicht, einfach vergessen, die Sperre zu sperren, wenn eine Methode für eine Klasse hinzugefügt wird.
Aber verschieben Vorgänge zu einem Modell Weiterleiten von Nachrichten und Message Blocks mit, wie z. B. Daten und Klassen kann der überschreiben-Puffer auf Ihre natürliche Weise oft aufbewahren synchronisiert.
Drei: Verwenden kombinierbare für lokalen Thread-Accumulations und Initialisierung
Das zweite Szenario, in dem wir Zugriff auf ein Objekt mit Sperren oder Message Blocks geschützt funktioniert sehr gut für höhere Gewichtung Objekte, die selten zugegriffen werden.
Falls beim Lesen von diesem Beispiel absehbar, dass es möglicherweise ein Leistungsproblem Wenn in einer Schleife enge (und parallele) synchronisierte Widget verwendet wurden, haben Sie vermutlich Recht.
Das liegt daran Schützen von gemeinsam genutzten Zustand kann problematisch sein, und nicht für vollständig Allzweck-Algorithmen und Objekte, die tatsächlich Status Freigabe vorhanden Leider sind viele Optionen außer koordinieren Zugriff oder eine Sperre einführen.
Aber finden Sie fast immer eine Möglichkeit, Code oder ein Algorithmus, die Abhängigkeit von gemeinsam genutzten Zustands zu lockern Umgestalten und einmal haben Sie hierzu einige bestimmte jedoch allgemeine Muster kombinierbare < T > ein Objekt ruftin der Parallel Pattern Library wirklich helfen aus.
Kombinierbare < T >ist ein gleichzeitiger Container, der Unterstützung für drei breiten Anwendungsfälle bietet: gedrückt halten, eine threadlokale Variable oder lokalen Thread-Initialisierung durchführen, Vorgänge assoziative Binärdatei (z. B. Summe, min und Mix) auf den lokalen Thread-Variablen und kombinieren und jeder lokalen Thread-Kopie mit einem Vorgang (wie Listen zusammen splicing) besuchen.
In diesem Abschnitt werde ich erläutern jedem dieser Fälle und enthalten Beispiele für deren Verwendung.
Halten einer lokalen Thread-Variablen oder lokalen Thread-Initialisierung ausführen
Die erste Anwendungsfall erwähnt für kombinierbare < T >war für eine threadlokale Variable halten.
Es ist relativ zum Speichern einer lokalen Thread-Kopie des globalen Zustands üblich.
Z. B. in farbigem Ray Tracers Anwendungen wie in unserem Beispiel-Pack (code.msdn.microsoft.com/concrtextras) oder befindet sich die Beispiele für die parallele Entwicklung mit .NET 4.0 (code.msdn.microsoft.com/ParExtSamples) gibt es eine Option zum Kolorieren Sie jede Zeile von Thread auf die Parallelität zu visualisieren.
In der systemeigenen Version der Demo erfolgt dies mithilfe von ein kombinierbarer Objekt, das die lokalen Thread-Farbe enthält.
Sie können eine threadlokale Variable natürlich halten, mithilfe von lokalen Threadspeicher (TLS), aber es gibt einige Nachteile – insbesondere Lebensdauerverwaltung Sichtbarkeit und diese gehen hand in hand.
Um TLS verwenden, müssen Sie zunächst einen Index mit TlsAlloc reservieren, Ihr Objekt reservieren und speichern einen Zeiger auf das Objekt in den Index mit TlsSetValue.
Wenn der Thread beendet wird, müssen Sie dann sicher, dass das Objekt freigegeben wird.
(TlsFree wird automatisch aufgerufen.) Dies einmal oder zweimal pro Thread und sicherstellen, dass nicht vorhanden sind Verluste aufgrund von früh beendet oder Ausnahmen nicht, dass eine Herausforderung, aber wenn Ihre Anwendung Dutzende oder Hunderte von diese Elemente erforderlich ist, ein anderen Ansatz ist wahrscheinlich besser.
Kombinierbare < T >mit einen lokalen Thread-Wert verwendet werden können, aber die Lebensdauer der einzelnen Objekte sind an die Lebensdauer der kombinierbare < T > gebundenElement und ein großer Teil der Initialisierung ist automatisiert.
Sie zugreifen den lokalen Thread-Wert, indem Sie einfach aufrufen, die combinable::local-Methode, die einen Verweis auf das lokale Objekt zurückgibt.
Hier ist ein Beispiel zur Verwendung Task_group, aber dies kann mit Win32-Threads sowie durchgeführt werden:
combinable<int> values;
auto task = [&](){
values.local() = GetCurrentThreadId();
printf("hello from thread: %d\n",values.local());
};
task_group tasks;
tasks.run(task);
//run a copy of the task on the main thread
task();
tasks.wait();
Erwähnt, dass Thread-Local-Initialisierung auch mit kombinierbare erreicht werden kann.
Wenn z. B. müssen Sie einen Bibliothek-Aufruf an jeden Thread initialisiert werden, auf dem es verwendet wird, können Sie eine Klasse erstellen, die die Initialisierung in seinem Konstruktor ausführt.
Sie dann auf der ersten Verwendung pro Thread, der Bibliothek-Aufruf vorgenommen werden jedoch wird bei nachfolgenden Verwendungen übersprungen werden.
Beispiel:
class ThreadInitializationClass
{
public:
ThreadInitializationClass(){
ThreadInitializationRoutine();
};
};
...
//a combinable object will initialize these
combinable<ThreadInitializationClass> libraryInitializationToken;
...
//initialize the library if it hasn't been already on this thread
ThreadInitializationClass& threadInit = libraryInitalizationToken.local();
Durchführen von Reduzierung in einer parallelen Schleife
Ein weiteres wichtigsten Szenario für kombinierbare Objekt ist lokalen Thread-Reduzierung oder lokalen Thread-Accumulations durchführen.
Insbesondere können Sie einen bestimmten Typ von Race-Bedingung beim Parallelisieren von Schleifen oder in rekursive parallele Traversalen mit kombinierbare vermeiden.
Hier ist ein unglaublich vereinfachte Beispiel, die nicht zum Anzeigen von Speed-ups vorgesehen ist.
Der folgende Code zeigt eine einfache Schleife, die aussieht, als es mit Parallel_for_each, außer für den Zugriff auf die Variable Summe parallelisiert werden kann:
int sum = 0;
for (vector<int>::iterator it = myVec.begin(); it != myVec.end(); ++it) {
int element = *it;
SomeFineGrainComputation(element);
sum += element;
}
Anstatt eine Sperre in unserer Parallel_for_each, die Chance zerstört wir der Speed-ups mussten, platzieren können wir jetzt ein kombinierbarer-Objekt verwenden, um lokalen Thread-Summen berechnen:
combinable<int> localSums;
parallel_for_each(myVec.begin(), myVec.end(), [&localSums] (int element) {
SomeFineGrainComputation(element);
localSums.local() += element;
});
Wir haben die Racebedingung jetzt erfolgreich vermieden jedoch wir haben eine Auflistung von lokalen Thread-Summen in dem LocalSums-Objekt gespeichert und wir müssen noch den letzten Wert extrahieren.
Wir können dies mit der Methode kombinieren tun die eine binäre Functor wie folgt:
int sum = localSums.combine(std::plus<int>);
Der dritte Anwendungsfall für kombinierbare < T >, der mit der Combine_each-Methode, ist Wenn Sie müssen, besuchen Sie jede der lokalen Thread-Kopien, und führen eine Operation auf diese (wie bereinigen oder Fehlerüberprüfung).
Ein anderes, interessanter wird z. B. das kombinierbare Objekt ist ein kombinierbarer < Liste < T > > und in Ihre Threads Sie std::lists oder std::sets erstellen.
Im Fall von std::lists können Sie problemlos zusammen mit list::splice; spliced werdenmit std::sets können Sie mit set::insert eingefügt werden.
Vier: Konvertieren eines vorhandenen Hintergrund-Thread in ein Agent oder einer Aufgabe
Genommen Sie an, Sie bereits einen Hintergrund oder Worker Thread in Ihrer Anwendung haben.
Es gibt einige sehr gute Gründe, warum möglicherweise möchten Sie die Hintergrund-Thread zu einem Vorgang aus die PPL oder in einen Agent zu konvertieren und dies daher relativ einfach.
Einige der wichtigsten Vorteile dies gehören:
Komponierbarkeit und Leistung. Wenn den Arbeitsthreads werden intensiv berechnen und Sie erwägen werden, zusätzliche Threads in der PPL oder Agents Bibliothek verwenden, kann konvertieren den Hintergrund-Thread in einem Worker-Vorgang mit anderen Aufgaben in der Laufzeit zusammenarbeiten und vermeiden Oversubscription auf dem System.
Abbruch und Ausnahmebehandlung. Wenn Sie problemlos abbrechen Arbeit in einem Thread oder einen well-described Mechanismus zum Behandeln von Ausnahmen können möchten, hat eine Task_group diese integrierten Funktionen.
Steuern Sie Fluss und Status. Wenn müssen Sie den Status der Thread (gestartet oder abgeschlossen ist, für Beispiel) verwalten oder über ein Objekt, dessen Status effektiv vom Arbeitsthread inseparable ist, kann das Implementieren eines Agents nützlich sein.
Task_group bietet Abbruch und Ausnahmebehandlung
Im ersten Szenario untersucht wir, was es dauert, arbeiten mit einer Task_group planen: im Wesentlichen Verpacken Ihrer Arbeit in einer Functor (mithilfe von einen Lambda-Ausdruck, ein std::bind oder ein benutzerdefinierte Funktion-Objekt) und mit der task_group::run-Methode planen.
Was beschrieben haben wurde der Abbruch und Ausnahmebehandlung Semantik, in der Tat verknüpft sind.
Abbildung 5 Implementierung von MyAgentClass
class MyAgentClass : public agent{
public:
MyAgentClass (){
}
AgentsWidget widget;
void run(){
//run is started asynchronously when agent::start is called
//...
//set status to complete
agent::done();
}
};
Zunächst werde ich einfach Semantik erläutern.
Wenn Ihr Code aufgerufen, task_group::cancel wird oder eine Aufgabe eine nicht abgefangene Ausnahme auslöst, ist Absage für die Task_group in wirksam.
Wenn Abbruch in Kraft ist, wird nicht Aufgaben, die auf die Task_group gestartet wurde noch nicht gestartet werden, wodurch Arbeit schnell und einfach auf eine Task_group abgebrochen werden.
Abbruch unterbrechen nicht Aufgaben ausgeführt oder blockiert, damit eine laufende Aufgabe den Abbruch Status mit der task_group::is_canceling-Methode oder durch die Hilfsfunktion Abfragen können
Is_current_task_group_canceling.
Hier ist ein kurze Beispiel:
task_group tasks;
tasks.run([](){
...
if(is_current_task_group_canceling())
{
//cleanup then return
...
return;
}
});
tasks.cancel();
tasks.wait();
Ausnahmebehandlung wirkt sich auf Abbruch, da eine nicht abgefangene Ausnahme in einem Task_group Abbruch auf die Task_group auslöst.
Wenn eine nicht abgefangene Ausnahme vorhanden ist, wird die Task_group tatsächlich std::exception_ptr verwenden, um die Ausnahme im Thread Verpacken er ausgelöst wurde, auf.
Später, wenn task_group::wait aufgerufen wird, wird die Ausnahme erneut auf dem Thread ausgelöst, die Wait aufgerufen.
Implementieren eines asynchronen Agents
Der Agents Library bietet eine Alternative zur Verwendung einer Task_group: Ersetzen einen Thread mit der Agent-Basisklasse.
Wenn Ihre Thread viel threadspezifischen Zustand und Objekte verfügt, kann ein Agent eine bessere Übereinstimmung für das Szenario sein.
Die Klasse abstract-Agent ist eine Implementierung das Actor-Musterdie beabsichtigte Verwendung ist das Implementieren einer eigene Klasse von Agent und dann alle Zustände, die Ihre Akteur (oder Thread) möglicherweise in diesen Agent kapseln.
Felder, die öffentlich zugänglich sein sollen ist, die Anleitung werden als Nachricht blockiert oder Quellen und Ziele verfügbar machen und Nachrichtenübergabe, mit dem Agent kommunizieren.
Implementieren eines Agents erfordert eine Klasse von der Agent-Basisklasse ableiten und überschreiben die virtuelle Methode Ausführung.
Der Agent kann durch Aufrufen von agent::start, der die run-Methode als Aufgabe, ähnlich wie ein Thread erstellt dann gestartet werden.
Der Vorteil besteht darin, dass lokalen Thread-Zustand jetzt in der Klasse gespeichert werden kann.
Auf diese Weise einfacher Synchronisierung des Status zwischen Threads, insbesondere, wenn der Status in Message Block gespeichert ist.
Abbildung 5 zeigt ein Beispiel einer Implementierung, die eine Membervariable öffentlich verfügbar gemachte des Typs AgentsWidget verfügt.
Beachten Sie, die ich den Agentstatus festgelegt haben ausgeführt, da die run-Methode beendet wird.
Dadurch wird den Agent nicht nur gestartet werden, sondern auch auf gewartet werden.
Darüber hinaus kann aktuelle Status des Agents durch einen Aufruf von agent::status abgefragt werden.
Starten und Warten auf unsere-Agent-Klasse ist einfach, wie der folgende Code zeigt:
MyAgentClass MyAgent;
//start the agent
MyAgent.start();
//do something else
...
//wait for the agent to finish
MyAgent.wait(&MyAgent);
Bonus-Element: Sortieren in Parallel mit parallel_sort
Schließlich möchte ich einen anderen potenziell leicht Punkt der Parallelisierung dieses Mal nicht aus der PPL oder der Agents Bibliothek jedoch aus unserem Beispiel Pack code.msdn.microsoft.com/concrtextras erhältlich vorschlagen.
Parallele Quicksort ist eine der Beispiele verwenden wir für erläutert, wie rekursive und herrsche Algorithmen mit Aufgaben zu parallelisieren, und das Beispiel Pack enthält eine Implementierung der parallelen Quicksort.
Parallele sortieren kann Speed-ups anzeigen, wenn Sie eine große Anzahl von Elementen sortieren, in dem der Vergleichsvorgang etwas teurer, als mit Zeichenfolgen ist.
Es wird nicht wahrscheinlich Speed-ups für kleine Zahlen von Elementen oder anzeigen, wenn integrierte Typen wie ganze Zahlen und Double-Werte sortieren.
Hier ist ein Beispiel wie es verwendet werden kann:
//from the sample pack
#include "parallel_algorithms.h"
int main()
using namespace concurrency_extras;
{
vector<string> strings;
//populate the strings
...
parallel_sort(strings.begin(),strings.end());
}
Zusammenfassung
Ich hoffe, dieser Spalte hilft, erweitern Sie den Horizont des wie die parallelen Bibliotheken in Visual Studio 2010 auf Ihre Projekte über einfach mit Parallel_for oder Vorgänge beschleunigen berechnungsintensiv Schleifen angewendet werden.
Viele Beispiele für andere hilfreich finden Sie in unserer Dokumentation auf MSDN (msdn.microsoft.com/library/dd504870(VS.100).aspx) und in unserem Beispiel-Pack (code.msdn.microsoft.com/concrtextras), die veranschaulichen, die parallelen-Bibliotheken und wie Sie verwendet werden können.
Fangen Sie Sie auschecken.
Rick Molloy
ist Programmmanager im Parallel Computing Platform-Team bei Microsoft.
|