线程间同步

线程间同步

本文重要侧重于介绍线程同步相关类的说明及使用。

  • 互斥锁
    先看看锁的描述:

      Mutual exclusion
      Mutual exclusion algorithms prevent multiple threads from simultaneously accessing shared resources. This prevents data races and provides support for synchronization between threads.Defined in header <mutex>
      
      mutex (C++11):                 provides basic mutual exclusion facility
      timed_mutex(C++11):            provides mutual exclusion facility which implements locking with a timeout
      recursive_mutex(C++11):        provides mutual exclusion facility which can be locked recursively by the same thread 
      recursive_timed_mutex(C++11):  provides mutual exclusion facility which can be locked recursively by the same thread and implements locking with a timeout,Defined in header <shared_mutex>
      shared_mutex(C++17):           provides shared mutual exclusion facility 
      shared_timed_mutex(C++14):     provides shared mutual exclusion facility and implements locking with a timeout
    

1、std::mutex:

The mutex class is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads.

mutex offers exclusive, non-recursive ownership semantics:
A calling thread owns a mutex from the time that it successfully calls either lock or try_lock until it calls unlock.
When a thread owns a mutex, all other threads will block (for calls to lock) or receive a false return value (for try_lock) if they attempt to claim ownership of the mutex.
A calling thread must not own the mutex prior to calling lock or try_lock.
The behavior of a program is undefined if a mutex is destroyed while still owned by any threads, or a thread terminates while owning a mutex. The mutex class satisfies all requirements of Mutex and StandardLayoutType.
std::mutex is neither copyable nor movable.

Notes
std::mutex is usually not accessed directly: std::unique_lock, std::lock_guard, or std::scoped_lock (since C++17) manage locking in a more exception-safe manner.

上面的英文已经写的很清楚了,下面大概翻译一下:
A、std::mutex 是一种同步原语,可被用于保护共享数据,当多线程同时访问时;
B、std::mutex 提供了 互斥,非递归语义;
C、std::mutex 不支持拷贝和移动语义;
D、通常不直接使用std::mutex,而是借助std::unique_lock,std::lock_guard,或者std::scoped_lock用一种更加异常安全的行为去管理锁。

看下面例子:


	std::map<std::string, std::string> g_pages; //shared data
	std::mutex g_pages_mutex; //protected mutex
	
	void save_page(const std::string &url)
	{
		// simulate a long page fetch
		std::this_thread::sleep_for(std::chrono::seconds(2));
		std::string result = "fake content";
		//use lock_guard to manage mutex resource automatically through RAII implementation,as similar to unique_lock
		std::lock_guard<std::mutex> guard(g_pages_mutex);
		g_pages[url] = result;
	}
	
	int main()
	{
		std::thread t1(save_page, "http://foo");
		std::thread t2(save_page, "http://bar");
		t1.join();
		t2.join();
	
		// safe to access g_pages without lock now, as the threads are joined
		for (const auto &pair : g_pages) {
			std::cout << pair.first << " => " << pair.second << '\n';
		}
	}

2、std::timed_mutex

provides mutual exclusion facility which implements locking with a timeout.
The timed_mutex class is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads.
In a manner similar to mutex, timed_mutex offers exclusive, non-recursive ownership semantics. In addition, timed_mutex provides the ability to attempt to claim ownership of a timed_mutex with a timeout via the try_lock_for() and try_lock_until() methods.
The timed_mutex class satisfies all requirements of TimedMutex and StandardLayoutType.

