├── synctable ├── Makefile ├── synctable.cpp ├── example.cpp └── synctable.h ├── README.md └── lockable_objects ├── example.cpp └── lockable.hpp /synctable/Makefile: -------------------------------------------------------------------------------- 1 | LINK.o = $(LINK.cc) 2 | CXX = clang++ 3 | CXXFLAGS = -Wall -Wextra -pedantic -std=c++11 4 | 5 | LDFLAGS = -pthread 6 | 7 | PRGM = example 8 | OBJECTS = $(PRGM).o synctable.o 9 | 10 | all: $(PRGM) 11 | 12 | $(PRGM): $(OBJECTS) 13 | 14 | clean: 15 | rm -f *.o $(PRGM) 16 | -------------------------------------------------------------------------------- /synctable/synctable.cpp: -------------------------------------------------------------------------------- 1 | #include "synctable.h" 2 | 3 | #include 4 | 5 | namespace synclock { 6 | 7 | // SyncTable 8 | 9 | std::mutex& SyncTable::get_lock(void *addr) 10 | { 11 | std::lock_guard lock(this->table_lock); // lock the table 12 | 13 | return this->locks_table.emplace( 14 | std::piecewise_construct, 15 | std::make_tuple(addr), 16 | std::tuple<>{}).first->second; 17 | } 18 | 19 | 20 | // Table_Locker 21 | 22 | Table_Locker::Table_Locker(void *addr, SyncTable& sync_table) 23 | : var_lock_holder{sync_table.get_lock(addr)} 24 | { } 25 | 26 | SyncTable Table_Locker::shared_table{}; 27 | } 28 | -------------------------------------------------------------------------------- /synctable/example.cpp: -------------------------------------------------------------------------------- 1 | #include "synctable.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #define DONT_THROW -1 8 | 9 | void count_to(synclock::SyncTable & syncer, int & counter) 10 | { 11 | for(int i=0; i < 100000; ++i){ 12 | tablesynchronized(syncer, &counter){ 13 | int a = counter; 14 | a = a + 1; 15 | counter = a; 16 | } 17 | } 18 | } 19 | 20 | 21 | void global_count_to(int & counter, int throw_on) 22 | { 23 | try { 24 | for(int i=0; i < 100000; ++i){ 25 | synchronized(&counter){ 26 | if (throw_on == i){ 27 | throw i; 28 | } 29 | //elongated increment to allow more interleaving 30 | int a = counter; 31 | a = a + 1; 32 | counter = a; 33 | } 34 | } 35 | } catch (int i) { 36 | std::cout << "exception occurred with value: " << i << std::endl; 37 | } 38 | } 39 | 40 | 41 | int main(){ 42 | synclock::SyncTable syncer; 43 | 44 | // table sync 45 | int i = 0; 46 | 47 | std::thread t1(count_to, std::ref(syncer), std::ref(i)); 48 | std::thread t2(count_to, std::ref(syncer), std::ref(i)); 49 | 50 | t1.join(); 51 | t2.join(); 52 | std::cout << i << std::endl; 53 | 54 | // global sync (the cool one) 55 | i = 0; 56 | 57 | std::thread t3(global_count_to, std::ref(i), 50000); 58 | std::thread t4(global_count_to, std::ref(i), DONT_THROW); 59 | 60 | t3.join(); 61 | t4.join(); 62 | std::cout << i << std::endl; 63 | 64 | return 0; 65 | } 66 | 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CPP Synchronized 2 | ================ 3 | 4 | When I first discovered Java's `synchronized` blocks, I was intrigued. It's 5 | pretty easy to forget to unlock a mutex, though RAII and unique_lock aid with 6 | this, and easy to follow the structure of a block. I thought for a while about 7 | how I might implement something like this in C++11, and eventually accomplished 8 | this in two different ways. 9 | 10 | synctable 11 | --------- 12 | My favorite of the two implementations. Allows for a synchronized block on 13 | any pointer using the syntax: 14 | 15 | ```c++ 16 | synchronized(&obj) { 17 | // ... critical section 18 | } 19 | ``` 20 | 21 | Note that this could have been done using an object rather than a pointer with 22 | something more resembling Java `synchronized(obj)` but it seemed to be more in 23 | the spirit of C++ to use the address. 24 | 25 | This is accomplished with a `#define`, and a `for` loop that initializes an 26 | object to lock and unlock a `std::mutex` on `for` entry/exit. The mutexes 27 | themselves are stored in a global `std::unordered_map` of 28 | `void *` to `mutex`. 29 | 30 | An alternative use is a `tablesynchronized` block. This requires the 31 | programmer to create a `SyncTable` and pass that as the first argument to 32 | `tablesynchronized` 33 | 34 | ```c++ 35 | SyncTable table; 36 | // ... later on 37 | tablesynchronized(table, &obj) { 38 | // ... critical section 39 | } 40 | ``` 41 | 42 | The idea here is a group of threads can use a local `SyncTable` rather than 43 | the global one to avoid contention on the global table. 44 | 45 | 46 | lockable_objects 47 | ---------------- 48 | A non-intrusive use of multiple inheritance. A "lockable" version of any class 49 | can be instantiated with 50 | 51 | ```c++ 52 | Lockable lockable_object(/* Object ctor args */); 53 | ``` 54 | 55 | Any lockable object can be used within a `synchronized` block. This approach 56 | avoids the `SyncTable`s entirely 57 | 58 | ```c++ 59 | synchronized(lockable_object){ 60 | // ... critical section 61 | } 62 | ``` 63 | 64 | Can then be used without the overhead of a lookup for the mutex. Additionally, 65 | no `&` is used for lockable objects. 66 | -------------------------------------------------------------------------------- /lockable_objects/example.cpp: -------------------------------------------------------------------------------- 1 | #include "lockable.hpp" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | // standard person class with name and age 9 | // default constructor private to show it can be worked around 10 | class Person{ 11 | private: 12 | int age; 13 | std::string name; 14 | 15 | Person(); 16 | 17 | public: 18 | Person(int age, std::string name): 19 | age(age), 20 | name(name) { } 21 | Person(const Person & other) { 22 | this->age = other.age; 23 | this->name = other.name; 24 | } 25 | 26 | void set_name(std::string name){ 27 | this->name = name; 28 | } 29 | void show(){ 30 | std::cout << this->age << ' ' << this->name << std::endl; 31 | } 32 | 33 | ~Person(){ 34 | } 35 | 36 | }; 37 | 38 | // a wrapper for an int. using a .inc() function to do ++ 39 | class Counter{ 40 | private: 41 | int _count; 42 | public: 43 | Counter(): _count(0){} 44 | void inc(){ this->_count++; } 45 | int count() { return this->_count; } 46 | }; 47 | 48 | void count_to(synclock::Lockable &counter, int raise_on){ 49 | try { 50 | for(int i=0; i < 10000000; ++i){ 51 | synchronized(counter){ 52 | if (i == raise_on){ 53 | throw i; // show exception safety 54 | } 55 | counter.inc(); 56 | } 57 | } 58 | } catch (int i) { 59 | std::cout << "exception occurred with value: " << i << std::endl; 60 | } 61 | } 62 | 63 | int main(){ 64 | Person bill(18, "bill"); 65 | synclock::Lockable lp(2, "jane"); 66 | synchronized(lp){ 67 | lp.show(); 68 | } 69 | 70 | bill.show(); 71 | synclock::Lockable lockable_bill(bill); 72 | lockable_bill.set_name("william"); 73 | bill.show(); //this will NOT print william 74 | 75 | synclock::Lockable lc; 76 | 77 | synchronized(lc){ 78 | synchronized(lp){ 79 | std::cout << "proper nesting" << std::endl; 80 | } 81 | } 82 | std::thread t1(count_to, std::ref(lc), 100000); 83 | std::thread t2(count_to, std::ref(lc), -1); 84 | 85 | t1.join(); 86 | t2.join(); 87 | std::cout << lc.count() << std::endl; 88 | 89 | return 0; 90 | } 91 | -------------------------------------------------------------------------------- /lockable_objects/lockable.hpp: -------------------------------------------------------------------------------- 1 | #ifndef __LOCKABLE__H__ 2 | #define __LOCKABLE__H__ 3 | 4 | #include 5 | 6 | namespace synclock{ 7 | 8 | // Creating a Lockable object will result in an object 9 | // subclassing Type and std::mutex. This yields and object with 10 | // the original Type's member functions with the additional .lock(), 11 | // .unlock() and .try_lock() member functions. 12 | // 13 | // Valid arguments to the Lockable constructor are any arguments 14 | // that the Type constructor can handle. 15 | template 16 | class Lockable : public Enclosed, public std::mutex{ 17 | public: 18 | // pass any arguments to Enclosed class constructor 19 | template 20 | Lockable(Ts... params): Enclosed(params...) {} 21 | 22 | // create Lockable from nonlockable. the behavior of this 23 | // completely depends on how the Enclosed class's copy 24 | // constructor is defined. 25 | Lockable(const Enclosed & other): Enclosed(other) {} 26 | 27 | ~Lockable(){} 28 | }; 29 | 30 | // this class should only be used by the synchronized macro 31 | template 32 | class _Locker{ 33 | private: 34 | Contained & lockable; 35 | public: 36 | bool finished; 37 | _Locker(Contained & obj): lockable(obj), finished(false) { 38 | // on construction, lock the contained object 39 | this->lockable.lock(); 40 | } 41 | ~_Locker() { 42 | // on destruction, unlock it. This will occur on for-loop 43 | // exit and in the event that an exception occurs 44 | this->lockable.unlock(); 45 | } 46 | }; 47 | } 48 | 49 | 50 | // synchronize(object){ critical_section } 51 | // similar to javas synchronized block, the object will be locked at the start 52 | // of the block and unlocked at the end of the block to provide safety 53 | // across threads 54 | 55 | // for loop creates a Locker, which locks the LKBLEOBJ given 56 | // the condition checks if the Locker::finished variable is true. initially, 57 | // it is false, so the body of the loop is executed 58 | // the for loop sets the finished variable to true, causing the condition to 59 | // fail and the loop to exit 60 | // this triggers the destruction of the Locker which unlocks the LKBLEOBJ 61 | // since destruction will also occur on exception, this is exception safe. 62 | // once the synchronized block is exited, the LKBLEOBJ is guaranteed to be in 63 | // an unlocked state 64 | #define synchronized(LKBLEOBJ) \ 65 | for(synclock::_Locker _locker_obj(LKBLEOBJ); \ 66 | !_locker_obj.finished; \ 67 | _locker_obj.finished = true) 68 | 69 | 70 | // example usage: 71 | // 72 | // Lockable jane; 73 | // void threaded_function(Lockable & p){ 74 | // synchronized(p){ 75 | // //critical section 76 | // } 77 | 78 | #endif // ifndef __LOCKABLE__H__ 79 | -------------------------------------------------------------------------------- /synctable/synctable.h: -------------------------------------------------------------------------------- 1 | #ifndef SYNCHRONIZER__H__ 2 | #define SYNCHRONIZER__H__ 3 | 4 | #include 5 | #include 6 | 7 | namespace synclock{ 8 | class SyncTable{ 9 | friend class Table_Locker; 10 | private: 11 | std::unordered_map locks_table; 12 | std::mutex table_lock; 13 | std::mutex& get_lock(void *addr); 14 | 15 | public: 16 | SyncTable() = default; 17 | // I might remove the delete's but it seems like a bad idea 18 | // to have the same entries in multiple tables. I can't think 19 | // of why you would want that on purpose 20 | SyncTable(const SyncTable&) = delete; 21 | SyncTable& operator=(const SyncTable&) = delete; 22 | ~SyncTable() = default; 23 | 24 | }; 25 | 26 | // This class is only for use by the synchronized/tablesynchronized blocks 27 | // and should not be used directly. The name of the class is 28 | // intentionlly poorly formed. 29 | // The lock_guard data member is created and destroyed along with the 30 | // Table_Locker object. The finished data member is used by the for 31 | // loop to signal completion. 32 | class Table_Locker { 33 | private: 34 | //used by default by all threads 35 | static SyncTable shared_table; 36 | 37 | // holds the lock for the lifetime of the Table_Locker 38 | std::lock_guard var_lock_holder; 39 | 40 | public: 41 | bool finished = false; 42 | Table_Locker(void *addr, 43 | SyncTable& sync_table =Table_Locker::shared_table); 44 | Table_Locker(const Table_Locker&) = delete; 45 | Table_Locker& operator=(const Table_Locker&) = delete; 46 | }; 47 | 48 | // shared table for use in synchronized blocks 49 | } 50 | 51 | // the Table_Lockers have a bunch of capital letters on the end of them 52 | // to (try to) ensure there are no collisions 53 | 54 | // tablesynchronized(synctable, &var) { critical section } 55 | // 56 | // tablesynchronized block takes in a table and an address, locking the 57 | // address for the body of the block, but only in the given table 58 | // this is provided so that groups of unrelated threads do not result in a 59 | // large, slow, Table_locker::shared_table. 60 | // using a value in a local SyncTable will NOT add it to tho shared synctable 61 | #define tablesynchronized(TABLE, ADDR) \ 62 | for(synclock::Table_Locker table_locker_obj_ABCDEFAOEUI( \ 63 | static_cast(ADDR), TABLE); \ 64 | !table_locker_obj_ABCDEFAOEUI.finished; \ 65 | table_locker_obj_ABCDEFAOEUI.finished = true) 66 | 67 | 68 | // synchronized(&var) { critical section } 69 | // 70 | // synchronized blocks construct a Table_Locker on entry and destroy it 71 | // on exit. This results in a locking of var for the body of the block. 72 | // It is also exception safe since destructon occurs when an exception 73 | // causes the block to exit, which releases the mutex. 74 | #define synchronized(ADDR) \ 75 | for(synclock::Table_Locker table_locker_obj_ABCDEFAOEUI( \ 76 | static_cast(ADDR)); \ 77 | !table_locker_obj_ABCDEFAOEUI.finished; \ 78 | table_locker_obj_ABCDEFAOEUI.finished = true) 79 | 80 | #endif // #ifndef SYNCHRONIZER__H__ 81 | --------------------------------------------------------------------------------