Re-sync with internal repository
[hiphop-php.git] / third-party / folly / src / folly / docs / Synchronized.md
blobcfaa9379e8bd842463e2323904875cb37f775d36
1 `folly/Synchronized.h`
2 ----------------------
4 `folly/Synchronized.h` introduces a simple abstraction for mutex-
5 based concurrency. It replaces convoluted, unwieldy, and just
6 plain wrong code with simple constructs that are easy to get
7 right and difficult to get wrong.
9 ### Motivation
11 Many of our multithreaded C++ programs use shared data structures
12 associated with locks. This follows the time-honored adage of
13 mutex-based concurrency control "associate mutexes with data, not code".
14 Consider the following example:
16 ``` Cpp
18     class RequestHandler {
19       ...
20       RequestQueue requestQueue_;
21       SharedMutex requestQueueMutex_;
23       std::map<std::string, Endpoint> requestEndpoints_;
24       SharedMutex requestEndpointsMutex_;
26       HandlerState workState_;
27       SharedMutex workStateMutex_;
28       ...
29     };
30 ```
32 Whenever the code needs to read or write some of the protected
33 data, it acquires the mutex for reading or for reading and
34 writing. For example:
36 ``` Cpp
37     void RequestHandler::processRequest(const Request& request) {
38       stop_watch<> watch;
39       checkRequestValidity(request);
40       SharedMutex::WriteHolder lock(requestQueueMutex_);
41       requestQueue_.push_back(request);
42       stats_->addStatValue("requestEnqueueLatency", watch.elapsed());
43       LOG(INFO) << "enqueued request ID " << request.getID();
44     }
45 ```
47 However, the correctness of the technique is entirely predicated on
48 convention.  Developers manipulating these data members must take care
49 to explicitly acquire the correct lock for the data they wish to access.
50 There is no ostensible error for code that:
52 * manipulates a piece of data without acquiring its lock first
53 * acquires a different lock instead of the intended one
54 * acquires a lock in read mode but modifies the guarded data structure
55 * acquires a lock in read-write mode although it only has `const` access
56   to the guarded data
58 ### Introduction to `folly/Synchronized.h`
60 The same code sample could be rewritten with `Synchronized`
61 as follows:
63 ``` Cpp
64     class RequestHandler {
65       ...
66       Synchronized<RequestQueue> requestQueue_;
67       Synchronized<std::map<std::string, Endpoint>> requestEndpoints_;
68       Synchronized<HandlerState> workState_;
69       ...
70     };
72     void RequestHandler::processRequest(const Request& request) {
73       stop_watch<> watch;
74       checkRequestValidity(request);
75       requestQueue_.wlock()->push_back(request);
76       stats_->addStatValue("requestEnqueueLatency", watch.elapsed());
77       LOG(INFO) << "enqueued request ID " << request.getID();
78     }
79 ```
81 The rewrite does at maximum efficiency what needs to be done:
82 acquires the lock associated with the `RequestQueue` object, writes to
83 the queue, and releases the lock immediately thereafter.
85 On the face of it, that's not much to write home about, and not
86 an obvious improvement over the previous state of affairs. But
87 the features at work invisible in the code above are as important
88 as those that are visible:
90 * Unlike before, the data and the mutex protecting it are
91   inextricably encapsulated together.
92 * If you tried to use `requestQueue_` without acquiring the lock you
93   wouldn't be able to; it is virtually impossible to access the queue
94   without acquiring the correct lock.
95 * The lock is released immediately after the insert operation is
96   performed, and is not held for operations that do not need it.
98 If you need to perform several operations while holding the lock,
99 `Synchronized` provides several options for doing this.
101 The `wlock()` method (or `lock()` if you have a non-shared mutex type)
102 returns a `LockedPtr` object that can be stored in a variable.  The lock
103 will be held for as long as this object exists, similar to a
104 `std::unique_lock`.  This object can be used as if it were a pointer to
105 the underlying locked object:
107 ``` Cpp
108     {
109       auto lockedQueue = requestQueue_.wlock();
110       lockedQueue->push_back(request1);
111       lockedQueue->push_back(request2);
112     }
115 The `rlock()` function is similar to `wlock()`, but acquires a shared lock
116 rather than an exclusive lock.
118 We recommend explicitly opening a new nested scope whenever you store a
119 `LockedPtr` object, to help visibly delineate the critical section, and
120 to ensure that the `LockedPtr` is destroyed as soon as it is no longer
121 needed.
123 Alternatively, `Synchronized` also provides mechanisms to run a function while
124 holding the lock.  This makes it possible to use lambdas to define brief
125 critical sections:
127 ``` Cpp
128     void RequestHandler::processRequest(const Request& request) {
129       stop_watch<> watch;
130       checkRequestValidity(request);
131       requestQueue_.withWLock([&](auto& queue) {
132         // withWLock() automatically holds the lock for the
133         // duration of this lambda function
134         queue.push_back(request);
135       });
136       stats_->addStatValue("requestEnqueueLatency", watch.elapsed());
137       LOG(INFO) << "enqueued request ID " << request.getID();
138     }
141 One advantage of the `withWLock()` approach is that it forces a new
142 scope to be used for the critical section, making the critical section
143 more obvious in the code, and helping to encourage code that releases
144 the lock as soon as possible.
146 ### Template class `Synchronized<T>`
148 #### Template Parameters
150 `Synchronized` is a template with two parameters, the data type and a
151 mutex type: `Synchronized<T, Mutex>`.
153 If not specified, the mutex type defaults to `folly::SharedMutex`.  However, any
154 mutex type supported by `folly::LockTraits` can be used instead.
155 `folly::LockTraits` can be specialized to support other custom mutex
156 types that it does not know about out of the box.
158 `Synchronized` provides slightly different APIs when instantiated with a
159 shared mutex type or an upgrade mutex type then with a plain exclusive mutex.
160 If instantiated with either of the two mutex types above through having a
161 member called lock_shared(), the `Synchronized` object has corresponding
162 `wlock`, `rlock` or `ulock` methods to acquire different lock types.  When
163 using a shared or upgrade mutex type, these APIs ensure that callers make an
164 explicit choice to acquire a shared, exclusive or upgrade lock and that
165 callers do not unintentionally lock the mutex in the incorrect mode.  The
166 `rlock()` APIs only provide `const` access to the underlying data type,
167 ensuring that it cannot be modified when only holding a shared lock.
169 #### Constructors
171 The default constructor default-initializes the data and its
172 associated mutex.
175 The copy constructor locks the source for reading and copies its
176 data into the target. (The target is not locked as an object
177 under construction is only accessed by one thread.)
179 Finally, `Synchronized<T>` defines an explicit constructor that
180 takes an object of type `T` and copies it. For example:
182 ``` Cpp
183     // Default constructed
184     Synchronized<map<string, int>> syncMap1;
186     // Copy constructed
187     Synchronized<map<string, int>> syncMap2(syncMap1);
189     // Initializing from an existing map
190     map<string, int> init;
191     init["world"] = 42;
192     Synchronized<map<string, int>> syncMap3(init);
193     EXPECT_EQ(syncMap3->size(), 1);
196 #### Assignment, swap, and copying
198 The copy assignment operator copies the underlying source data
199 into a temporary with the source mutex locked, and then move the
200 temporary into the destination data with the destination mutex
201 locked. This technique avoids the need to lock both mutexes at
202 the same time. Mutexes are not copied or moved.
204 The move assignment operator assumes the source object is a true
205 rvalue and does lock the source mutex. It moves the source
206 data into the destination data with the destination mutex locked.
208 `swap` acquires locks on both mutexes in increasing order of
209 object address, and then swaps the underlying data. This avoids
210 potential deadlock, which may otherwise happen should one thread
211 do `a = b` while another thread does `b = a`.
213 The data copy assignment operator copies the parameter into the
214 destination data while the destination mutex is locked.
216 The data move assignment operator moves the parameter into the
217 destination data while the destination mutex is locked.
219 To get a copy of the guarded data, there are two methods
220 available: `void copy(T*)` and `T copy()`. The first copies data
221 to a provided target and the second returns a copy by value. Both
222 operations are done under a read lock. Example:
224 ``` Cpp
225     Synchronized<vector<string>> syncVec1, syncVec2;
226     vector<string> vec;
228     // Assign
229     syncVec1 = syncVec2;
230     // Assign straight from vector
231     syncVec1 = vec;
233     // Swap
234     syncVec1.swap(syncVec2);
235     // Swap with vector
236     syncVec1.swap(vec);
238     // Copy to given target
239     syncVec1.copy(&vec);
240     // Get a copy by value
241     auto copy = syncVec1.copy();
244 #### `lock()`
246 If the mutex type used with `Synchronized` is a simple exclusive mutex
247 type (as opposed to a shared mutex), `Synchronized<T>` provides a
248 `lock()` method that returns a `LockedPtr<T>` to access the data while
249 holding the lock.
251 The `LockedPtr` object returned by `lock()` holds the lock for as long
252 as it exists.  Whenever possible, prefer declaring a separate inner
253 scope for storing this variable, to make sure the `LockedPtr` is
254 destroyed as soon as the lock is no longer needed:
256 ``` Cpp
257     void fun(Synchronized<vector<string>, std::mutex>& vec) {
258       {
259         auto locked = vec.lock();
260         locked->push_back("hello");
261         locked->push_back("world");
262       }
263       LOG(INFO) << "successfully added greeting";
264     }
267 #### `wlock()` and `rlock()`
269 If the mutex type used with `Synchronized` is a shared mutex type,
270 `Synchronized<T>` provides a `wlock()` method that acquires an exclusive
271 lock, and an `rlock()` method that acquires a shared lock.
273 The `LockedPtr` returned by `rlock()` only provides const access to the
274 internal data, to ensure that it cannot be modified while only holding a
275 shared lock.
277 ``` Cpp
278     int computeSum(const Synchronized<vector<int>>& vec) {
279       int sum = 0;
280       auto locked = vec.rlock();
281       for (int n : *locked) {
282         sum += n;
283       }
284       return sum;
285     }
287     void doubleValues(Synchronized<vector<int>>& vec) {
288       auto locked = vec.wlock();
289       for (int& n : *locked) {
290         n *= 2;
291       }
292     }
295 This example brings us to a cautionary discussion.  The `LockedPtr`
296 object returned by `lock()`, `wlock()`, or `rlock()` only holds the lock
297 as long as it exists.  This object makes it difficult to access the data
298 without holding the lock, but not impossible.  In particular you should
299 never store a raw pointer or reference to the internal data for longer
300 than the lifetime of the `LockedPtr` object.
302 For instance, if we had written the following code in the examples
303 above, this would have continued accessing the vector after the lock had
304 been released:
306 ``` Cpp
307     // No. NO. NO!
308     for (int& n : *vec.wlock()) {
309       n *= 2;
310     }
313 The `vec.wlock()` return value is destroyed in this case as soon as the
314 internal range iterators are created.  The range iterators point into
315 the vector's data, but lock is released immediately, before executing
316 the loop body.
318 Needless to say, this is a crime punishable by long debugging nights.
320 Range-based for loops are slightly subtle about the lifetime of objects
321 used in the initializer statement.  Most other problematic use cases are
322 a bit easier to spot than this, since the lifetime of the `LockedPtr` is
323 more explicitly visible.
325 #### `withLock()`
327 As an alternative to the `lock()` API, `Synchronized` also provides a
328 `withLock()` method that executes a function or lambda expression while
329 holding the lock.  The function receives a reference to the data as its
330 only argument.
332 This has a few benefits compared to `lock()`:
334 * The lambda expression requires its own nested scope, making critical
335   sections more visible in the code.  Callers are recommended to define
336   a new scope when using `lock()` if they choose to, but this is not
337   required.  `withLock()` ensures that a new scope must always be
338   defined.
339 * Because a new scope is required, `withLock()` also helps encourage
340   users to release the lock as soon as possible.  Because the critical
341   section scope is easily visible in the code, it is harder to
342   accidentally put extraneous code inside the critical section without
343   realizing it.
344 * The separate lambda scope makes it more difficult to store raw
345   pointers or references to the protected data and continue using those
346   pointers outside the critical section.
348 For example, `withLock()` makes the range-based for loop mistake from
349 above much harder to accidentally run into:
351 ``` Cpp
352     vec.withLock([](auto& locked) {
353       for (int& n : locked) {
354         n *= 2;
355       }
356     });
359 This code does not have the same problem as the counter-example with
360 `wlock()` above, since the lock is held for the duration of the loop.
362 When using `Synchronized` with a shared mutex type, it provides separate
363 `withWLock()` and `withRLock()` methods instead of `withLock()`.
365 #### `ulock()` and `withULockPtr()`
367 `Synchronized` also supports upgrading and downgrading mutex lock levels as
368 long as the mutex type used to instantiate the `Synchronized` type has the
369 same interface as the mutex types in the C++ standard library, or if
370 `LockTraits` is specialized for the mutex type and the specialization is
371 visible. See below for an intro to upgrade mutexes.
373 An upgrade lock can be acquired as usual either with the `ulock()` method or
374 the `withULockPtr()` method as so
376 ``` Cpp
377     {
378       // only const access allowed to the underlying object when an upgrade lock
379       // is acquired
380       auto ulock = vec.ulock();
381       auto newSize = ulock->size();
382     }
384     auto newSize = vec.withULockPtr([](auto ulock) {
385       // only const access allowed to the underlying object when an upgrade lock
386       // is acquired
387       return ulock->size();
388     });
391 An upgrade lock acquired via `ulock()` or `withULockPtr()` can be upgraded or
392 downgraded by calling any of the following methods on the `LockedPtr` proxy
394 * `moveFromUpgradeToWrite()`
395 * `moveFromWriteToUpgrade()`
396 * `moveFromWriteToRead()`
397 * `moveFromUpgradeToRead()`
399 Calling these leaves the `LockedPtr` object on which the method was called in
400 an invalid `null` state and returns another LockedPtr proxy holding the
401 specified lock.  The upgrade or downgrade is done atomically - the
402 `Synchronized` object is never in an unlocked state during the lock state
403 transition.  For example
405 ``` Cpp
406     auto ulock = obj.ulock();
407     if (ulock->needsUpdate()) {
408       auto wlock = ulock.moveFromUpgradeToWrite();
410       // ulock is now null
412       wlock->updateObj();
413     }
416 This "move" can also occur in the context of a `withULockPtr()`
417 (`withWLockPtr()` or `withRLockPtr()` work as well!) function as so
419 ``` Cpp
420     auto newSize = obj.withULockPtr([](auto ulock) {
421       if (ulock->needsUpdate()) {
423         // release upgrade lock get write lock atomically
424         auto wlock = ulock.moveFromUpgradeToWrite();
425         // ulock is now null
426         wlock->updateObj();
428         // release write lock and acquire read lock atomically
429         auto rlock = wlock.moveFromWriteToRead();
430         // wlock is now null
431         return rlock->newSize();
433       } else {
435         // release upgrade lock and acquire read lock atomically
436         auto rlock = ulock.moveFromUpgradeToRead();
437         // ulock is now null
438         return rlock->newSize();
439       }
440     });
443 #### Intro to upgrade mutexes:
445 An upgrade mutex is a shared mutex with an extra state called `upgrade` and an
446 atomic state transition from `upgrade` to `unique`. The `upgrade` state is more
447 powerful than the `shared` state but less powerful than the `unique` state.
449 An upgrade lock permits only const access to shared state for doing reads. It
450 does not permit mutable access to shared state for doing writes. Only a unique
451 lock permits mutable access for doing writes.
453 An upgrade lock may be held concurrently with any number of shared locks on the
454 same mutex. An upgrade lock is exclusive with other upgrade locks and unique
455 locks on the same mutex - only one upgrade lock or unique lock may be held at a
456 time.
458 The upgrade mutex solves the problem of doing a read of shared state and then
459 optionally doing a write to shared state efficiently under contention. Consider
460 this scenario with a shared mutex:
462 ``` Cpp
463     struct MyObect {
464       bool isUpdateRequired() const;
465       void doUpdate();
466     };
468     struct MyContainingObject {
469       folly::Synchronized<MyObject> sync;
471       void mightHappenConcurrently() {
472         // first check
473         if (!sync.rlock()->isUpdateRequired()) {
474           return;
475         }
476         sync.withWLock([&](auto& state) {
477           // second check
478           if (!state.isUpdateRequired()) {
479             return;
480           }
481           state.doUpdate();
482         });
483       }
484     };
487 Here, the second `isUpdateRequired` check happens under a unique lock. This
488 means that the second check cannot be done concurrently with other threads doing
489 first `isUpdateRequired` checks under the shared lock, even though the second
490 check, like the first check, is read-only and requires only const access to the
491 shared state.
493 This may even introduce unnecessary blocking under contention. Since the default
494 mutex type, `folly::SharedMutex`, has write priority, the unique lock protecting
495 the second check may introduce unnecessary blocking to all the other threads
496 that are attempting to acquire a shared lock to protect the first check. This
497 problem is called reader starvation.
499 One solution is to use a shared mutex type with read priority, such as
500 `folly::SharedMutexReadPriority`. That can introduce less blocking under
501 contention to the other threads attempting to acquire a shared lock to do the
502 first check. However, that may backfire and cause threads which are attempting
503 to acquire a unique lock (for the second check) to stall, waiting for a moment
504 in time when there are no shared locks held on the mutex, a moment in time that
505 may never even happen. This problem is called writer starvation.
507 Starvation is a tricky problem to solve in general. But we can partially side-
508 step it in our case.
510 An alternative solution is to use an upgrade lock for the second check. Threads
511 attempting to acquire an upgrade lock for the second check do not introduce
512 unnecessary blocking to all other threads that are attempting to acquire a
513 shared lock for the first check. Only after the second check passes, and the
514 upgrade lock transitions atomically from an upgrade lock to a unique lock, does
515 the unique lock introduce *necessary* blocking to the other threads attempting
516 to acquire a shared lock. With this solution, unlike the solution without the
517 upgrade lock, the second check may be done concurrently with all other first
518 checks rather than blocking or being blocked by them.
520 The example would then look like:
522 ``` Cpp
523     struct MyObject {
524       bool isUpdateRequired() const;
525       void doUpdate();
526     };
528     struct MyContainingObject {
529       folly::Synchronized<MyObject> sync;
531       void mightHappenConcurrently() {
532         // first check
533         if (!sync.rlock()->isUpdateRequired()) {
534           return;
535         }
536         sync.withULockPtr([&](auto ulock) {
537           // second check
538           if (!ulock->isUpdateRequired()) {
539             return;
540           }
541           auto wlock = ulock.moveFromUpgradeToWrite();
542           wlock->doUpdate();
543         });
544       }
545     };
548 Note: Some shared mutex implementations offer an atomic state transition from
549 `shared` to `unique` and some upgrade mutex implementations offer an atomic
550 state transition from `shared` to `upgrade`. These atomic state transitions are
551 dangerous, however, and can deadlock when done concurrently on the same mutex.
552 For example, if threads A and B both hold shared locks on a mutex and are both
553 attempting to transition atomically from shared to upgrade locks, the threads
554 are deadlocked. Likewise if they are both attempting to transition atomically
555 from shared to unique locks, or one is attempting to transition atomically from
556 shared to upgrade while the other is attempting to transition atomically from
557 shared to unique. Therefore, `LockTraits` does not expose either of these
558 dangerous atomic state transitions even when the underlying mutex type supports
559 them. Likewise, `Synchronized`'s `LockedPtr` proxies do not expose these
560 dangerous atomic state transitions either.
562 #### Timed Locking
564 When `Synchronized` is used with a mutex type that supports timed lock
565 acquisition, `lock()`, `wlock()`, and `rlock()` can all take an optional
566 `std::chrono::duration` argument.  This argument specifies a timeout to
567 use for acquiring the lock.  If the lock is not acquired before the
568 timeout expires, a null `LockedPtr` object will be returned.  Callers
569 must explicitly check the return value before using it:
571 ``` Cpp
572     void fun(Synchronized<vector<string>>& vec) {
573       {
574         auto locked = vec.lock(10ms);
575         if (!locked) {
576           throw std::runtime_error("failed to acquire lock");
577         }
578         locked->push_back("hello");
579         locked->push_back("world");
580       }
581       LOG(INFO) << "successfully added greeting";
582     }
585 #### `unlock()` and `scopedUnlock()`
587 `Synchronized` is a good mechanism for enforcing scoped
588 synchronization, but it has the inherent limitation that it
589 requires the critical section to be, well, scoped. Sometimes the
590 code structure requires a fleeting "escape" from the iron fist of
591 synchronization, while still inside the critical section scope.
593 One common pattern is releasing the lock early on error code paths,
594 prior to logging an error message.  The `LockedPtr` class provides an
595 `unlock()` method that makes this possible:
597 ``` Cpp
598     Synchronized<map<int, string>> dic;
599     ...
600     {
601       auto locked = dic.rlock();
602       auto iter = locked->find(0);
603       if (iter == locked.end()) {
604         locked.unlock();  // don't hold the lock while logging
605         LOG(ERROR) << "key 0 not found";
606         return false;
607       }
608       processValue(*iter);
609     }
610     LOG(INFO) << "succeeded";
613 For more complex nested control flow scenarios, `scopedUnlock()` returns
614 an object that will release the lock for as long as it exists, and will
615 reacquire the lock when it goes out of scope.
617 ``` Cpp
619     Synchronized<map<int, string>> dic;
620     ...
621     {
622       auto locked = dic.wlock();
623       auto iter = locked->find(0);
624       if (iter == locked->end()) {
625         {
626           auto unlocker = locked.scopedUnlock();
627           LOG(INFO) << "Key 0 not found, inserting it."
628         }
629         locked->emplace(0, "zero");
630       } else {
631         *iter = "zero";
632       }
633     }
636 Clearly `scopedUnlock()` comes with specific caveats and
637 liabilities. You must assume that during the `scopedUnlock()`
638 section, other threads might have changed the protected structure
639 in arbitrary ways. In the example above, you cannot use the
640 iterator `iter` and you cannot assume that the key `0` is not in the
641 map; another thread might have inserted it while you were
642 bragging on `LOG(INFO)`.
644 Whenever a `LockedPtr` object has been unlocked, whether with `unlock()`
645 or `scopedUnlock()`, it will behave as if it is null.  `isNull()` will
646 return true.  Dereferencing an unlocked `LockedPtr` is not allowed and
647 will result in undefined behavior.
649 #### `Synchronized` and `std::condition_variable`
651 When used with a `std::mutex`, `Synchronized` supports using a
652 `std::condition_variable` with its internal mutex.  This allows a
653 `condition_variable` to be used to wait for a particular change to occur
654 in the internal data.
656 The `LockedPtr` returned by `Synchronized<T, std::mutex>::lock()` has a
657 `as_lock()` method that returns a reference to a
658 `std::unique_lock<std::mutex>`, which can be given to the
659 `std::condition_variable`:
661 ``` Cpp
662     Synchronized<vector<string>, std::mutex> vec;
663     std::condition_variable emptySignal;
665     // Assuming some other thread will put data on vec and signal
666     // emptySignal, we can then wait on it as follows:
667     auto locked = vec.lock();
668     emptySignal.wait(locked.as_lock(),
669                      [&] { return !locked->empty(); });
672 ### `acquireLocked()`
674 Sometimes locking just one object won't be able to cut the mustard. Consider a
675 function that needs to lock two `Synchronized` objects at the
676 same time - for example, to copy some data from one to the other.
677 At first sight, it looks like sequential `wlock()` calls will work just
678 fine:
680 ``` Cpp
681     void fun(Synchronized<vector<int>>& a, Synchronized<vector<int>>& b) {
682       auto lockedA = a.wlock();
683       auto lockedB = b.wlock();
684       ... use lockedA and lockedB ...
685     }
688 This code compiles and may even run most of the time, but embeds
689 a deadly peril: if one threads call `fun(x, y)` and another
690 thread calls `fun(y, x)`, then the two threads are liable to
691 deadlocking as each thread will be waiting for a lock the other
692 is holding. This issue is a classic that applies regardless of
693 the fact the objects involved have the same type.
695 This classic problem has a classic solution: all threads must
696 acquire locks in the same order. The actual order is not
697 important, just the fact that the order is the same in all
698 threads. Many libraries simply acquire mutexes in increasing
699 order of their address, which is what we'll do, too. The
700 `acquireLocked()` function takes care of all details of proper
701 locking of two objects and offering their innards.  It returns a
702 `std::tuple` of `LockedPtr`s:
704 ``` Cpp
705     void fun(Synchronized<vector<int>>& a, Synchronized<vector<int>>& b) {
706       auto ret = folly::acquireLocked(a, b);
707       auto& lockedA = std::get<0>(ret);
708       auto& lockedB = std::get<1>(ret);
709       ... use lockedA and lockedB ...
710     }
713 Note that C++ 17 introduces
714 [structured binding syntax](http://wg21.link/P0144r2)
715 which will make the returned tuple more convenient to use:
717 ``` Cpp
718     void fun(Synchronized<vector<int>>& a, Synchronized<vector<int>>& b) {
719       auto [lockedA, lockedB] = folly::acquireLocked(a, b);
720       ... use lockedA and lockedB ...
721     }
724 An `acquireLockedPair()` function is also available, which returns a
725 `std::pair` instead of a `std::tuple`.  This is more convenient to use
726 in many situations, until compiler support for structured bindings is
727 more widely available.
729 ### Synchronizing several data items with one mutex
731 The library is geared at protecting one object of a given type
732 with a mutex. However, sometimes we'd like to protect two or more
733 members with the same mutex. Consider for example a bidirectional
734 map, i.e. a map that holds an `int` to `string` mapping and also
735 the converse `string` to `int` mapping. The two maps would need
736 to be manipulated simultaneously. There are at least two designs
737 that come to mind.
739 #### Using a nested `struct`
741 You can easily pack the needed data items in a little struct.
742 For example:
744 ``` Cpp
745     class Server {
746       struct BiMap {
747         map<int, string> direct;
748         map<string, int> inverse;
749       };
750       Synchronized<BiMap> bimap_;
751       ...
752     };
753     ...
754     bimap_.withLock([](auto& locked) {
755       locked.direct[0] = "zero";
756       locked.inverse["zero"] = 0;
757     });
760 With this code in tow you get to use `bimap_` just like any other
761 `Synchronized` object, without much effort.
763 #### Using `std::tuple`
765 If you won't stop short of using a spaceship-era approach,
766 `std::tuple` is there for you. The example above could be
767 rewritten for the same functionality like this:
769 ``` Cpp
770     class Server {
771       Synchronized<tuple<map<int, string>, map<string, int>>> bimap_;
772       ...
773     };
774     ...
775     bimap_.withLock([](auto& locked) {
776       get<0>(locked)[0] = "zero";
777       get<1>(locked)["zero"] = 0;
778     });
781 The code uses `std::get` with compile-time integers to access the
782 fields in the tuple. The relative advantages and disadvantages of
783 using a local struct vs. `std::tuple` are quite obvious - in the
784 first case you need to invest in the definition, in the second
785 case you need to put up with slightly more verbose and less clear
786 access syntax.
788 ### Summary
790 `Synchronized` and its supporting tools offer you a simple,
791 robust paradigm for mutual exclusion-based concurrency. Instead
792 of manually pairing data with the mutexes that protect it and
793 relying on convention to use them appropriately, you can benefit
794 of encapsulation and typechecking to offload a large part of that
795 task and to provide good guarantees.