和std::mutex相似,只是提供了超时机制,增加了两个超时相关的接口,try_lock_for和try_lock_until。其实这个类是借助std::mutex封装的,代码写的很有启发性。应用和std::mutex基本一样,这里就不给示例了,主要看看这个类的封装。


	class timed_mutex
		{	// class for timed mutual exclusion
	public:
		timed_mutex() noexcept
			: _My_locked(0)
			{	// default construct
			}
	
		timed_mutex(const timed_mutex&) = delete;
		timed_mutex& operator=(const timed_mutex&) = delete;
	
		void lock()
			{	// lock the mutex
			unique_lock<mutex> _Lock(_My_mutex);
			while (_My_locked != 0)
				_My_cond.wait(_Lock);
			_My_locked = UINT_MAX;
			}
	
		_NODISCARD bool try_lock() noexcept
			{	// try to lock the mutex
			lock_guard<mutex> _Lock(_My_mutex);
			if (_My_locked != 0)
				return (false);
			else
				{
				_My_locked = UINT_MAX;
				return (true);
				}
			}
	
		void unlock()
			{	// unlock the mutex
				{
				// The lock here is necessary
				lock_guard<mutex> _Lock(_My_mutex);
				_My_locked = 0;
				}
			_My_cond.notify_one();
			}
	
		template<class _Rep,
			class _Period>
			_NODISCARD bool try_lock_for(const chrono::duration<_Rep, _Period>& _Rel_time)
			{	// try to lock for duration
			return (try_lock_until(chrono::steady_clock::now() + _Rel_time));
			}
	
		template<class _Time>
			bool _Try_lock_until(_Time _Abs_time)
			{	// try to lock the mutex with timeout
			unique_lock<mutex> _Lock(_My_mutex);
			if (!_My_cond.wait_until(_Lock, _Abs_time, _UInt_is_zero{_My_locked}))
				{
				return (false);
				}
	
			_My_locked = UINT_MAX;
			return (true);
			}
	
		template<class _Clock,
			class _Duration>
			_NODISCARD bool try_lock_until(const chrono::time_point<_Clock, _Duration>& _Abs_time)
			{	// try to lock the mutex with timeout
			return (_Try_lock_until(_Abs_time));
			}
	
		_NODISCARD bool try_lock_until(const xtime *_Abs_time)
			{	// try to lock the mutex with timeout
			return (_Try_lock_until(_Abs_time));
			}
	
	private:
		mutex _My_mutex; // 锁
		condition_variable _My_cond; //条件变量
		unsigned int _My_locked; //共享资源
		};

上面timed_lock的实现代码很简单,主要就是对std::mutex,和std::condition_variable的封装,通过改变共享变量_My_locked的值,来实现锁的获得和释放,据此逻辑,其实我们也可以完全实现自己的版本。

3、std::recursive_mutex,std::recursive_timed_mutex

和std::mutex的区别就是可递归,或者翻译成可重入,类似java里面的synchronized,  
但是有一点必须注意: 你重入了几次,也就是lock()了几次,就必须以相同的次数unlock(),  
可推想其里面维持一个锁的引用计数。其他方面都一样。  

	std::mutex mutex_1;
	std::recursive_mutex mutex_2;
	int num = 0;
	
	void f2(int& n)
	{
		std::lock_guard<std::recursive_mutex> lk(mutex_2);
		//std::lock_guard<std::mutex> lk(mutex_1);
		n += 2;
	}
	void f1(int& n)
	{
		std::lock_guard<std::recursive_mutex> lk(mutex_2);
		//std::lock_guard<std::mutex> lk(mutex_1);
		++n;
		f2(n);
	}
	
	
	int main()
	{
		std::thread thr(f1, std::ref(num));
		thr.join();
		std::cout << "num: " << num << std::endl;
	}

上面代码揭示了recursive的概念,若是用mutex_2,程序没有问题,num输出为3,可以看出,f1自己lock了一次,在没有释放锁的情况下,f2又lock一次,最后,依次为f2释放锁,f1释放锁。上锁和释放的次数相等,均为2。 若改为mutex_1,则存在死锁,因为std::mutex 是不可重入的。

4、std::shared_mutex 和 std::shared_timed_mutex

The shared_mutex class is a synchronization primitive that can be used to protect shared data from being simultaneously accessed by multiple threads. In contrast to other mutex types which facilitate exclusive access, a shared_mutex has two levels of access:  

shared - several threads can share ownership of the same mutex.
exclusive - only one thread can own the mutex.  

Shared mutexes are usually used in situations when multiple readers can access the same resource at the same time without causing data races, but only one writer can do so.
The shared_mutex class satisfies all requirements of SharedMutex and StandardLayoutType.  

