Learning notes of C + + standard library - STL - concurrency - start thread

1, Advanced interfaces async() and Future

async() provides an interface to make a caller object a separate thread.
class future allows you to wait for the thread to end and get its results.

1. Instance

Consider the following:

func1() + func2()

In a single thread, the overall processing time is the time spent by func1() plus the time spent by func2(), plus the time spent calculating the sum.

If we try to implement such an operation in multithreading, we can limit the total time to the greater of the time spent by func1() and func2(), plus the time to calculate the sum:

#include <iostream>
#include <random>
#include <future>
#include <thread>
#include <chrono>
using namespace std;

int doSomething(char c)
{
	default_random_engine e(time(0));
	uniform_int_distribution u(10, 1000);

	for (int i = 0; i < 10; ++i)
	{
		this_thread::sleep_for(chrono::milliseconds(u(e)));
		cout.put(c).flush();
	}

	return u(e);
}

int func1()
{
	return doSomething('.');
}

int func2()
{
	return doSomething('+');
}

int main()
{
	future<int> result1(async(func1));

	int result2 = func2();

	int result = result2 + result1.get();

	cout << endl << "result is " << result << endl;
}


First, we use async() to try to run func1() in the background and assign the result to an std::future object. This call is asynchronous, which is equivalent to executing the method in a separate thread, so it will not block the main thread. It is necessary to return future objects for two reasons:

  • It allows us to get the return value (or exception) of the method executed in a separate thread
  • It ensures that the object passed to the objective function will be executed. Because the call of async() function does not mean that the target function will start to execute.

One of the following three things happens when we call the get() function:

  1. If func1() has started and ended in a separate thread, we can get its return value immediately
  2. If func1() is started but not finished, it will cause the main thread to stagnate until the function runs, and then we can get the running results
  3. If func1() has not been started, it will be forced to start like a synchronous call.

Calling async() does not guarantee that the incoming function will start and end. If a thread is available, it will indeed be started, but if not (in a single threaded environment, or the thread resources are full), the call will be postponed until you explicitly say that you need its result (when you call get()) or just want the target function to complete its task (call wait()).

Therefore, we can define the general practice of making the program faster: modify the program to benefit from parallel processing (if supported by the platform), but still operate in a single threaded environment. To achieve this goal, we need to:

  1. #include <future>
  2. Pass some parallel executable functions to std::async() as a callable object
  3. Assign the execution result to a future object.
  4. When you need the execution result of the started function or ensure that the function ends, you call the future::get() method.
    It should be noted that the above principles are only used when there is no data competition.

In addition, in order to optimize program performance, we must ensure that the execution results of the objective function are obtained only when it is most necessary. Otherwise, the following code may cause unexpected operation:

int result = func2()+ result1.get();

In C + +, the call order of the two functions (func2() and get()) in the above code is uncertain. Therefore, such a call may not achieve the effect of asynchronous execution at all.

For best execution, we should maximize the distance between async() and call get().

2. Launch strategy

The async() function provides a parameter to set the launch policy:

future<int> result1(async(launch::async, func1));

This will require the function to explicitly start the objective function asynchronously. If the asynchronous call cannot be implemented, the program will throw an std::system_error exception with error code resource_unavailable_try_again.

3. Handling exceptions

Exceptions in the target function will not be triggered, but will not be triggered until the get() function is called:

void func1()
{
	throw std::runtime_error("an error in func1");
}

int main()
{
	future<void> result1(async(func1));

	default_random_engine e(time(0));
	uniform_int_distribution u(10, 1000);
	for (int i = 0; i < 10; ++i)
	{
		this_thread::sleep_for(chrono::milliseconds(u(e)));
		cout << "sleep " << i << endl;
	}

	try
	{
		result1.get();
	}
	catch (exception& e)
	{
		cout << e.what() << endl;
	}
}

4. Waiting and polling

A future object calls get() once. After that, the object is in an invalid state, which can only be detected by the valid() method. Sometimes we don't want the program to stop waiting for the get () function, but we want to query its completion status regularly. We can use wait(), wait_for(),wait_until() method to implement related functions. They respectively mean waiting, waiting for a certain period of time and waiting to a certain point in time. We can complete the polling of status by means of a waiting period with a length of 0:

auto f = async(task);
if (f.wait_for(chrono::seconds(0)) != future_status::deferred)
{
	while (f.wait_for(chrono::seconds(0)) != future_status::ready)
	{
		this_thread::yield();
	}
}

Note that the if judgment of the outer layer cannot be omitted. We must confirm that the target task has started to execute before we can use polling to confirm whether its status is incomplete. In addition, if the thread running this loop for polling keeps occupying cpu, other threads may not be able to obtain cpu time slices. Therefore, we need to call the yield method in the loop to transfer control to the next thread.

5,shared_future

Sometimes we want a future object to share state among multiple threads. That is, we want the objective function to run only once, but multiple threads can get its results. This is similar to shared_ptr. C + + provides shared_future is used to realize this function:

void func1(string str)
{
	cout << str << endl;
}

int main()
{
	future<void> result(async(func1, "future"));
	shared_future<void> shared_result(async(func1, "shared_future"));

	try
	{
		shared_result.get();
		shared_result.get();
	}
	catch (exception& e)
	{
		cout << "shared_future exception:" << e.what() << endl;
	}

	try
	{
		result.get();
		result.get();
	}
	catch (exception& e)
	{
		cout << "future exception:" << e.what() << endl;
	}
}


