The implementation is based around a transaction stack that records each change to a container. Each change is recorded in a struct
struct Transaction
{
TransactionalContainer *container;
void *object;
enum Action { created, deleted, none } action;
};
that is pushed onto a vector. All containers share the same stack.
When a container adds an item, it calls the function log_insert(), which pushes an operation onto the transaction- stack, with action=created, container=the container holding the item, and object=the node of the item being inserted.
When a container removes an item, it calls log_delete(), which pushes an operation onto the transaction-stack, with action=deleted, container=the container containing the item, and object=the node that was deleted. The deleted item is not actually deleted at this point in case it needs to be reinserted back into the container on roll-back. The container makes sure that the deleted object holds enough context to be inserted back into the container in the right position if need be.
Containers inherit from the atomic::container class:
class container
{
public:
virtual ~container();
virtual void destroy(void*)=0;
virtual void unlink(void*)=0;
virtual void relink(void*)=0;
};
When an insertion operation is rolled back, the transaction manager calls container->unlink() to remove the item from the container, and then container->destroy() to destroy the object.
When a deletion operation is rolled back, the transaction manager calls container->relink() which inserts the item back into the container.
When the outermost transaction has been finished, the transaction stack can be emptied. This scans the transaction stack, and calls container->destroy() for all delete operations. Containers can no longer be rolled back after a cleanup has occurred.
Cleanup means that objects are not kept for longer than necessary, and that the memory used by deleted items is deallocated so that the program will not run out of memory. In fact if transactions are short, there is very little memory overhead at all.
The benchmark compares the performance of the atomic containers versus their std:: equivalents, using the atomic_bench.cpp program. (download)
The output was from 1000000 operations, performed using gcc 3.4.4, on a 1.7GHz Centrino processor.
| Container | Operation | std:: (ms) | atomic:: (ms) | atomic/std |
| list<int> | insert | 750 | 937 | 1.25 |
| delete | 297 | 313 | 1.05 | |
| list<string> | insert | 3469 | 3718 | 1.07 |
| delete | 985 | 937 | 0.95 | |
| vector<int> | insert | 16 | 375 | 23.4 |
| vector<string> | insert | 3688 | 4516 | 1.22 |
| set<int> | insert | 1172 | 1484 | 1.26 |
| delete | 375 | 390 | 1.04 | |
| set<string> | insert | 5765 | 5890 | 1.02 |
| delete | 1062 | 1016 | 0.96 | |
| map<int,int> | insert | 2203 | 2452 | 1.11 |
| delete | 375 | 390 | 1.04 | |
| map<string,string> | insert | 9517 | 9813 | 1.03 |
| delete | 1375 | 1391 | 1.01 |
This shows that there is overhead in using atomic containers, which is to be expected.
Restore points.
Implement thread safety.