Safe parallel read-only access to a STL container - c++

I want access a STL based container read-only from parallel running threads. Without using any user implemented locking. The base of the following code is C++11 with a proper implementation of the standard.
http://gcc.gnu.org/onlinedocs/libstdc++/manual/using_concurrency.html
http://www.sgi.com/tech/stl/thread_safety.html
http://www.hpl.hp.com/personal/Hans_Boehm/c++mm/threadsintro.html
http://www.open-std.org/jtc1/sc22/wg21/ (current draft or N3337, which is essentially C++11 with minor errors and typos corrected)
23.2.2 Container data races [container.requirements.dataraces]
For purposes of avoiding data races (17.6.5.9), implementations shall
consider the following functions to be const: begin, end, rbegin,
rend, front, back, data, find, lower_bound, upper_bound, equal_range,
at and, except in associative or unordered associative containers,
operator[].
Notwithstanding (17.6.5.9), implementations are required
to avoid data races when the contents of the con- tained object in
different elements in the same sequence, excepting vector<bool>, are
modified concurrently.
[ Note: For a vector<int> x with a size greater
than one, x[1] = 5 and *x.begin() = 10 can be executed concurrently
without a data race, but x[0] = 5 and *x.begin() = 10 executed
concurrently may result in a data race. As an exception to the general
rule, for a vector<bool> y, y[0] = true may race with y[1]
= true. — end note ]
and
17.6.5.9 Data race avoidance [res.on.data.races] 1 This section specifies requirements that implementations shall meet to prevent data
races (1.10). Every standard library function shall meet each
requirement unless otherwise specified. Implementations may prevent
data races in cases other than those specified below.
2 A C++ standard
library function shall not directly or indirectly access objects
(1.10) accessible by threads other than the current thread unless
the objects are accessed directly or indirectly via the function’s
argu- ments, including this.
3 A C++ standard library function shall
not directly or indirectly modify objects (1.10) accessible by threads
other than the current thread unless the objects are accessed directly
or indirectly via the function’s non-const arguments, including
this.
4 [ Note: This means, for example, that implementations can’t
use a static object for internal purposes without synchronization
because it could cause a data race even in programs that do not
explicitly share objects between threads. — end note ]
5 A C++ standard library function shall not access objects indirectly
accessible via its arguments or via elements of its container
arguments except by invoking functions required by its specification
on those container elements.
6 Operations on iterators obtained by
calling a standard library container or string member function may
access the underlying container, but shall not modify it. [ Note: In
particular, container operations that invalidate iterators conflict
with operations on iterators associated with that container. — end
note ]
7 Implementations may share their own internal objects between
threads if the objects are not visible to users and are protected
against data races.
8 Unless otherwise specified, C++ standard library
functions shall perform all operations solely within the current
thread if those operations have effects that are visible (1.10) to
users.
9 [ Note: This allows implementations to parallelize operations
if there are no visible side effects. — end note ]
Conclusion
Containers are not thread safe! But it is safe to call const functions on containers from multiple parallel threads. So it is possible to do read-only operations from parallel threads without locking.
Am I right?
I pretend that their doesn't exist any faulty implementation and every implementation of the C++11 standard is correct.
Sample:
// concurrent thread access to a stl container
// g++ -std=gnu++11 -o p_read p_read.cpp -pthread -Wall -pedantic && ./p_read
#include <iostream>
#include <iomanip>
#include <string>
#include <unistd.h>
#include <thread>
#include <mutex>
#include <map>
#include <cstdlib>
#include <ctime>
using namespace std;
// new in C++11
using str_map = map<string, string>;
// thread is new in C++11
// to_string() is new in C++11
mutex m;
const unsigned int MAP_SIZE = 10000;
void fill_map(str_map& store) {
int key_nr;
string mapped_value;
string key;
while (store.size() < MAP_SIZE) {
// 0 - 9999
key_nr = rand() % MAP_SIZE;
// convert number to string
mapped_value = to_string(key_nr);
key = "key_" + mapped_value;
pair<string, string> value(key, mapped_value);
store.insert(value);
}
}
void print_map(const str_map& store) {
str_map::const_iterator it = store.begin();
while (it != store.end()) {
pair<string, string> value = *it;
cout << left << setw(10) << value.first << right << setw(5) << value.second << "\n";
it++;
}
}
void search_map(const str_map& store, int thread_nr) {
m.lock();
cout << "thread(" << thread_nr << ") launched\n";
m.unlock();
// use a straight search or poke around random
bool straight = false;
if ((thread_nr % 2) == 0) {
straight = true;
}
int key_nr;
string mapped_value;
string key;
str_map::const_iterator it;
string first;
string second;
for (unsigned int i = 0; i < MAP_SIZE; i++) {
if (straight) {
key_nr = i;
} else {
// 0 - 9999, rand is not thread-safe, nrand48 is an alternative
m.lock();
key_nr = rand() % MAP_SIZE;
m.unlock();
}
// convert number to string
mapped_value = to_string(key_nr);
key = "key_" + mapped_value;
it = store.find(key);
// check result
if (it != store.end()) {
// pair
first = it->first;
second = it->second;
// m.lock();
// cout << "thread(" << thread_nr << ") " << key << ": "
// << right << setw(10) << first << setw(5) << second << "\n";
// m.unlock();
// check mismatch
if (key != first || mapped_value != second) {
m.lock();
cerr << key << ": " << first << second << "\n"
<< "Mismatch in thread(" << thread_nr << ")!\n";
exit(1);
// never reached
m.unlock();
}
} else {
m.lock();
cerr << "Warning: key(" << key << ") not found in thread("
<< thread_nr << ")\n";
exit(1);
// never reached
m.unlock();
}
}
}
int main() {
clock_t start, end;
start = clock();
str_map store;
srand(0);
fill_map(store);
cout << "fill_map finished\n";
// print_map(store);
// cout << "print_map finished\n";
// copy for check
str_map copy_store = store;
// launch threads
thread t[10];
for (int i = 0; i < 10; i++) {
t[i] = thread(search_map, store, i);
}
// wait for finish
for (int i = 0; i < 10; i++) {
t[i].join();
}
cout << "search_map threads finished\n";
if (store == copy_store) {
cout << "equal\n";
} else {
cout << "not equal\n";
}
end = clock();
cout << "CLOCKS_PER_SEC " << CLOCKS_PER_SEC << "\n";
cout << "CPU-TIME START " << start << "\n";
cout << "CPU-TIME END " << end << "\n";
cout << "CPU-TIME END - START " << end - start << "\n";
cout << "TIME(SEC) " << static_cast<double>(end - start) / CLOCKS_PER_SEC << "\n";
return 0;
}
This code can be compiled with GCC 4.7 and runs fine on my machine.
$ echo $?
$ 0

