在一个成员变量中存储`std::future`并反复覆盖这个成员变量是否安全?

huangapple go评论125阅读模式
英文:

Is it safe to store std::future in a member variable and overwrite this member variable repeatedly?

问题

我正在尝试使用std::async来在单独的线程中运行启动和停止一些外部设备的耗时任务,以便在执行此操作时保持GUI的响应性。

我已经了解到,从std::async返回的std::future在超出作用域时会等待异步任务完成。由于我不想等待这个(那会阻塞GUI线程),所以我将future存储在成员变量中。

但是当未等待future并且成员变量被重复覆盖时会发生什么?

以下代码是否安全?

class StartStop {
public:
	enum State {STARTING, STARTED, STOPPING, STOPPED};
	StartStop() {
		m_legalStateTransitions[(int)State::STARTING] = (int)State::STARTED;
		m_legalStateTransitions[(int)State::STARTED] = (int)State::STOPPING;
		m_legalStateTransitions[(int)State::STOPPING] = (int)State::STOPPED;
		m_legalStateTransitions[(int)State::STOPPED] = (int)State::STARTING;
	}
	bool startInSeparateThread() {
		if (!changeState(State::STARTING)) {
			return false;
		}
		m_startFuture = std::async(std::launch::async, [this]() {
			// 进行启动,需要几秒钟
			changeState(State::STARTED);
		});
		return true;
	}
	bool stopInSeparateThread() {
		if (!changeState(State::STOPPING)) {
			return false;
		}
		m_stopFuture = std::async(std::launch::async, [this]() {
			// 进行停止,需要几秒钟
			changeState(State::STOPPED);
		});
		return true;
	}
private:
	bool changeState(State state) {
		std::lock_guard<std::mutex> guard(m_stateMutex);
		if (m_legalStateTransitions[(int)m_state] == state) {
			m_state = state;
			return true;
		}
		return false;
	}
	std::array<int, 4> m_legalStateTransitions;
	State m_state  = State::STOPPED;
	std::mutex         m_stateMutex;
	std::future<void> m_startFuture;
	std::future<void>  m_stopFuture;
};

startInSeparateThread()stopInSeparateThread()将在用户按下启动或停止按钮时从GUI线程中重复调用。

英文:

I am trying to use std::async to run the time consuming task of starting and stopping some external equipment in a separate thread so that the GUI stays responsive whilst doing this.

I have learned that when the std::future returned from std::async goes out of scope it waits on the async task. Since I do not want to wait for this (that would block the GUI thread) I store the future in a member variable.

But what happens when the future is never waited on and the member variable is repeatedly overwritten?

Is the following code safe?

class StartStop {
public:
	enum State {STARTING, STARTED, STOPPING, STOPPED};
	StartStop() {
		m_legalStateTransitions[(int)State::STARTING] = (int)State::STARTED;
		m_legalStateTransitions[(int)State::STARTED] = (int)State::STOPPING;
		m_legalStateTransitions[(int)State::STOPPING] = (int)State::STOPPED;
		m_legalStateTransitions[(int)State::STOPPED] = (int)State::STARTING;
	}
	bool startInSeparateThread() {
		if (!changeState(State::STARTING)) {
			return false;
		}
		m_startFuture = std::async(std::launch::async, [this]() {
			// do the starting, takes several seconds
			changeState(State::STARTED);
		});
		return true;
	}
	bool stopInSeparateThread() {
		if (!changeState(State::STOPPING)) {
			return false;
		}
		m_stopFuture = std::async(std::launch::async, [this]() {
			// do the stopping, takes several seconds
			changeState(State::STOPPED);
		});
        return true; 
	}
private:
	bool changeState(State state) {
		std::lock_guard&lt;std::mutex&gt; guard(m_stateMutex);
		if (m_legalStateTransitions[(int)m_state] == state) {
			m_state = state;
			return true;
		}
		return false;
	}
	std::array&lt;int, 4&gt; m_legalStateTransitions;
	State m_state  = State::STOPPED;
	std::mutex         m_stateMutex;
	std::future&lt;void&gt; m_startFuture;
	std::future&lt;void&gt;  m_stopFuture;
};