std::shared_mutex主要提供了两种级别的互斥访问:
A、共享访问,接口有lock_shared,try_lock_shared,unlock_shared;
B、互斥访问,接口有lock,try_lock,unlock;
主要应用于有多个读线程同时访问共享资源,一个写线程的场景。
其实我理解这个std::shared_mutex就是Linux c中的读写锁,pthread_rwlock_t的概念。
看例子:


	class ThreadSafeCounter {
	public:
		ThreadSafeCounter() = default;
	
		// Multiple threads/readers can read the counter's value at the same time.
		unsigned int get() const {
			//注意这块是shared_lock,共享访问,允许多个读线程同时访问
			std::shared_lock<std::shared_mutex> lock(mutex_);
			return value_;
		}
	
		// Only one thread/writer can increment/write the counter's value.
		void increment() {
			//注意这块是unique_lock,互斥访问,只允许一个线程去访问
			std::unique_lock<std::shared_mutex> lock(mutex_);
			value_++;
		}
	
		// Only one thread/writer can reset/write the counter's value.
		void reset() {
			std::unique_lock<std::shared_mutex> lock(mutex_);
			value_ = 0;
		}
	
	private:
		mutable std::shared_mutex mutex_;
		unsigned int value_ = 0;
	};
	
	std::mutex io_mutex;
	
	int main() {
		ThreadSafeCounter counter;
	
		auto increment_and_print = [&counter]() {
			for (int i = 0; i < 3; i++) {
				counter.increment();
				std::lock_guard<std::mutex> lk(io_mutex);
				std::cout << std::this_thread::get_id() << ' ' << counter.get() << '\n';
			}
		};
	
		std::thread thread1(increment_and_print);
		std::thread thread2(increment_and_print);
	
		thread1.join();
		thread2.join();
	}

std::shared_timed_mutex不多说了,和std::shared_mutex差不多,无非额外提供了超时机制。

  • 锁管理
    这里涉及到一个RAII的概念,查维基百科有下面:
    Resource acquisition is initialization (RAII)[1] is a programming idiom[2] used in several object-oriented languages to describe a particular language behavior. In RAII, holding a resource is a class invariant, and is tied to object lifetime: resource allocation (or acquisition) is done during object creation (specifically initialization), by the constructor, while resource deallocation (release) is done during object destruction (specifically finalization), by the destructor. Thus the resource is guaranteed to be held between when initialization finishes and finalization starts (holding the resources is a class invariant), and to be held only when the object is alive. Thus if there are no object leaks, there are no resource leaks.

1、std::lock_guard
先理解上面关于RAII的解释,再看下面的源码,由于比较简单,这里只贴出std::lock_guard的源码:


	template<class _Mutex>
		class lock_guard
		{	// class with destructor that unlocks a mutex
	public:
		using mutex_type = _Mutex;
	
		explicit lock_guard(_Mutex& _Mtx)
			: _MyMutex(_Mtx)
			{	// construct and lock
			_MyMutex.lock();
			}
	
		lock_guard(_Mutex& _Mtx, adopt_lock_t)
			: _MyMutex(_Mtx)
			{	// construct but don't lock
			}
	
		~lock_guard() noexcept
			{	// unlock
			_MyMutex.unlock();
			}
	
		lock_guard(const lock_guard&) = delete;
		lock_guard& operator=(const lock_guard&) = delete;
	private:
		_Mutex& _MyMutex;
		};

其实前面的实例中已多次用到,这里在看一个实例:


	void write_to_file (const std::string & message) {
	    // mutex to protect file access (shared across threads)
		// Note the modifier here 'static'
	    static std::mutex mutex;
	
	    // lock mutex before accessing file
	    std::lock_guard<std::mutex> lock(mutex);
	
	    // try to open file
	    std::ofstream file("example.txt");
	    if (!file.is_open())
	        throw std::runtime_error("unable to open file");
	    
	    // write message to file
	    file << message << std::endl;
	    
	    // file will be closed 1st when leaving scope (regardless of exception)
	    // mutex will be unlocked 2nd (from lock destructor) when leaving
	    // scope (regardless of exception)
	}

