As we know, the correct usage of std::lock_guard is like this RAII style:
void increase_decrease() {
std::lock_guard<std::mutex> guard(global_mutex);
static const int times = 50;
for (int i = 0; i < times; i++) {
global_data ++;
}
for (int i = 0; i < times; i++) {
global_data --;
}
}
Here, my point is not about how to use std::lock_guard
or mutex.
In the code below, we deliberately use std::lock_guard
in a wrong way. (That is to put it into a block before the critical section.)
16 threads are created to add 1 to and subtract 1 from a global int variable, which is initialized as 0, for 50 times.
std::lock_guard
is called in a block, and the block is before the critical section (WRONG WAY! Never do something like this!). Mutex will be released after the block(wrong usage, again), following RAII-style mechanism. So, when it goes into the critical section, no lock is there.
#include <iostream>
#include <chrono>
#include <thread>
#include <mutex>
#include <vector>
int global_data = 0;
std::mutex global_mutex;
void increase_decrease() {
// XXX: INCORRECT USAGE! INCORRECT USAGE! INCORRECT USAGE!
{
std::lock_guard<std::mutex> guard(global_mutex);
}
// // XXX: uncomment to sleep for a litter while
// std::this_thread::sleep_for(std::chrono::milliseconds(10));
static const int times = 50;
for (int i = 0; i < times; i++) {
global_data ++;
}
for (int i = 0; i < times; i++) {
global_data --;
}
}
void try_mutex() {
const int num_workers = 16;
std::vector<std::thread> workers;
auto start = std::chrono::system_clock::now();
for (int i = 0; i < num_workers; i++) {
std::thread t(increase_decrease);
workers.push_back(std::move(t));
}
for (auto &t: workers) {
t.join();
}
auto end = std::chrono::system_clock::now();
std::chrono::duration<double> elapsed_seconds = end-start;
std::cout << "global_data: " << global_data
<< ", elapsed second: " << elapsed_seconds.count();
}
int main() {
try_mutex();
}
I found that whether sleeping for 10ms causes different results.
Without sleep, stdout of 20 calls of main is:
global_data: 0, elapsed second: 0.000363
global_data: 0, elapsed second: 0.000359
global_data: 0, elapsed second: 0.000349
global_data: 0, elapsed second: 0.000345
global_data: 0, elapsed second: 0.000352
global_data: 0, elapsed second: 0.000323
global_data: 0, elapsed second: 0.000619
global_data: 0, elapsed second: 0.000431
global_data: 34, elapsed second: 0.000405
global_data: -14, elapsed second: 0.000415
global_data: 0, elapsed second: 0.000497
global_data: 0, elapsed second: 0.000366
global_data: 0, elapsed second: 0.000413
global_data: 0, elapsed second: 0.000406
global_data: 0, elapsed second: 0.000353
global_data: 0, elapsed second: 0.000363
global_data: 0, elapsed second: 0.000361
global_data: 0, elapsed second: 0.000358
global_data: 0, elapsed second: 0.000348
global_data: 0, elapsed second: 0.000367
However, if we uncomment the sleep, stdout of 20 calls of main is:
global_data: 44, elapsed second: 0.011108
global_data: 15, elapsed second: 0.010645
global_data: 25, elapsed second: 0.012905
global_data: 27, elapsed second: 0.012914
global_data: 9, elapsed second: 0.012871
global_data: 46, elapsed second: 0.012836
global_data: 44, elapsed second: 0.011307
global_data: -2, elapsed second: 0.01286
global_data: 77, elapsed second: 0.012853
global_data: 43, elapsed second: 0.011984
global_data: 0, elapsed second: 0.011134
global_data: -3, elapsed second: 0.011571
global_data: 49, elapsed second: 0.012438
global_data: 43, elapsed second: 0.011552
global_data: -20, elapsed second: 0.010807
global_data: 0, elapsed second: 0.010514
global_data: 0, elapsed second: 0.010916
global_data: -44, elapsed second: 0.012829
global_data: 50, elapsed second: 0.011759
global_data: 9, elapsed second: 0.012873
The probability that global_data
equals to 0
is much larger in the first case than in the second one. I tried many times. It's not just a coincidence.
So, it seems that there is a chance for mutex to take effect for a little while after the block where it got acquired via std::lock_guard
. Why?
Thank you.