startInSeparateThread() and stopInSeparateThread() will be called repeatedly from the GUI thread when the user presses a start or stop button.

答案1

得分: 1

No, this will not work. The move assignment will still wait. Here is a quick test case:

#include <iostream>
#include <chrono>
#include <future>
#include <thread>


int main()
{
  std::future<void> fut = std::async([]() {
    std::cout << "future 1 launched\n";
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout << "future 1 ended\n";
  });
  fut = std::async([]() {
    std::cout << "future 2 launched\n";
    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::cout << "future 2 ended\n";
  });
  std::cout << "second future assigned\n";
  fut.wait();
}

The output is

future 1 launched
future 2 launched
future 1 ended
second future assigned
future 2 ended

This indicates that the second thread is immediately launched but the assignment to the future object blocks.

What you want to do instead:

  1. Keep a list of futures (or a set, however you want to build it)
  2. Let the future notify the GUI thread when it is done
  3. Let the GUI thread clean up finished futures, which also gives you a convenient place to deal with exceptions

In general, I would advise using the threading mechanism provided by your GUI framework instead of std::future. For example, Qt's QFuture comes with a QFutureWatcher that can handle this directly.

Another wrinkle in your original design: Would you really want to have two start threads run in parallel if the state transition is triggered twice? Wouldn't you want to block transitions until the current background operation is done?

Therefore, maybe a setup like this works better:

  • Keep 3 state variables: current state, desired state, transition in progress
  • User actions change the desired state
  • If the desired state changes and no transition is in progress, a background action is started to change the state
  • When the background action finishes, it changes the current state
  • If the desired state has changed while this took place, the GUI thread immediately launches a new background action
英文:

No, this will not work. The move assignment will still wait. Here is a quick test case:

#include &lt;iostream&gt;
#include &lt;chrono&gt;
#include &lt;future&gt;
#include &lt;thread&gt;


int main()
{
  std::future&lt;void&gt; fut = std::async([]() {
    std::cout &lt;&lt; &quot;future 1 launched\n&quot;;
    std::this_thread::sleep_for(std::chrono::seconds(2));
    std::cout &lt;&lt; &quot;future 1 ended\n&quot;;
  });
  fut = std::async([]() {
    std::cout &lt;&lt; &quot;future 2 launched\n&quot;;
    std::this_thread::sleep_for(std::chrono::seconds(3));
    std::cout &lt;&lt; &quot;future 2 ended\n&quot;;
  });
  std::cout &lt;&lt; &quot;second future assigned\n&quot;;
  fut.wait();
}

The output is

future 1 launched
future 2 launched
future 1 ended
second future assigned
future 2 ended

This indicates that the second thread is immediately launched but the assignment to the future object blocks.

What you want to do instead:

  1. Keep a list of futures (or a set, however you want to build it)
  2. Let the future notify the GUI thread when it is done
  3. Let the GUI thread clean up finished futures, which also gives you a convenient place to deal with exceptions

In general I would advise to use the threading mechanism provided by your GUI framework instead of std::future. For example Qt's QFuture comes with a QFutureWatcher that can handle this directly.

Another wrinkle in your original design: Would you really want to have two start threads run in parallel if the state transition is triggered twice? Wouldn't you want to block transitions until the current background operation is done?

Therefore maybe a setup like this works better:

  • Keep 3 state variables: current state, desired state, transition in progress
  • User actions change the desired state
  • If the desired state changes and no transition is in progress, a background action is started to change the state
  • When the background action finishes, it changes the current state
  • If the desired state has changed while this took place, the GUI thread immediately launches a new background action

huangapple
  • 本文由 发表于 2023年5月25日 15:48:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/76330003.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定