2、std::unique_lock
std::unique_lock 除了和 std::lock_guard 提供了相同的RAII特性外,还提供了许多控制接口,如lock,unlock,try_lock等等,提供了更多的灵活性。根据不同场景灵活的选择使用哪个。
看代码:


	struct Box {
		explicit Box(int num) : num_things{ num } {}
	
		int num_things;
		std::mutex m;
	};
	
	void transfer(Box &from, Box &to, int num)
	{
		// don't actually take the locks yet
		std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
		std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
	
		// lock both unique_locks without deadlock
		std::lock(lock1, lock2);
	
		from.num_things -= num;
		to.num_things += num;
	
		// 'from.m' and 'to.m' mutexes unlocked in 'unique_lock' dtors
	}
	
	int main()
	{
		Box acc1(100);
		Box acc2(50);
	
		std::thread t1(transfer, std::ref(acc1), std::ref(acc2), 10);
		std::thread t2(transfer, std::ref(acc2), std::ref(acc1), 5);
	
		t1.join();
		t2.join();
	}

上面的实例,主要有两个知识点,std::unique_lockstd::mutex lock1(from.m, std::defer_lock)和std::lock(lock1, lock2),std::defer_lock的作用其实是,只引用锁资源,但不执行实际的lock动作,相应的还有std::adopt_lock_t,std::try_to_lock_t,具体含义,请看std::unique_lock的源码。std::lock()的作用是同时锁住好几个锁,其内部采用一种避免死锁的算法去同时锁住几把锁。有一点需要特别注意,std::lock()只管锁,不管释放,所以必须在scope结束后,释放锁,上面实例会在transfer结束时自动释放from.m和to.m。

其实上面示例之所以采用如下的书写方式,也正是考虑到std::lock只管锁不管释放的特性,


		// don't actually take the locks yet
		std::unique_lock<std::mutex> lock1(from.m, std::defer_lock);
		std::unique_lock<std::mutex> lock2(to.m, std::defer_lock);
	
		// lock both unique_locks without deadlock
		std::lock(lock1, lock2);

还有等价的写法:


		// lock both unique_locks without deadlock
		std::lock(lock1, lock2);
		// don't actually take the locks yet
		std::lock_guard<std::mutex> lock1(from.m, std::adopt_lock);
		std::lock_guard<std::mutex> lock2(to.m, std::adopt_lock);

看着是不是很别扭,难道std::lock()就不能像lock_guard或者unique_lock采用RAII机制在析构时自动释放所持有的锁? 可惜在C++11中还真没有这样的接口,至少std::lock()是办不到的,所以只能采用这种笨拙的写法。但是在C++17中出现了std::scoped_lock()满足了这个需求。

3、std::scoped_lock
deadlock-avoiding RAII wrapper for multiple mutexes
采用避免死锁算法同时锁住多个锁且实现了RAII
直接看源码:


	#if _HAS_CXX17
			// CLASS TEMPLATE scoped_lock
	template<class... _Mutexes>
		class scoped_lock
		{	// class with destructor that unlocks mutexes
	public:
		explicit scoped_lock(_Mutexes&... _Mtxes)
			: _MyMutexes(_Mtxes...)
			{	// construct and lock
			_STD lock(_Mtxes...);
			}
	
		explicit scoped_lock(adopt_lock_t, _Mutexes&... _Mtxes)
			: _MyMutexes(_Mtxes...)
			{	// construct but don't lock
			}
	
		~scoped_lock() noexcept
			{	// unlock all
			_For_each_tuple_element(
				_MyMutexes,
				[](auto& _Mutex) noexcept { _Mutex.unlock(); });
			}
	
		scoped_lock(const scoped_lock&) = delete;
		scoped_lock& operator=(const scoped_lock&) = delete;
	private:
		tuple<_Mutexes&...> _MyMutexes;
		};
	
	template<class _Mutex>
		class scoped_lock<_Mutex>
		{	// specialization for a single mutex
	public:
		typedef _Mutex mutex_type;
	
		explicit scoped_lock(_Mutex& _Mtx)
			: _MyMutex(_Mtx)
			{	// construct and lock
			_MyMutex.lock();
			}
	
		explicit scoped_lock(adopt_lock_t, _Mutex& _Mtx)
			: _MyMutex(_Mtx)
			{	// construct but don't lock
			}
	
		~scoped_lock() noexcept
			{	// unlock
			_MyMutex.unlock();
			}
	
		scoped_lock(const scoped_lock&) = delete;
		scoped_lock& operator=(const scoped_lock&) = delete;
	private:
		_Mutex& _MyMutex;
		};

