So I'm pretty new to CPP and i was trying to implement a resource pool (SQLITE connections), for a small project that I'm developing.
The problem is that I have a list(vector) with objects created at the beginning of the program, that have the given connection and its availability (atomic_bool). If my program requests a connection from this pool, the following function gets executed:
gardener_db &connection_pool_inner::get_connection() {
bool expected = false;
for(auto & pair : pool) {
std::atomic_bool &ref = pair->get_is_busy_ref();
if (ref.compare_exchange_strong(expected, true)) {
return pair->get_conn();
}//if false it means that its busy
}
std::lock_guard<std::mutex> guard(vector_mutex);//increment size
std::atomic_bool t = true;
pool.emplace_back(std::make_shared<pool_elem>());
pool.back()->get_is_busy_ref().store(t);// because we are giving the resource to the caller
return pool.back()->get_conn();
}
So I made a simple test to see if my vector is resizing:
constexpr unsigned int CONNECTION_POOL_START_SIZE = 20;
TEST(testConnPool, regrow) {
auto saturation = std::vector<connection_handler>();
ASSERT_EQ(saturation.size(), 0);
for(int i = 0; i < CONNECTION_POOL_START_SIZE; i++) {
saturation.emplace_back();
}
auto ptr = connection_pool_inner::instance();
auto size = ptr->get_pool().size();
//it should be full at this point
ASSERT_EQ(size, CONNECTION_POOL_START_SIZE);
}
the problem is that I get 22 as my size as opposed to 20 which is what I would expect.
I was able to narrow down the problem to the compare_exchage_strong() but, it was my understanding that the "strong" variant wouldn't fail. So I debugged it, and its always the third element of my vector that gets skipped, (even when working on a single thread).
I already test it the same thing on a different computer(and architecture) but the same problem still occurs so I'm guessing the problem is the logic.
Any ideas about what's going on?
#include <mutex>
#include <atomic>
#include <iostream>
#include <vector>
#include <memory>
class foo {
std::atomic_bool inner;
public:
explicit foo(): inner(false){};
std::atomic_bool &get(){return inner;}
};
std::mutex vector_mutex;
std::vector<std::shared_ptr<foo>> resources;
void get_resource() {
bool expected = false;
for(auto & rsc : resources) {
if (rsc->get().compare_exchange_strong(expected, true)) {
return;
}
}
std::lock_guard<std::mutex> guard(vector_mutex);//increment size
resources.emplace_back(std::make_shared<foo>());
}
int main() {
std::vector<std::shared_ptr<foo>> *local = &resources;
for(int i = 0; i < 20; i++) {
resources.emplace_back(std::make_shared<foo>());
}
for(int i = 0; i < 20; i++) {
get_resource();
}
std::cout << resources.size();
}
The code results in undefined behavior in a multithreaded environment.
While the loop for(auto & pair : pool)
is runing in one thread, pool.emplace_back(std::make_shared<pool_elem>())
in another thread invalidates pool
iterators that are used in the running loop under the hood.
You have the error in the loop. std::atomic::compare_exchange_strong:
<...> loads the actual value stored in
*this
intoexpected
(performs load operation).
Let's vector busy
to be a conditional name for busy states.
The first get_connection()
call results in the vector busy
to be { true, false, ... false }
.
The second get_connection()
call:
expected = false;
busy[0]
is true
is not equal to expected
, it gets expected = true;
busy[1]
is false
is not equal to the updated expected
, it gets expected = false;
busy[2]
is false
is equal to the updated expected
, results in the vector busy
to be { true, false, true, false, ... false }
.The further 8 get_connection()
calls result in the vector busy
to be { (true, false) * 10 }
.
The 11th and 12th get_connection()
calls add yet a couple of true
and result in the vector busy
to be { (true, false) * 10, true, true }
, the size is 22.
Other get_connection()
calls do not modify the vector anymore:
busy[10]
is true
is not equal to expected
, it gets expected = true;
busy[11]
is true
is equal to the updated expected
, return.The fix
// bool expected = false; ───────┐
// │
for(auto & pair : pool) { // │
bool expected = false; // ◄──┘
std::atomic_bool &ref = pair->get_is_busy_ref();
if (ref.compare_exchange_strong(expected, true)) {
return pair->get_conn();
}//if false it means that its busy
}