A data-race, from the C++11 specification in sections 1.10/4 and 1.10/21, requires at least two threads with non-atomic access to the same set of memory locations, the two threads are not synchronized with regards to accessing the set of memory locations, and at least one thread writes to or modifies an element in the set of memory locations. So in your case, if the threads are only reading, you are fine ... by definition since none of the threads write to the same set of memory locations, there are no data-races even though there is no explicit synchronization mechanism between the threads.

Yes, you are right. You are safe as long as the thread that populates your vector finishes doing so before the reader threads start. There was a similar question recently.

Related

Thread-safe access to nested map entries [c++]

I have an unordered_map<int, float> localCostMap describing the costs (float) between node IDs (int). The calculation of the second value is quite complex, but due to the structure of graph (directed, acyclic, up to two parent nodes) I can save many calculations by pooling the maps for each node into another map like so:
unordered_map<int, shared_ptr<unordered_map<int, float>>> pooledMaps.
Once the values are written (into localCostMap), they do not get updated again, but the calculations required the map entries of the connected nodes, which may leed to lock-ups.
How can I make it so that I can read the values stored in the inner map while also safely adding new entries (e.g. { 3, 1.23 }? I'm new to multithreading and have tried to search for solutions, but the only results I got were older, despite reading that multithreading has improved much, particularly in C++20.
Thank you in advance!
Edit: As requested, here is a minimal working example. Of course, the full algorithm is more complex, considers the edge cases and enters them also for other applicable nodes (e.g. the result of comparing 5 & 7 also applies for 6 & 7).
// Example.h
#pragma once
#include <iostream>
#include <unordered_map>
#include <thread>
struct MyNode {
int id;
int leftNodeID;
int rightNodeID;
std::shared_ptr<std::unordered_map<int, float>> localCostMap; /* inherited from parent (left/right) nodes */
std::unordered_map<int, std::shared_ptr<std::unordered_map<int, float>>> pooledMaps;
MyNode() : id(0), leftNodeID(0), rightNodeID(0) { setLocalCostMap(); }
MyNode(int _id, int leftID, int rightID) :
id(_id), leftNodeID(leftID), rightNodeID(rightID) { setLocalCostMap(); }
void setLocalCostMap();
float calculateNodeCost(int otherNodeID);
};
// Example.cpp
#include "NodeMapMin.h"
MyNode nodes[8];
void MyNode::setLocalCostMap() {
if (leftNodeID == 0) { // rightNodeID also 0
localCostMap = std::make_shared<std::unordered_map<int, float>>();
}
else { // get map from connectednode if possible
auto poolmap = nodes[leftNodeID].pooledMaps.find(rightNodeID);
if (poolmap == nodes[leftNodeID].pooledMaps.end()) {
localCostMap = std::make_shared<std::unordered_map<int, float>>();
nodes[leftNodeID].pooledMaps.insert({ rightNodeID, localCostMap }); // [1] possible conflict
nodes[rightNodeID].pooledMaps.insert({ leftNodeID, localCostMap }); // [1] possible conflict
}
else { localCostMap = poolmap->second; }
}
}
float MyNode::calculateNodeCost(int otherNodeID) {
if (id > 0) {
std::cout << "calculateNodeCost for " << nodes[id].id << " and " << nodes[otherNodeID].id << std::endl;
}
float costs = -1.0f;
auto mapIterator = localCostMap->find(otherNodeID);
if (mapIterator == localCostMap->end()) {
if (id == otherNodeID) { // same node
std::cout << "return costs for " << id << " and " << otherNodeID << " (same node): " << 0.0f << std::endl;
return 0.0f;
}
else if (leftNodeID == 0 || nodes[otherNodeID].leftNodeID == 0) {
costs = ((float)(id + nodes[otherNodeID].id)) / 2;
std::cout << "calculated costs for " << id << " and " << otherNodeID << " (no connections): " << costs << std::endl;
}
else if (leftNodeID == nodes[otherNodeID].leftNodeID &&
rightNodeID == nodes[otherNodeID].rightNodeID) { // same connected nodes
costs = nodes[leftNodeID].calculateNodeCost(rightNodeID); // [2] possible conflict
std::cout << "return costs for " << id << " and " << otherNodeID << " (same connections): " << costs << std::endl;
return costs;
}
else {
costs = nodes[leftNodeID].calculateNodeCost(otherNodeID) +
nodes[rightNodeID].calculateNodeCost(otherNodeID) +
nodes[id].calculateNodeCost(nodes[otherNodeID].leftNodeID) +
nodes[id].calculateNodeCost(nodes[otherNodeID].rightNodeID); // [2] possible conflict
std::cout << "calculated costs for " << id << " and " << otherNodeID << ": " << costs << std::endl;
}
// [3] possible conflict
localCostMap->insert({ otherNodeID, costs });
nodes[otherNodeID].localCostMap->insert({ id, costs });
}
else {
costs = mapIterator->second;
std::cout << "found costs for " << id << " and " << otherNodeID << ": " << costs << std::endl;
}
return costs;
}
float getNodeCost(int node1, int node2) {
return nodes[node1].calculateNodeCost(node2);
}
int main()
{
nodes[0] = MyNode(0, 0, 0); // should not be used
nodes[1] = MyNode(1, 0, 0);
nodes[2] = MyNode(2, 0, 0);
nodes[3] = MyNode(3, 0, 0);
nodes[4] = MyNode(4, 0, 0);
nodes[5] = MyNode(5, 1, 2);
nodes[6] = MyNode(6, 1, 2);
nodes[7] = MyNode(7, 3, 4);
//getNodeCost(5, 7);
//getNodeCost(6, 7);
std::thread w1(getNodeCost, 5, 7);
std::thread w2(getNodeCost, 6, 7);
w1.join();
w2.join();
std::cout << "done";
}
I commented out the single-thread variant, but you can easily see the difference as the multi-threaded version already has more (unneccessary) comparisons.
As you can see, whenever two "noteworthy" comparisons take place, the result is added to localCostMap, which is normally derived from the connected two nodes. Thus, one insert is necessary for all nodes with these two connections (left & right).
I see at least 3 problematic point:
When initializing the node and inserting the pooled maps for the connected nodes: if two nodes with the same connections were to be added at the same time, they would both want to create and add the maps for the connected nodes. [1]
When calculating the values, another thread might already be doing it, thus leading to unneccessary calculations. [2]
When inserting the results into localCostMap (and by that also to the maps of the connected nodes). [3]
If you already have a std::shared_ptr to one of the inner maps it can be safely used, since, as you explained, once created it is never updated by any execution thread.
However since the outer map is being modified, all access to the outer map must be thread safe. None of the containers in the C++ library are thread safe, it is your responsibility to make them thread safe when needed. This includes threads that only access the outer map, since other execution threads might be modifying it. When something is modified all execution threads are required to use thread-safe access.
This means holding a mutex lock. The best way to avoid bugs that involve thread safety is to make it logically impossible to access something without holding a lock. And the most direct way of enforcing this would be the map to be wrapped as a private class member, with the only way to access it is to call a public method that grabs a mutex lock:
#include <unordered_map>
#include <memory>
#include <mutex>
#include <iostream>
class costs {
std::unordered_map<int, std::shared_ptr<std::unordered_map<int, float>
>> m;
std::mutex mutex;
public:
template<typename T>
auto operator()(T && t)
{
std::unique_lock lock{mutex};
return t(m);
}
};
int main() {
costs c;
// Insert into the map
c([&](auto &m) {
auto values=std::make_shared<std::unordered_map<int, float>>();
(*values)[1]=2;
m.emplace(1, values);
});
// Look things up in a map
auto values=c([]
(auto &m) -> std::shared_ptr<std::unordered_map<int, float>>
{
auto iter=m.find(1);
if (iter == m.end())
return nullptr;
return iter->second;
});
// values can now be used, since nothing will modify it.
return 0;
}
This uses some convenient features of modern C++, but can be implemented all the way back to C++11, with some additional typing.
The only way to access the map is to call the class's () operator which acquires a lock and calls the passed-in callable object, like a lambda, passing to it a reference to the outer map. The lambda can do whatever it wants with it, before returning, at which point the lock gets released.
It is not entirely impossible to defeat this kind of enforced thread safety, but you'll have to go out of your way to access this outer unordered map without holding a lock.
For completeness' sake you may need to implement a second () overload as a const class method.
Note that the second example looks up one of the inner maps and returns it, at which point it's accessible without any locks being held. Presumably nothing would modify it.
You should consider using maps of std::shared_ptr<const std::unordered_map<int, float> instead of std::shared_ptr<std::unordered_map<int, float>. This will let your C++ compiler enforce the fact that, presumably, once created these maps will never be modified. Like I already mentioned: the best way to avoid bugs is to make it logically impossible for them to happen.

Multithreading and sequence of instructions

While learning multithread programming I've written the following code.
#include <thread>
#include <iostream>
#include <cassert>
void check() {
int a = 0;
int b = 0;
{
std::jthread t2([&](){
int i = 0;
while (a >= b) {
++i;
}
std::cout << "failed at iteration " << i << "\n"
// I know at this point a and b may have changed
<< a << " >= " << b << "\n";
std::exit(0);
});
std::jthread t1([&](){
while (true) {
++a;
++b;
}
});
}
}
int main() {
check();
}
Since ++a always happens before ++b a should be always greater or equal to b.
But experiment shows that sometimes b > a. Why? What causes it? And how can I enforce it?
Even when I replace int a = 0; with int a = 1000; which makes all of this even more crazy.
The program exits soon so no int overflow happens.
I found no instructions reordering in assembly which might have caused this.
Since ++a always happens before ++b a should be always greater or
equal to b
Only in its execution thread. And only if that's observable by the execution thread.
C++ requires certain explicit "synchronization" in order for changes made by one execution thread be visible by other execution threads.
++a;
++b;
With these statements alone, there are no means by which this execution thread would actually be able to "tell the difference" whether a or b was incremented first. As such, C++ allows the compiler to implement whatever optimizations or code reordering steps it wants, as long as it has no observable effects in its execution thread, and if the actual generated code incremented b first there will not be any observable effects. There's no way that this execution thread could possibly tell the difference.
But if there was some intervening statement that "looked" at a, then this wouldn't hold true any more, and the compiler would be required to actually generate code that increments a before using it in some way.
And that's just this execution thread, alone. Even if it was possible to observe the relative order of changes to a in b in this execution thread the C++ compiler is allowed, by the standard, to actually increment the actual variables in any order, as long as there are also any other adjustments that make this not observable. But it could be observable by another execution thread. To prevent that it will be necessary to take explicit synchronization steps, using mutexes, condition variables, and other parts of C++'s execution thread model.
There are non-trivial race conditions between the increment of these different variables and when you read them. If you want strict ordering of these reads and writes you will have to use some sort of synchronization mechanism. std::atomic<> makes it easier.
Try this instead:
#include<iostream>
#include <thread>
#include <iostream>
#include <cassert>
#include <atomic>
void check() {
struct AB { int a = 0; int b=0; };
std::atomic<AB> ab;
{
std::jthread t2([&](){
int i = 0;
AB temp;
while (true) {
temp = ab;
if ( temp.a > temp.b ) break;
++i;
}
std::cout << "failed at iteration " << i << "\n"
// I know at this point a and b may have changed
<< temp.a << " >= " << temp.b << "\n";
std::exit(0);
});
std::jthread t1([&](){
while (true) {
AB temp = ab;
temp.a++;
temp.b++;
ab = temp;
}
});
}
}
int main() {
check();
}
Code: https://godbolt.org/z/Kxeb8d8or
Result:
Program returned: 143
Program stderr
Killed - processing time exceeded

Do i have to mutex a reading operation while other threads are writing thread safe?

i am confused in a specific multithreading situation and couldn´t find clear explanations to this scenario. In the code below two custom threads are writing+reading thread safe but the main thread is also reading concurrently. So here is my question: Do i have to mutex the read function, too? Or is it absolutly impossible to crash the app maybe cause of previously deleted pointers in the vector for example? I hope you guys can help me, Thanks!
#include <thread>
#include <mutex>
#include <iostream>
#include <vector>
int g_i = 0;
std::vector<int> test;
std::mutex g_i_mutex; // protects g_i
void safe_increment()
{
std::lock_guard<std::mutex> lock(g_i_mutex);
++g_i;
test.resize(test.size() + 1, 2);
std::cout << std::this_thread::get_id() << ": " << g_i << '\n';
for (std::vector<int>::const_iterator i = test.begin(); i != test.end(); ++i)
std::cout << std::this_thread::get_id() << " thread vector: " << *i << '\n';
// g_i_mutex is automatically released when lock
// goes out of scope
}
void request_threadedvar()
{
for (std::vector<int>::const_iterator i = test.begin(); i != test.end(); ++i)
std::cout << std::this_thread::get_id() << " request threaded vector: " << *i << '\n';
}
int main()
{
std::cout << "main: " << g_i << '\n';
test.resize(test.size() + 1, 1);
for (std::vector<int>::const_iterator i = test.begin(); i != test.end(); ++i)
std::cout << "main vector: " << *i << '\n';
std::thread t1(safe_increment);
request_threadedvar();
std::thread t2(safe_increment);
t1.join();
t2.join();
std::cout << "main: " << g_i << '\n';
for (std::vector<int>::const_iterator i = test.begin(); i != test.end(); ++i)
std::cout << "main vector: " << *i << '\n';
}
Both std::thread execution threads, as well as the original main execution thread are invoking various methods of the same std::vector object.
None of std::vector's methods (or any of the C++ library containers' methods) are thread-safe, so all access to them from all execution threads must be sequenced (i.e. protected by a mutex). It doesn't matter whether the vector's contents are modified, or not. begin() is not thread safe. Full stop. Etc...
Adding insult to injury, resize() invalidates all existing iterators to the contents of a std::vector, so a resize() that gets executed by any of the std::threads will immediately invalidate all iterators to the same std::vector object in use by the other threads; and thusly the whole thing must be sequenced/locked.
TL;DR: you must use a mutex for any access to a std::vector. Whether the contents of the vector are modified, or not, by a particular execution thread, is immaterial.
P.S. The issue of std::vector methods not being thread safe is independent of the thread-safety of whatever's in the vector. If you already have all the iterators to the vector's contents ready, and start doing things to what's in the vector using the existing iterators in multiple execution threads: whether that requires sequencing depends on the inherent nature of whatever's in your vector, and its thread-safety requirements.