By the same shared_ The object sharing state of the future instance copy. Pass shared between different threads_ When you create a future object, you should use value passing. The structure evolution is similar to that of shared_ptr.

2, Low level interfaces Thread and Promise

In addition to the high-level interfaces async() and future, the C99 standard library also provides a low-level interface for starting and processing threads.

1. thread class

(1) Different from async()

  • Thread class does not support setting emission policy. That is, the target task will always be started in a new thread.
  • No interface can be used to get the result of the objective function.
  • If an exception occurs and the exception is not caught in the thread, the program will immediately terminate and call std::terminate().
  • We need to explicitly declare whether we want to wait for the thread to end (join()) or let it run in the background (detach()). If you do not do so before the end of the thread, or the thread object is moved and assigned, it will result in the call of std::terminate().
  • If you let the thread run in the background and main() ends, all threads will be recklessly and rigidly terminated.

(2) Basic use

int func(int num)
{
	default_random_engine e(time(0));
	uniform_int_distribution u(200, 1000);

	for (int i = 0; i < 10; ++i)
	{
		this_thread::sleep_for(chrono::milliseconds(u(e)));
		cout << num;
		cout.flush();
	}

	return u(e);
}

int main()
{
	try
	{
		thread t1(func, 10000);
		cout << "start foreground thread " << t1.get_id() << endl;;

		for (int i = 0; i < 5; ++i)
		{
			thread t(func, i);
			cout << "start background thread " << t.get_id() << endl;
			t.detach();
		}

		cin.get();

		cout << "join foreground thread " << t1.get_id() << endl;;
		t1.join();
	}
	catch(...)
	{
		cout << "exception " << endl;
	}
}


In the above code, whether the call to detach() or join() is commented out, the program will crash.

(3) Thread id

We use the class thread::id to describe the thread id. Each thread has a unique id object. We can use the default constructor of this class to indicate no thread.

Reasonable operations on thread IDS include comparison and output. We cannot assume that the thread ID of a thread (such as no thread or main thread) is a special value. In fact, the generation of thread ID may be the first time get is called_ Generated dynamically only when id():

thread t1(func, 10000);
thread t2(func, 10000);
thread t3(func, 10000);

cout << "t3 thread " << t3.get_id() << endl;
cout << "main thread " << this_thread::get_id() << endl;



Therefore, the only way to identify the main thread is to save its id in the main thread and make comparison and judgment when necessary.

2,Promise

So how do we get the results of the thread? Using reference parameters is an option. In addition, the class promise is provided in C + + to realize this requirement. This class is very similar to future. Both can temporarily hold a shared state (result or exception). However, the future object allows you to retrieve data, while the promise object allows you to provide data.

#include <iostream>
#include <random>
#include <future>
#include <thread>
#include <chrono>
using namespace std;

int doSomething(promise<string>& p)
{
	try
	{
		string result;
		while (true)
		{
			char ch = cin.get();
			if (ch > 'z' || ch < 'A')
			{
				throw runtime_error("invalid character");
			}

			if (ch == 'q')
			{
				p.set_value(result);
				break;
			}

			result += ch;
		}
	}
	catch (...)
	{
		p.set_exception(current_exception());
	}
}

int main()
{
	try
	{
		promise<string> p;
		thread t(doSomething, ref(p));
		t.detach();

		auto f = p.get_future();
		string s = f.get();

		cout << "result: " << s << endl;
	}
	catch (const exception& e)
	{
		cout << "exception " << e.what() << endl;
	}
	catch (...)
	{
		cout << "exception " << endl;
	}
}



It should be noted here that if we want to ensure that the thread has finished running when we get the result, we need to call set_exception_at_thread_exit and set_value_at_thread_exit. Otherwise, even if the thread returns a result or throws an exception, it may still be running.

Similar to future::get(), promise::get_future() can only be called once.

3,packaged_task

This class is similar to async(). However, this class does not require the objective function to be executed immediately, but by calling the overloaded operator():

int main()
{
	packaged_task<int(void)> pt(doSomething);
	auto f = pt.get_future();
	pt();
	f.get();
}

We can call the make provided by it_ ready_ at_ thread_ The exit method ensures that the state is returned after the thread ends.

3, Future and shared_future

Future and shared_ The return value types of future are different:


For template parameters of non reference type, future will return the moving copy object instead of shared_future will return const reference; For template parameters of reference type, both return reference type. This tells us that in a multithreaded environment, we need to pay special attention to shared_ Use of future:: get(). On the one hand, we need to ensure that the returned reference has a shorter lifetime than shared_ The shared state saved by the future object; On the other hand, shared is handled in different threads_ Different copies of future may cause data competition:

#include <iostream>
#include <random>
#include <future>
#include <thread>
#include <chrono>
using namespace std;

class MyException
{
public:
	MyException(int num):
		num(num)
	{}

	int num;
};

int doSomething()
{
	throw new MyException(2);
}

void addAndPrint(shared_future<int> sf)
{
	try
	{
		sf.get();
	}
	catch (MyException* e)
	{
		e->num++;
		this_thread::sleep_for(chrono::seconds(1));
		cout << e->num << endl;
	}
}

int main()
{
	shared_future<int> sf(async(doSomething));

	auto f1 = async(addAndPrint, sf);
	auto f2 = async(addAndPrint, sf);

	f1.get();
	f2.get();
}


Originally, I tried to use quoted exceptions and found that they would not cause data competition as mentioned in the book.

Tags: C++ Back-end Multithreading Concurrent Programming

Posted on Fri, 03 Dec 2021 15:57:31 -0500 by woza_uk