可以看出在构造时,同时锁住许多锁,析构时,又依次释放。无需用户去考虑多把锁的顺序以及死锁问题。
实例代码:


	struct Employee {
		Employee(std::string id) : id(id) {}
		std::string id;
		std::vector<std::string> lunch_partners;
		std::mutex m;
		std::string output() const
		{
			std::string ret = "Employee " + id + " has lunch partners: ";
			for (const auto& partner : lunch_partners)
				ret += partner + " ";
			return ret;
		}
	};
	
	void send_mail(Employee &, Employee &)
	{
		// simulate a time-consuming messaging operation
		std::this_thread::sleep_for(std::chrono::seconds(1));
	}
	
	void assign_lunch_partner(Employee &e1, Employee &e2)
	{
		static std::mutex io_mutex;
		{
			std::lock_guard<std::mutex> lk(io_mutex);
			std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
		}
	
		{
			// use std::scoped_lock to acquire two locks without worrying about 
			// other calls to assign_lunch_partner deadlocking us
			// and it also provides a convenient RAII-style mechanism
	
			std::scoped_lock lock(e1.m, e2.m);
	
			// Equivalent code 1 (using std::lock and std::lock_guard)
			// std::lock(e1.m, e2.m);
			// std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
			// std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);
	
			// Equivalent code 2 (if unique_locks are needed, e.g. for condition variables)
			// std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
			// std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
			// std::lock(lk1, lk2);
			{
				std::lock_guard<std::mutex> lk(io_mutex);
				std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
			}
			e1.lunch_partners.push_back(e2.id);
			e2.lunch_partners.push_back(e1.id);
		}
	
		send_mail(e1, e2);
		send_mail(e2, e1);
	}
	
	int main()
	{
		Employee alice("alice"), bob("bob"), christina("christina"), dave("dave");
	
		// assign in parallel threads because mailing users about lunch assignments
		// takes a long time
		std::vector<std::thread> threads;
		threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
		threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(bob));
		threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(alice));
		threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));
	
		for (auto &thread : threads) thread.join();
		std::cout << alice.output() << '\n' << bob.output() << '\n'
			<< christina.output() << '\n' << dave.output() << '\n';
	}

上面的示例很有代表性,很好的揭示了std::scoped_lock的用法,希望读者能好好理解,不过想运行上面的代码,编译器必须得支持C++17的特性。

4、std::shared_lock
这个不多解释,上面讲std::shared_mutex已经讲到了,看当时的示例。可以说这个封装就是专门针对std::shared_mutex使用的。

  • 锁算法
    1、std::try_lock 和 std::lock
    其实std::lock在上面的示例中已多次用到。这里结合std::try_lock再讲讲。
    A、先看std::try_lock

template< class Lockable1, class Lockable2, class… LockableN>
int try_lock( Lockable1& lock1, Lockable2& lock2, LockableN&… lockn);

Tries to lock each of the given Lockable objects lock1, lock2, …, lockn by calling try_lock in order beginning with the first.
If a call to try_lock fails, no further call to try_lock is performed, unlock is called for any locked objects and a 0-based index of the object that failed to lock is returned.
If a call to try_lock results in an exception, unlock is called for any locked objects before rethrowing.
Parameters
lock1, lock2, … , lockn - the Lockable objects to lock
Return value
-1 on success, or 0-based index value of the object that failed to lock.