C++11 vector argument to thread appears uninitialized

I am trying to create a proof of concept for inter-thread communication by meanings of shared state: the main thread creates worker threads giving each a separate vector by reference, lets each do its work and fill its vector with results, and finally collects the results.
However, weird things are happening for which I can't find an explanation other than some race between the initialization of the vectors and the launch of the worker threads. Here is the code.
#include <iostream>
#include <vector>
#include <thread>
class Case {
public:
int val;
Case(int i):val(i) {}
};
void
run_thread (std::vector<Case*> &case_list, int idx)
{
std::cout << "size in thread " << idx <<": " << case_list.size() << '\n';
for (int i=0; i<10; i++) {
case_list.push_back(new Case(i));
}
}
int
main(int argc, char **argv)
{
int nthrd = 3;
std::vector<std::thread> threads;
std::vector<std::vector<Case*>> case_lists;
for (int i=0; i<nthrd; i++) {
case_lists.push_back(std::vector<Case*>());
std::cout << "size of " << i << " in main:" << case_lists[i].size() << '\n';
threads.push_back( std::thread( run_thread, std::ref(case_lists[i]), i) );
}
std::cout << "All threads lauched.\n";
for (int i=0; i<nthrd; i++) {
threads[i].join();
for (const auto cp:case_lists[i]) {
std::cout << cp->val << '\n';
}
}
return 0;
}
Tested on repl.it (gcc 4.6.3), the program gives the following result:
size of 0 in main:0
size of 1 in main:0
size of 2 in main:0
All threads lauched.
size in thread 0: 18446744073705569740
size in thread 2: 0
size in thread 1: 0
terminate called after throwing an instance of 'std::bad_alloc'
what(): std::bad_alloc
exit status -1
On my computer, besides something like the above, I also get:
Segmentation fault (core dumped)
It appears thread 0 is getting a vector that hasn't been initialized, although the vector appears properly initialized in main.
To isolate the problem, I have tried going single threaded by changing the line:
threads.push_back( std::thread( run_thread, std::ref(case_lists[i]), i) );
to
run_thread(case_lists[i], i);
and commenting out:
threads[i].join();
Now the program runs as expected, with the "threads" running one after another before the main collects the results.
My question is: what is wrong with the multi-threaded version above?
References (and iterators) for a vector are invalidated any time the capacity of the vector changes. The exact rules for overallocation vary by implementation, but odds are, you've got at least one capacity change between the first push_back and the last, and all the references made before that final capacity increase are garbage the moment it occurs, invoking undefined behavior.
Either reserve your total vector size up front (so push_backs don't cause capacity increases), initialize the whole vector to the final size up front (so no resizes occur at all), or have one loop populate completely, then launch the threads (so all resizes occur before you extract any references). The simplest fix here would be to initialize it to the final size, changing:
std::vector<std::vector<Case*>> case_lists;
for (int i=0; i<nthrd; i++) {
case_lists.push_back(std::vector<Case*>());
std::cout << "size of " << i << " in main:" << case_lists[i].size() << '\n';
threads.push_back( std::thread( run_thread, std::ref(case_lists[i]), i) );
}
to:
std::vector<std::vector<Case*>> case_lists(nthrd); // Default initialize nthrd elements up front
for (int i=0; i<nthrd; i++) {
// No push_back needed
std::cout << "size of " << i << " in main:" << case_lists[i].size() << '\n';
threads.push_back( std::thread( run_thread, std::ref(case_lists[i]), i) );
}
You might be thinking that vectors would overallocate fairly aggressively, but at least on many popular compilers, this is not the case; both gcc and clang follow a strict doubling pattern, so the first three insertions reallocate every time (capacity goes from 1, to 2, to 4); the reference to the first element is invalidated by the insertion of the second, and the reference to the second is invalidated by the insertion of the third.

Is it safe/efficient to cancel a c++ thread by writing to an outside variable?

I have a search problem, which I want to parallelize. If one thread has found a solution, I want all other threads to stop. Otherwise, if all threads exit regularly, I know, that there is no solution.
The following code (that demonstrates my cancelling strategy) seems to work, but I'm not sure, if it is safe and the most efficient variant:
#include <iostream>
#include <thread>
#include <cstdint>
#include <chrono>
using namespace std;
struct action {
uint64_t* ii;
action(uint64_t *ii) : ii(ii) {};
void operator()() {
uint64_t k = 0;
for(; k < *ii; ++k) {
//do something useful
}
cout << "counted to " << k << " in 2 seconds" << endl;
}
void cancel() {
*ii = 0;
}
};
int main(int argc, char** argv) {
uint64_t ii = 1000000000;
action a{&ii};
thread t(a);
cout << "start sleeping" << endl;
this_thread::sleep_for(chrono::milliseconds(2000));
cout << "finished sleeping" << endl;
a.cancel();
cout << "cancelled" << endl;
t.join();
cout << "joined" << endl;
}
Can I be sure, that the value, to which ii points, always gets properly reloaded? Is there a more efficient variant, that doesn't require the dereferenciation at every step? I tried to make the upper bound of the loop a member variable, but since the constructor of thread copies the instance of action, I wouldn't have access to that member later.
Also: If my code is exception safe and does not do I/O (and I am sure, that my platform is Linux), is there a reason not to use pthread_cancel on the native thread?
No, there's no guarantee that this will do anything sensible. The code has one thread reading the value of ii and another thread writing to it, without any synchronization. The result is that the behavior of the program is undefined.
I'd just add a flag to the class:
std::atomic<bool> time_to_stop;
The constructor of action should set that to false, and the cancel member function should set it to true. Then change the loop to look at that value:
for(; !time_to_stop && k < *ii; ++k)
You might, instead, make ii atomic. That would work, but it wouldn't be as clear as having a named member to look at.
First off there is no reason to make ii a pointer. You can have it just as a plain uint64_t.
Secondly if you have multiple threads and at least one of them writes to a shared variable then you are going to have to have some sort of synchronization. In this case you could just use std::atomic<uint64_t> to get that synchronization. Otherwise you would have to use a mutex or some sort of memory fence.