上面的英文已经解释的很清楚了,这里我就不翻译了。直接看示例:


	//该示例来源于cppreference.com,但是我觉得在此处不是很恰当
	int main()
	{
		int foo_count = 0;
		std::mutex foo_count_mutex;
		int bar_count = 0;
		std::mutex bar_count_mutex;
		int overall_count = 0;
		bool done = false;
		std::mutex done_mutex;
	
		auto increment = [](int &counter, std::mutex &m, const char *desc) {
			for (int i = 0; i < 10; ++i) {
				std::unique_lock<std::mutex> lock(m);
				++counter;
				std::cout << desc << ": " << counter << '\n';
				//此处展示了unique_lock和lock_guard的差异,lock_guard显然是没法办到的,灵活的使用。
				lock.unlock(); 
				std::this_thread::sleep_for(std::chrono::seconds(1));
			}
		};
	
		// 线程increment_foo和increment_bar分别用各自的锁去保护共享变量counter,
		//其实就等于没有同步,两个线程间没有关系。当然此示例的是为了说明std::try_lock的用法。
		std::thread increment_foo(increment, std::ref(foo_count),
			std::ref(foo_count_mutex), "foo");
		std::thread increment_bar(increment, std::ref(bar_count),
			std::ref(bar_count_mutex), "bar");
	
		std::thread update_overall([&]() {
			done_mutex.lock();
			while (!done) {
				done_mutex.unlock();
				int result = std::try_lock(foo_count_mutex, bar_count_mutex);
				if (result == -1) { //这里表示std::try\_lock获取成功
					overall_count += foo_count + bar_count;
					foo_count = 0;
					bar_count = 0;
					std::cout << "overall: " << overall_count << '\n';
					foo_count_mutex.unlock(); //std::try_lock也是只管锁不管释放
					bar_count_mutex.unlock(); //所以这里需要手动释放
				}
				std::this_thread::sleep_for(std::chrono::seconds(2));
				done_mutex.lock();
			}
			done_mutex.unlock();
		});
	
		increment_foo.join();
		increment_bar.join();
		done_mutex.lock();
		done = true;
		done_mutex.unlock();
		update_overall.join();
	
		std::cout << "Done processing\n"
			<< "foo: " << foo_count << '\n'
			<< "bar: " << bar_count << '\n'
			<< "overall: " << overall_count << '\n';
	}

和std::try_lock相比,std::lock没有返回值。std::try_lock不阻塞,根据返回值判断锁定成功与否,而std::lock会阻塞,如果某把锁一直占用,将会使std::lock一直阻塞住。从这点来看,我觉得还是尽量使用std::try_lock,好控制,不至于让线程长期阻塞。

5、std::once_flag 和 std::call_once
这两个通常一起使用,一个属性值和一个函数。其目的就是保证在多线程环境下,某个函数只被执行一次。类似Linux c中的pthread_once(pthread_once_t *initflag, void (*initfn)(void))。多余的不说了,直接看代码:


	std::once_flag flag1, flag2;
	 
	void simple_do_once()
	{
	    std::call_once(flag1, [](){ std::cout << "Simple example: called once\n"; });
	}
	 
	void may_throw_function(bool do_throw)
	{
	  if (do_throw) {
	    std::cout << "throw: call_once will retry\n"; // this may appear more than once
	    throw std::exception();
	  }
	  std::cout << "Didn't throw, call_once will not attempt again\n"; // guaranteed once
	}
	 
	void do_once(bool do_throw)
	{
	  try {
	    std::call_once(flag2, may_throw_function, do_throw);
	  }
	  catch (...) {
	  }
	}
	 
	int main()
	{
	    std::thread st1(simple_do_once);
	    std::thread st2(simple_do_once);
	    std::thread st3(simple_do_once);
	    std::thread st4(simple_do_once);
	    st1.join();
	    st2.join();
	    st3.join();
	    st4.join();
	 
	    std::thread t1(do_once, true);
	    std::thread t2(do_once, true);
	    std::thread t3(do_once, false);
	    std::thread t4(do_once, true);
	    t1.join();
	    t2.join();
	    t3.join();
	    t4.join();
	}

感兴趣的可以运行这个示例看看。最近通过C++11中线程相关知识点的学习,发现很多概念或者用法和java以及Linux c编程中的pthread库都很相似,有时间打算写一篇关于这方面的文章。今天就到这里,下次讲讲C++11 中 Future相关的知识点,这部分内容也就算基本上结束了。

参考文章:
https://en.cppreference.com/w/cpp/thread
https://en.wikipedia.org/wiki/Resource_acquisition_is_initialization

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值