C++并发编程(九)进阶线程管理

C++多线程并发(一)--- 线程创建与管理 一、何为并发 简单来说,并发指的是两个或多个独立的活动在同一时段内交替发生。与并发相近的另一个概念是并行,并行则强调的是多个独立的活动在同一时刻点同时发生。 二、为什么使用并发 在应用程序中使用并发的原因主要有两个:关注点分离和性能。事实上,甚至可以说它们差不多是使用并发的唯一原因;当你观察的足够仔细时,一切其他因素都可以归结到这两者之一(或者可能是二者兼有)。 阅读详情

本章主要关于线程和任务的管控机制,起步内容是自动控制线程数目,以及线程间自动划分任务。

目录

1.线程池

1.1简易线程池

1.2支持等待任务的线程池

1.3支持任务相互等待的线程池

1.4避免任务队列上的争夺

1.5任务窃取

2.中断线程

2.1可中断的线程

2.2检测线程是否被中断

2.3中断条件变量上的等待

2.4中断std::condition_variable_any上的等待

2.5中断其他类型的阻塞

2.6处理中断

2.7在应用程序退出时中断后台任务 

小结


1.线程池

在大多数系统上,若仅因为某任务可以与其他任务并行处理,就分别给它们配备单独的专属线程,这种做法不切实际,但我们依然想充分利用可调配的并发算力。线程池可以实现我们的目的:将可同时执行的任务提交到线程池,并放入任务队列中等待,队列中的任务分别由某一线程领取执行,执行完成后该线程再从队列中取出另一任务,如此反复循环。

线程池的构建涉及几个关键要素:线程数量、分配任务的方法、等待任务完成与否。

1.1简易线程池

最简单的实现形式是使用固定数目的工作线程(往往是std::hardware_concurrency())。每当有任务要处理,就调用某个函数,将它放入任务队列等待,工作线程从队列领取指定任务执行。简易的线程池无法等待任务。

class thread_pool
{
	std::atomic_bool done;
	threadsafe_queue<std::function<void()>> work_queue;
	std::vector<std::thread> threads;
	join_threads joiner;

	void worker_thread()
	{
		while (!done)//只要线程池没有析构或抛出异常,就一直尝试从队列取出任务执行
		{
			std::function<void()> task;
			if (work_queue.try_pop(task))
			{
				task();
			}
			else
			{
				std::this_thread::yield();//没有任务则稍事休息
			}
		}
	}
public:
	thread_pool() :done(false), joiner(threads)
	{
		const unsigned thread_count = std::thread::hardware_concurrency();
		try
		{
			for (unsigned i = 0; i < thread_count; ++i)
			{
				threads.push_back(std::thread(&thread_pool::worker_thread, this));//每个线程从队列中取出任务
			}
		}
		catch (...)
		{
			done = true;
			throw;
		}
	}
	~thread_pool()
	{
		done = true;
	}

	//提交到任务队列的接口
	template<typename FunctionType>
	void submit(FunctionType f)
	{
		work_queue.push(std::function<void()>(f));
	}
};

本例假设装入线程池的任务没有任何返回值,故将他封装为std::function<void()>实例,向submit()传入函数或可调用对象,都会包装在一个std::function<void()>实例中。

注意声明数据成员的顺序,done标志、work_queue、threads、joiner,为了确保数据成员按正确的顺序销毁:线程全部终结,任务列表才销毁。

上述线程池适合下列特点的任务:彼此完全独立、没有任何返回值、不执行阻塞操作。对于简单的问题,std::async()可能是更好的方式。

1.2支持等待任务的线程池

如果主线程需要获取某任务的结果,就必须等待生成的线程完成任务返回。运用future可将等待和传递结果两个操作合二为一。
由于std::packaged_task<>实例仅能移动不能复制,但std::function()要求本身所含的函数对象能进行拷贝构造,因此需要定制自己的类作为代替,包装任务以处理只移型别。

class function_wrapper
{
	struct impl_base {
		virtual void call() = 0;
		virtual ~impl_base() {}
	};
	std::unique_ptr<impl_base> impl;

	template<typename F>
	struct impl_type :impl_base
	{
		F f;
		impl_type(F&& f_) :f(std::move(f_)) {}//传入右值引用
		void call() { f(); }
	};
public:
	template<typename F>
	function_wrapper(F&& f):
		impl(new impl_type<F>(std::move(f)))
	{}
	//函数调用操作符
	void operator()() { impl->call(); }
	function_wrapper() = default;
	//移动构造函数(右值引用)
	function_wrapper(function_wrapper&& other) :
		impl(std::move(other.impl))
	{}
	//赋值
	function_wrapper& operator = (function_wrapper&& other)
	{
		impl = std::move(other.impl);
		return *this;
	}
	//取消拷贝语义
	function_wrapper(const function_wrapper&) = delete;
	function_wrapper(function_wrapper&) = delete;
	function_wrapper& operator=(const function_wrapper&) = delete;
};
class thread_pool
{
	std::atomic_bool done;
	thread_safe_queue<function_wrapper> work_queue;
	std::vector<std::thread> threads;
	join_threads joiner;
	void worker_thread()
	{
		while (!done)
		{
			function_wrapper task;
			if (work_queue.try_pop(task))
			{
				task();//调用call,进而调用函数
			}
			else
			{
				std::this_thread::yield();
			}
		}
	}
public:
	template<typename FunctionType>
	std::future<typename std::result_of<FunctionType()>::type> submit(FunctionType f)//预先确定返回类型
	{
		typedef typename std::result_of<FunctionType()> ::type result_type;//定义结果类型
		std::packaged_task<result_type()> task(std::move(f));//包装任务
		std::future<result_type> res(task.get_future());//取得对应future
		work_queue.push(std::move(task));//packaged_task不可复制一定要使用std::move()
		return res;
	}

	thread_pool() :done(false), joiner(threads)
	{
		const unsigned thread_count = std::thread::hardware_concurrency();
		try
		{
			for (unsigned i = 0; i < thread_count; ++i)
			{
				threads.push_back(std::thread(&thread_pool::worker_thread, this));//每个线程从队列中取出任务
			}
		}
		catch (...)
		{
			done = true;
			throw;
		}
	}
	~thread_pool()
	{
		done = true;
	}

};

任务队列中存储function_wrapper而非std::function<>。

我们可以设置数据块,把处理每个数据块的任务放入线程池,数据块体积设置为值得并发处理的最小尺寸,以发挥线程池的伸缩性。但任务规模不能过小,因为std::future<>返回结果附带固有开销,过小的任务导致频繁的等待会得不偿失。

线程池还顾及了异常安全,如果任务抛出异常,会通过submit()返回的future向外传播,线程池的析构函数丢弃尚未完成的任务,等待池内线程自行结束。

1.3支持任务相互等待的线程池

快速排序算法:从元素中选出一个基准元素,整体数据分成两部分,小于基准元素的排到它前面,大于基准元素的排到它后面,两部分按照上述规则递归排序,最终拼接为有序序列。

回顾第8章,我们使用栈容器管理待排序的数据块,各线程进行局部排序,操作过程中再将数据分成两部分,一部分由当前线程排序,另一部分调用先压入栈,等有空余线程再对栈内数据块排序,若当前线程已经处理好自己那部分的数据块,则会协助另一线程(从栈内取出数据块排序)。

为了避免空闲线程耗尽令线程停滞不前,我们参考第8章的方法:在等待数据块完成操作的过程中,主动处理相关还未排序的数据块。

在线程池上增加一个新函数,负责运行队列中的任务,自行管控“领取任务并执行”的循环,取代原本的线程饱和循环:

void thread_pool::run_pending_task()
{
	function_wrapper task;
	if (work_queue.try_pop(task))
	{
		task();
	}
	else
	{
		std::this_thread::yield();
	}
}

基于线程池的快速排序: 

template<typename T>
struct sorter
{
	thread_pool pool;
	std::list<T> do_sort(std::list<T>& chunk_data)
	{
		if (chunk_data.empty())
		{
			return chunk_data;
		}

		std::list<T> res;
		res.splice(res.begin(), chunk_data, chunk_data.begin());
		const T& partition = *res.begin();
		std::list<T>::iterator divide_point = std::partition(chunk_data.begin(), chunk_data.end(),
			[&](const& T val) {return val < partition; });
		//创建小于部分的链表
		std::list<T> new_lower_chunk;
		new_lower_chunk.splice(new_lower.end(),
			chunk_data,
			chunk_data.begin(), divide_point);
        //std::bind绑定成员函数时,第一个参数是对象的成员函数,第二个参数需指名对象的地址
		std::future<std::list<T>> new_lower = pool.submit(std::bind(&sorter::do_sort, this, std::move(new_lower_chunk)));

		//此部分由线程池管理--------
		//chunks.push(std::move(new_lower_chunk));//新划分出来的数据压入栈
		//if (threads.size() < max_thread_count)//存在空闲线程时才分配新线程
		//{
		//	threads.push_back(std::thread(&sorter<T>::sort_thread, this));//递归调用排序
		//}
        //-------------------------
		//创建大于部分的链表
		std::list<T> new_higher(do_sort(chunk_data));//递归调用排序
		res.splice(res.end(), new_higher);
		while (new_lower.wait_for(std::chrono::second(0)) == std::future_status::timeout)//有其它线程未就绪
		{
			pool.run_pending_task();//从线程池取出任务
		}
		res.splice(res.begin(), new_lower.get());
		return res;
	}
};
template<typename T>
std::list<T> parallel_quick_sort(std::list<T> input)
{
	if (input.empty())
	{
		return input;
	}
	sorter<T> s;
	return s.do_sort(input);
}

尽管上述实现解决了任务相互等待导致死锁,但线程池还不理想。每个submit()和每个run_pending_task()的调用都访问相同的任务队列。若同一组数据由多个线程并发改动,会对性能造成不利影响。

1.4避免任务队列上的争夺

线程池具备一个任务队列供多个线程共用,每当有线程调用submit(),就把新任务压入该队列,为了执行任务,不断从这一队列弹出任务,队列上的争夺行为随着处理器数目的增多而加剧。

针对此问题,我们可以为每个池内线程配备局部的任务队列,只有当局部任务队列中没有任务时,再从全局队列中取出任务:

class thread_pool
{
	std::atomic_bool done;
	threadsafe_queue<function_wrapper> pool_work_queue;
	typedef std::queue<function_wrapper> local_queue_type;
	std::vector<std::thread> threads;
	join_threads joiner;
	//关键字对程序内所有线程均起作用,池外线程队列指针为空
	static thread_local std::unique_ptr<local_queue_type> local_work_queue;
	void worker_thread()//初始化局部队列
	{
		local_work_queue.reset(new local_queue_type);
		while (!done)
		{
			run_pending_task();
		}
	}
public:
	thread_pool() :done(false), joiner(threads)
	{
		const unsigned thread_count = std::thread::hardware_concurrency();
		try
		{
			for (unsigned i = 0; i < thread_count; ++i)
			{
				threads.push_back(std::thread(&thread_pool::worker_thread, this));//每个线程从队列中取出任务
			}
		}
		catch (...)
		{
			done = true;
			throw;
		}
	}
	~thread_pool()
	{
		done = true;
	}

	template<typename FunctionType>
	std::future<typename std::result_of<FunctionType()>::type> submit(FunctionType f)
	{
		typedef typename std::result_of<FunctionType()>::type result_type;
		std::packaged_task<result_type()> task(f);
		std::future<result_type> res(task.get_future());
		if (local_work_queue)//判别当前线程是否为池内线程
		{
			local_work_queue->push(std::move(task));
		}
		else
		{
			pool_work_queue.push(std::move(task));
		}
		return res;
	}
	void run_pending_task()
	{
		function_wrapper task;
		if (local_work_queue && !local_work_queue->empty())//判断是否是局部线程,且是否含任务
		{
			task = std::move(local_work_queue->front());
			local_work_queue->pop();
			task();
		}
		else if(pool_work_queue->try_pop(task))//从全局队列中取出任务
		{
			task();
		}
		else
		{
			std::this_thread::yield();
		}
	}
};

线程局部变量thread_local

若将变量声明为线程局部变量,则每个线程上都会存在其独立的实例。
三种数据能声明为线程局部变量:
以名字空间为作用域、类的静态数据成员、普通局部变量。
⊙线程局部变量在给定翻译单元内(当前代码所在源文件,及预处理后全部有效包含的头文件或其它源文件)动态加载,若局部变量未被指涉,则无法保证它们已被构造出来。
⊙与静态变量一样,它们先进行零值初始化,再进行动态初始化(零值初始化和常量初始化以外的方式)。若其构造/析构函数抛出异常,则程序会调用std::terminate()。

⊙线程局部变量的地址因不同线程而异,若线程通过std::exit()或main()(先获取main()返回值再调用std::exit())自然退出,则局部变量随之销毁。若应用程序退出,线程仍在运行,则线程上的局部变量不会发生析构。

局部线程队列由worker_thread初始化(worker_thread由构造函数调用),初始化过程会为池内线程创建thread_local声明的任务队列,再进入任务处理循环。

submit()判别当前线程是否具备任务队列,若具备则为池内线程,把任务压入;否则,将任务加入全局队列。

run_pending_task()通过判断,优先选择从局部线程取出任务,再从全局取出,或等待。

上述方法减少了争夺,但是容易导致分配不均,以快速排序为例,可能只将顶层的数据块任务放入全局队列,而剩下的数据块全都加到自身的局部队列中(递归调用)。

1.5任务窃取

假设某线程运行run_pending_task(),自身队列却没有任务,而另一线程队列则是满的,前者可以通过某种方式窃取后者的任务,以达到分配均匀的目的。

为了简单阐明任务窃取思想,采用互斥保护队列数据,以下是基于锁的队列的简单实现:

class work_stealing_queue
{
private:
	typedef function_wrapper data_type;
	std::queue<data_type> the_queue;
	mutable std::mutex the_mutex;
public:
	work_stealing_queue() {}
	work_stealing_queue(const work_stealing_queue& other) = delete;
	work_stealing_queue& operator=(const work_stealing_queue& other) = delete;
	void push(data_type data)
	{
		std::lock_guard<std::mutex> lk(the_mutex);
		the_queue.push_front(data);
	}
	bool empty()
	{
		std::lock_guard<std::mutex> lk(the_mutex);
		return the_queue.empty();
	}
	bool try_pop(data_type& data)
	{
		std::lock_guard<std::mutex> lk(the_mutex);
		data = std::move(the_queue.front());
		the_queue.pop_front();
		return true;
	}
	bool try_steal(data_type& data)
	{
		std::lock_guard<std::mutex> lk(the_mutex);
		if (the_queue.empty())
		{
			return false;
		}
		data = std::move(the_queue.back());
		the_queue.pop_back();
		return true;
	}
};

该队列中,push()和try_pop()都操作队列的前端,而try_steal()则操作队列的末端,实际上是一个先进先出的“队列”,从缓存角度来看,有助于线程的改进,因为队列优先处理最近压入的任务,而最近的任务数据更有可能还留在缓存中。try_steal()则从另一端获取任务,最大限度地避免数据争夺。

把该队列应用在线程池中:

class thread_pool
{
	typedef function_wrapper task_type;
	std::atomic_bool done;
	threadsafe_queue<function_wrapper> pool_work_queue;
	//局部任务队列组
	std::vector<std::unique_ptr<work_stealing_queue>> queues;
	std::vector<std::thread> threads;
	join_threads joiner;
	static thread_local work_stealing_queue* local_work_queue;
	static thread_local unsigned m_index;

	void worker_thread(unsigned index_)//初始化局部队列
	{
		m_index = index_;
		local_work_queue = queues[m_index].get();
		while (!done)
		{
			run_pending_task();
		}
	}
	bool pop_from_local_queue(task_type& task)
	{
		return local_work_queue && local_work_queue->try_pop(task);
	}
	bool pop_from_pool_queue(task_type& task)
	{
		return pool_work_queue.try_pop(task);
	}
	//从任务队列组向其它任务队列窃取任务
	bool pop_from_other_queue(task_type& task)
	{
		for (unsigned i = 1; i < queues.size(); ++i)
		{
			const unsigned index = (index + i) % queues.size();
			if (queues[index]->try_steal(task))
			{
				return true;
			}
		}
		return false;
	}
public:
	thread_pool() :done(false), joiner(threads)
	{
		const unsigned thread_count = std::thread::hardware_concurrency();
		try
		{
			//先初始化局部任务队列组,因为初始化任务队列需要用到队列组的索引
			for (unsigned i = 0; i < thread_count; ++i)
			{
				queues.push_back(std::unique_ptr<work_stealing_queue>(new work_stealing_queue()));
			}
			for (unsigned i = 0; i < thread_count; ++i)
			{
				threads.push_back(std::thread(&thread_pool::worker_thread, this, i));//每个线程从队列中取出任务
			}
		}
		catch (...)
		{
			done = true;
			throw;
		}
	}
	~thread_pool()
	{
		done = true;
	}

	template<typename FunctionType>
	std::future<typename std::result_of<FunctionType()>::type> submit(FunctionType f)
	{
		typedef typename std::result_of<FunctionType()>::type result_type;
		std::packaged_task<result_type()> task(f);
		std::future<result_type> res(task.get_future());
		if (local_work_queue)//判别当前线程是否为池内线程
		{
			local_work_queue->push(std::move(task));
		}
		else
		{
			pool_work_queue.push(std::move(task));
		}
		return res;
	}
	void run_pending_task()
	{
		task_type task;
		if (pop_from_local_queue(task) ||
			pop_from_pool_queue(task) ||
			pop_from_other_queue(task))
		{
			task();
		}
		else
		{
			std::this_thread::yield();
		}
	}
};

增设由线程池维护的局部任务队列组,用于存储局部任务队列,各线程凭索引值获得自身的局部任务队列,线程空闲时,就会尝试从队列组中获取其它线程专属的任务队列。

在窃取队列函数中,线程会逐个访问线程池内全部局部队列,其顺序从自身索引的后一个索引开始,避免了第一个索引对应的队列成为“众矢之的”。

2.中断线程

在许多情况下,如果线程运行时间过长,我们便想发送信号令其终止,其原因可能是线程池需要销毁,或用户取消执行的任务。我们需要一种通用的中断方式,以方便应对各种情形。(C++20中正式引入std::jthread,其下管控的线程可以接受中断,还能自动汇合)。

2.1可中断的线程

中断线程需要一个interrupt()函数,可以将thread_local变量作为中断专门定制的数据结构,启动线程时,将其准备妥当,当线程调用中断函数时,就查验该数据结构,得知此处是否可中断:

class interrupt_flag
{
public:
	void set();
	bool is_set() const;
};
thread_local interrupt_flag this_thread_interrupt_flag;
class interruptible_thread
{
	std::thread internal_thread;
	interrupt_flag* flag;
public:
	template<typename FunctionType>
	interruptible_thread(FunctionType f)
	{
		std::promise<interrupt_flag*> p;
		internal_thread = std::thread([f, &p] {
			p.set_value(&this_thread_interrupt_flag);
			f(); })
			flag = p.get_future().get();
	}
	void interrupt()
	{
		if (flag)//检验中断标志的指针
		{
			flag->set();//设置标志成立
		}
	}
};

调用者给出函数f()用于创建中断线程,构造函数发起新线程,将promise关联值设置为标志地址,再运行函数f(),此时发起调用的线程等待,直到将promise关联的值存储到flag中。注意线程分离或退出时,必须清除flag,避免指针悬空。

2.2检测线程是否被中断

我们已经可以设置中断标志位成立,但如果没有进行检测,终究是徒劳。简单的做法是,在可中断的线程内设置interruption_point()函数,中断后抛出异常:

void interruption_point()
{
	if (this_thread_interrupt_flag.is_set())
	{
		throw thread_interrupted();
	}
}

实际上,中断线程的最佳时机是线程因等待而发生阻塞时,但是若线程被阻塞,遂无法执行interruption_point(),故需要一种能被中断的等待方式。

2.3中断条件变量上的等待

我们需要引入一种新的函数——interruptible_wait(),可以重载以分别处理不同需要的事项。条件变量是一种需要等的事项,我们从条件变量入手,再interrupt_flag内部增添一个指针,指向相应的条件变量,set()的调用会通知它,以下是interruptible_wait()的简单实现:

class interrupt_flag
{
	std::atomic<bool> flag;
	std::condition_variable* thread_cond;
	std::mutex set_clear_mutex;
public:
	interrupt_flag():thread_cond(0)
	{}
	void set()
	{
		flag.store(true, std::memory_order_relaxed);
		std::lock_guard<std::mutex> lk(set_clear_mutex);
		if (thread_cond)
		{
			thread_cond->notify_all();//设置flag为可中断时需要通知所有线程
		}
	}
	bool is_set() const
	{
		return flag.load(std::memory_order_relaxed);
	}
	void set_condition_variable(std::condition_variable& cv)
	{
		std::lock_guard<std::mutex> lk(set_clear_mutex);
		thread_cond = &cv;
	}
	void clear_condition_variable()
	{
		std::lock_guard<std::mutex> lk(set_clear_mutex);
		thread_cond = 0;
	}
	struct clear_cv_on_destruct
	{
		~clear_cv_on_destruct()
		{
			this_thread_interrupt_flag.clear_condition_variable();
		}
	};
};

void interrupt_wait(std::condition_variable& cv, std::unique_lock<std::mutex>& lk)
{
	interruption_point();
	this_thread_interrupt_flag.set_condition_variable(cv);
	interrupt_flag::clear_cv_on_destruct guard;
	interruption_point();
	//设置等待时间,便能察觉
	cv.wait_for(lk, std::chrono::milliseconds(1));
	interruption_point();
}

或需要断言的版本:

template<typename Predicate>
void interrupt_wait(std::condition_variable& cv, std::unique_lock<std::mutex>& lk, Predicate pred)
{
	interruption_point();
	this_thread_interrupt_flag.set_condition_variable(cv);
	interrupt_flag::clear_cv_on_destruct guard;
	while (!this_thread_interrupt_flag.is_set() && !pred)
	{
		cv.wait_for(lk, std::chrono::milliseconds(1));
	}
	interruption_point();
}

2.4中断std::condition_variable_any上的等待

std::condition_variable_any与std::condition_variable的区别在于,前者可以配合任意型别的锁,后者仅限于std::unique_lock<std::mutex>。因此我们可以自定义构建锁,用于锁定/解锁interrupt_flag中的互斥:

class interrupt_flag
{
	std::atomic<bool> flag;
	std::condition_variable* thread_cond;
	std::condition_variable_any* thread_cond_any;
	std::mutex set_clear_mutex;
public:
	interrupt_flag() :thread_cond(0)
	{}
	void set()
	{
		flag.store(true, std::memory_order_relaxed);
		std::lock_guard<std::mutex> lk(set_clear_mutex);
		if (thread_cond)
		{
			thread_cond->notify_all();//设置flag为可中断时需要通知所有线程
		}
		else if (thread_cond_any)
		{
			thread_cond_any->notify_all();
		}
	}
	template<typename Lockable>
	void wait(std::condition_variable_any& cv, Lockable& lk)
	{
		struct custom_lock
		{
			interrupt_flag* self;
			Lockable& lk;
			custom_lock(interrupt_flag* self_,
				std::condition_variable_any& cond,
				Lockable& lk_) :self(self_), lk(lk_)
			{
				self->set_clear_mutex.lock();
				self->thread_cond_any = &cond;
			}
			void unlock()
			{
				lk.unlock();
				self->set_clear_mutex.unlock();
			}
			void lock()
			{
				std::lock(self->set_clear_mutex, lk);//给互斥和锁对象一起加锁
			}
			~custom_lock()
			{
				self->thread_cond_any = 0;
				self->set_clear_mutex.unlock();
			}
		};
		custom_lock cl(this, cv, lk);
		interruption_point();
		cv.wait(cl);//调用lock()加锁
		interruption_point();
	}
    //以下代码与2.3节一样
	bool is_set() const
	{
		return flag.load(std::memory_order_relaxed);
	}
	void set_condition_variable(std::condition_variable& cv)
	{
		std::lock_guard<std::mutex> lk(set_clear_mutex);
		thread_cond = &cv;
	}
	void clear_condition_variable()
	{
		std::lock_guard<std::mutex> lk(set_clear_mutex);
		thread_cond = 0;
	}
	struct clear_cv_on_destruct
	{
		~clear_cv_on_destruct()
		{
			this_thread_interrupt_flag.clear_condition_variable();
		}
	};
};
//外部调用时
template<typename Lockable>
void interruptible_wait(std::condition_variable_any& cv, Lockable& lk)
{
	this_thread_interrupt_flag.wait(cv, lk);
}

在wait中,构造函数构建自定义锁的结构体,先在内部互斥set_clear_mutex上锁,获取内部锁(因为是引用,必须先完成锁定再保存到结构体内部)与条件变量。
wait()完成等待后,调用结构体自身的lock加锁。
接着重新检查中断,析构函数清除thread_cond_any指针,并解锁互斥。

2.5中断其他类型的阻塞

我们需要借助处理std::condition_variable用到的限时功能,因为上述等待行为不涉及等待某个条件成立,可以在interruptible_wait()函数中用循环等待(std::future<>的成员函数):

template<typename T>
void interruptible_wait(std::future<T>& uf)
{
	while (!this_thread_interrupt_flag.is_set())
	{
		if (uf.wait_for(lk, std::chrono::microseconds(1)) == std::future_status::ready)
			break;
	}
	interruption_point();
}

函数一直循环等待,知道中断标志位成立或future准备就绪。wait_for至少等待一个计时单元,缩短时间会使线程更频繁地唤醒。

2.6处理中断

以线程视角观察,中断本质上是一次thread_interrupt异常,可以用标准的try/catch块捕获:

try
{
	do_something();
}
catch(thread_interrupted&)
{
	handle_interruption();
}

令目标线程关联interruptible_thread对象,别的线程在该对象上调用interrupt()引发中断,程序捕获这个中断,放弃被中断的任务,线程继续执行下一项任务。 

由于thread_interrrupted是异常,需要保证异常安全。我们希望中断结束线程,中断异常向上传播,但线程构造设定了函数,异常传播到函数外会被std::terminate()终止整个程序。为了防止该情形,我们在interruptible_thread构造函数中放置catch块:

template<typename FunctionType>
interruptible_thread(FunctionType f)
{
	std::promise<interrupt_flag*> p;
	internal_thread = std::thread([f, &p] {
		p.set_value(&this_thread_interrupt_flag);
		try
		{
			f();
		}
		catch (thread_interrupted const&)
		{}
	});
	flag = p.get_future().get();
}

2.7在应用程序退出时中断后台任务 

为了避免影响图形用户界面的交互响应能力,往往需要分离出线程处理后台任务,其贯穿应用程序运行周期的全过程,当应用程序关闭时,需要依次结束后台线程,其中一个做法是中断:

std::mutex config_mutex;
std::vector<interruptible_thread> background_threads;

void background_thread(int disk_id)
{
	while (true)//后台线程不断更新索引
	{
		interruption_point();//判别是否中断
		fs_change fsc = get_fs_changes(disk_id);
		if (fsc.has_change())
		{
			update_index(fsc);
		}
	}
}
void start_background_processing()//后台线程启动
{
	background_threads.push_back(interruptible_thread(background_thread, disk_1));
	background_threads.push_back(interruptible_thread(background_thread, disk_2));
}
int main()
{
	start_background_processing();
	process_gui_until_exit();//主线程负责图形界面
	std::unique_lock<std::mutex> lk(config_mutex);
	for (unsigned i = 0; i < background_threads.size(); ++i)
	{
		background_threads[i].interrupt();//退出时后台线程中断
	}
	for (unsigned i = 0; i < background_threads.size(); ++i)
	{
		background_threads[i].join();//后台线程汇合后主线程才退出
	}
}

此处先中断全部线程再汇合全部线程,而不是中断一个汇合一个,是为了提高并发性能。线程不会因为中断就立马结束,它们必须先运行到中断点,调用析构函数,或处理异常,这样做令全体线程并行处理各自中断,只有每个线程都发起了中断,才让主线程等待汇合。

小结

线程池的基本构成:完成标志、线程组、线程安全的任务队列。

线程池的基本实现:线程池构建时,按照硬件参数创建线程并放入线程组,每个线程运行函数worker_thread()循环执行任务队列中的任务,直到完成标志成立(表示线程池被析构或抛出异常)。用户使用时,通过调用线程池的submit()接口往任务队列中压入任务,任务交由线程池处理。

支持等待的线程池:通过std::packaged_task包装任务,通过future等待返回结果。

支持相互等待的线程池:等待的线程通过调用run_pending_task()(实际是worker_thread()的外部接口,可在worker_thread()中调用)协助处理任务。

避免争夺:通过设置线程池内线程独有的局部任务队列(在worker_thread()内初始化),优先处理局部队列来实现。全局队列则存放局部队列指针为空的任务(没有经过线程池构造函数初始化指针)。

任务窃取的线程池:线程池内,除了全局队列和局部队列,增设局部任务队列组(通过索引跟踪所有局部队列),当局部线程和全局线程均空时,尝试从局部队列组中取出局部任务队列,执行局部任务队列中的任务。注意尝试窃取的顺序应错开(根据自身索引设置初始索引),避免所有线程都从第一个队列开始。

中断条件变量的等待:检查中断点、条件变量等待(wait_for)、检查中断点,std::condition_variable_any条件变量适用自定义锁。

中断其他阻塞:循环等待中断标志,并限时等待future准备就绪。

处理中断:在可中断线程类的构造函数中,引入try/catch块捕获内部线程的中断异常。

应用程序先中断所有后台线程,再进行汇合。

史上最全的C++面试宝典(九)—— 多线程 参考:https://www.runoob.com/cplusplus/cpp-tutorial.html 本教程旨在提取最精炼、实用的C++面试知识点,供读者快速学习及本人查阅复习所用。 第一章 C++基本语法 C++ 程序可以定义为对象的集合,这些对象通过调用彼此的方法进行交互。 对象 -对象具有状态和行为。例如:一只狗的状态 - 颜色、名称、品种,行为 - 摇动、叫唤、吃。 类 -类可以定义为描述对象行为/状态的模板,对象是类的实例。 方法 -从基本上说,一个方法表示一种行为。一... 阅读详情

相关推荐

【C++】并发编程

一、线程创建与管理 1.1 并发 1.1.1 并发与并行 并发:同一时间段内可以交替处理多个操作,强调同一时段内交替发生。 并行:同一时刻内同时处理多个操作,强调同一时刻点同时发生。 1.1.2 硬件并发与任务切换 单片机上的单核处理器支持并发多任务处理,依靠任务切换实现,与多核处理器上的多任务并发处理方式不同。 双核处理器并行执行(硬件并发)对比单核处理器并发执行(任务上下文切换) 双核处理器均并发执行(一般任务数远大于处理器核心数,多核并发更常见) 1.1.3 多线程并发与多进程并发 任务

踏雪寻梅 2861

C++11:std::function<void()> func;

转自:使用std::function作为函数入参 关于std::function() 在C语言的时代,我们可以使用函数指针来吧一个函数作为参数传递,这样我们就可以实现回调函数的机制。到了C++11以后在标准库里引入了std::function模板类,这个模板概括了函数指针的概念 函数指针只能指向一个函数,而std::function对象可以代表任何可以调用的对象,比如说任何可以被当作函数一样调用的对象。 当你创建一个函数指针的时候,你必须定义这个函数签名(表征这个函数的入参,返回值等信息);同样的,当你创建

苦逼的程序猿☺ 1万+

「C/C++」C++多线程 之 并发编程

C++通过其强大的标准库和灵活的语法特性,为并发编程提供了丰富的工具和手段。从基本的线程管理到高级的并行算法,再到异步编程的支持,C++都能满足不同的并发处理需求。然而,并发编程也带来了复杂性,开发者需要深入理解同步机制、线程安全等问题,以确保程序的正确性和性能。通过合理的设计和实践,C++并发编程可以极大地提升程序的执行效率和响应能力。何曾参静谧的博客。

何曾参静谧的博客 1059

std::function函数模板类 std::placehoders

转载 https://www.cnblogs.com/yinwei-space/p/12708871.html https://www.cnblogs.com/gavinhe/p/9728972.html 类模版std::function是一种通用、多态的函数封装。std::function的实例可以对任何可以调用的目标实体进行存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等。std::function对象是对C++中现有的可调用实体的一种类型安全的包裹(我

automate123的博客 2396

std::function<void(void)> 泛型函数对象,它可以存储任何类型的函数指针或Lambda表达式。

【代码】std::function<void(void)> 泛型函数对象,它可以存储任何类型的函数指针或Lambda表达式。

成长需要沉淀。 860

CXL Virtual Hierarchy (VH) Enumeration理解

在 CXL 3.0 协议中,CXL Virtual Hierarchy (VH) Enumeration 是一种针对 CXL 虚拟层级(Virtual Hierarchy) 设计的设备枚举机制,它允许软件将物理连接的 CXL 设备抽象为多个逻辑层级(虚拟层级),从而实现更灵活的资源管理和拓扑扩展。与传统的 PCIe 枚举相比,CXL VH 枚举在协议特性、拓扑抽象和资源分配机制上有显著差异。本质:VH 是一个逻辑上的设备层级结构,可能与物理连接拓扑不同。

Summer的博客 1002

【C++】可调用对象包装器function<void()> 和 bind

std::bind在 C++11 及之后,和std::bind提供了一种灵活的方式来存储和调用可调用对象(函数、成员函数、Lambda 等),它们是现代 C++ 设计模式的重要组成部分。

m0_49106549的博客 1000

GitHub工业级开源利器:StreamPipes如何简化工业物联网数据分析

Apache StreamPipes是一款GitHub上的工业级开源利器,旨在简化工业物联网数据分析。它通过图形化界面连接各类工业协议数据源,以拖拽方式构建流处理管道,并实现实时可视化,有效降低了数据分析的技术门槛,让业务人员也能自助完成从数据接入到洞察的全流程。

weixin_29279153的博客 185

C++ std::function<void(int)> 和 std::function<void()> 作为函数参数的注意事项

std::function 使用过程中的一些记录

天行健,地势坤 1万+

深入理解HanLP1.x,填平可能遇到的坑

提取指定数量的摘要。自定义词典为什么要单独领出来讲呢?因为在我们的业务开发中,可能分词的效果并不理想,需要定制自己的词库,索引更加常用。例如:小米su7特斯拉modelY比亚042025款6575767670908我们需要分出小米su72025款等词。我们分别看看用字典和不用字典的效果。

static_coder的博客 977

STM32F407用FSMC+DMA驱动LVGL,3.5寸屏流畅度翻倍实战(附CubeMX配置避坑)

本文详细介绍了如何在STM32F407上通过FSMC+DMA驱动LVGL,显著提升3.5寸屏的显示流畅度。从FSMC与DMA的配置原理到LVGL驱动层适配,提供了完整的优化方案和实测数据,帮助开发者解决界面卡顿问题,实现流畅度翻倍的效果。

weixin_30872499的博客 559

C++并发编程:线程、同步与异步的艺术

本文介绍了C++并发编程的核心概念和技术。主要内容包括:并发与并行的区别、线程创建与管理、同步机制(互斥锁、条件变量、原子操作)以及异步编程(future和promise)。通过代码示例演示了线程安全操作、线程间通信和异步任务处理的方法。文章旨在帮助开发者掌握并发编程思维,设计高效且线程安全的代码,提升多线程应用的性能和可靠性。

编程世界如山海,探索不止。 1万+

二维卡尔曼滤波位置,速度融合

这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个自定义列表如何创建一个注脚注释也是必不可少的KaTeX数学公式新的甘特图功能,丰富你的文章UML 图表FLowchart流程图导出与导入导出导入 欢迎使用Ma...

Johnlee2019的博客 8451

C++并发编程 -1.线程管理

本栏主要介绍《C++并发实战编程》这本书,。将按照书目录分为 九章节介绍C++并发编程,尽可能简化本书内晦涩难懂知识点。本章节主要讲解线程使用基础。详细介绍线程使用基础(线程发起、等待、参数、异常处理)、线程管控(归属权、并发数量等)。

qq_45604814的博客 1170

uniapp - [全端兼容] 高性能 tabbar 底部菜单凸起效果,原生系统自定义底部菜单不卡顿、切换页面不闪烁、自动缓存页面(底部菜单中间自定义一个图片并悬浮起来)

uniapp如何实现底部凸起按钮,uniapptabbar底部菜单怎么自定义,uniapp怎么让底部菜单中间突出来,uniapp自定义底部tabbar,uniapp高性能好用的底部菜单,uniapp自定义底部导航栏并解决闪烁问题,uniapp怎么设置凸起的底部tabbar,uniapp怎么用系统的底部菜单还想让中间的按钮凸起来,uniapp自定义tabbar性能不好怎么办,uni-app自定义导航栏实现中间凸起效果,uniapp底部导航栏中间圆形按钮凸起效果详细代码,uni-app自定义中间图标突起的

🏆 前端领域优质创作者 1万+

YOLO格式数据集划分

YOLO格式的数据集划分 标题标签格式txt import os import cv2 import random from PIL import Image # os.makedirs('labels_train') # os.makedirs('labels_val') img_train_path ='./img_train/' #图片训练集保存路径 img_val_path = './img_val/' #图片验证集保存路径 labels_train_path = './labels_t

weixin_51154380的博客 3308

Multithreading-Cpp-Course:掌握现代C++并发编程的不二选择

Multithreading-Cpp-Course:掌握现代C++并发编程的不二选择 项目介绍 在现代软件开发领域,多线程编程是一个至关重要的技能。它不仅能提升程序的性能和响应速度,还能有效利用多核处理器的能力。Multithreading-Cpp-Course 是一套针对现代C++并发编程的教程源码,源自知名的在线教育平台Udemy上的课程 "Learn Multithreading ...

gitblog_01108的博客 705

让 IF 更好读:在 ABAP 里尽量使用正向条件的实战指南

摘要:SAP系统开发中,代码可读性与可维护性的关键往往在于条件判断的写法。Clean ABAP风格指南建议尽量采用正向表达的条件判断,如使用has_entries = abap_true而非否定式写法。正向表达能降低理解成本,避免"否定的否定"带来的逻辑混乱,但不应为追求正向而制造空分支。在团队协作中,正向条件更利于代码评审和测试覆盖,建议通过工具检查和智能评审确保规范执行。落地时可关注变量命名正向化、避免空分支、主流程清晰展开等要点,以提升长期维护效率。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 246

C++ 中的多线程与并发编程:从基础到进阶

C++ 的多线程编程为我们提供了强大的并发控制和线程管理能力。通过使用std::mutexstd::async等工具,我们能够高效地创建和管理线程,同时保证数据的一致性和安全性。无论是简单的多线程任务,还是复杂的并发算法,C++ 都能为我们提供全面的支持。在实际开发中,掌握 C++ 多线程编程不仅能提升程序性能,还能帮助你更好地利用现代硬件资源。希望本文能够帮助你在 C++ 多线程编程的道路上迈出坚实的一步。

byby0325_的博客 1379

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

TMS_WebGMaps_1.9.8.0_Full_Source

TMS_WebGMaps_1.9.8.0_Full_Source 全源码 delphi cbuilder

线程纵横:C++并发编程的深度解析与实践

在C++11之前,涉及到多线程问题,都是和平台相关的,比如windows和linux下各有自己的接口,这使得代码的可移植性比较差。C++11中最重要的特性就是对线程进行支持了,使得C++在并行编程时不需要依赖第三方库,而且在原子操作中还引入了原子类的概念。要使用标准库中的 线程,必须包含< thread >头文件。常见线程函数简介:用于创建和管理的线程对象。通过构造函数启动线程,并将一个可调用对象(如函数、Lambda表达式、函数对象)作为参数传递给线程。返回当前线程的ID。

weixin_73951752的博客 1963

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

C++并发编程实战读书笔记——线程管理

并发的两种途径:多个进程,每个进程只有一个线程(multiple single-threaded processes);每个进程有多个线程(multiple threads in a single process)。多个单线程/进程的启动和通信开销要比单一进程多线程间的启动和通信大。若不考虑共享内存可能带来的问题,多线程是主流语言(包括C++)更青睐的并发途径。此外,C++标准并未对进程间通信提供...

肥叔菌的博客 8817

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

C++并发编程:线程与同步的艺术

本文介绍了C++并发编程的核心概念与实践方法。主要内容包括:线程的创建与管理(基本使用、属性设置、错误处理);互斥锁的应用(基本使用、死锁预防);以及条件变量的同步机制。通过代码示例演示了如何实现线程间的安全通信与资源同步,帮助开发者掌握高效、安全的多线程编程技术。文章涵盖了从基础概念到实际应用的全过程,适合希望提升并发编程能力的C++开发者学习参考。

编程世界如山海,探索不止。 2万+

C++的线程管理

这些提供者中的每一个(要么是promise或packaged_task对象,要么是对async的调用)与未来对象共享对共享状态的访问:提供者使共享状态准备就绪的点与未来对象访问共享状态的点同步状态。函数模板 std::async 异步运行函数 f ,可能在一个单独的线程中,该线程可能是线程池的一部分,并返回一个 std::future ,它最终将保存该函数调用的结果。默认构造的(未初始化的)线程对象是不可连接的,并且它的线程 id 对于所有不可连接的线程都是通用的。future 对象仅在有效时才有用。

IT_Beijing_BIT的博客 2062

C++并发编程实战(二):线程管理

一旦线程完成并且join()返回后,std::thread对象将不再关联任何线程,因此不能再次调用join()。如果std::thread对象在其析构之前没有被join()或detach(),那么析构函数会调用std::terminate()终止程序。因此,必须确保在线程对象销毁之前,要么join()要么detach()。如果std::thread对象没有关联任何执行线程(例如,它已经被join()或detach()),则get_id()将返回默认构造的std::thread::id值,表示“没有线程”。

_ 1199

C++并发编程:线程池学习

类模版 std::function 是一种通用、多态的函数封装,可以对任何可以调用的目标实体进行存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等std::function 对象是对C++中现有的可调用实体的一种类型安全的包裹(我们知道像函数指针这类可调用实体,是类型不安全的)

qq_45797625的博客 2044

C++并发编程 - 线程管理

我们的对象就是在创建的时候方才执行函数,而不是使用某一个类似于run的API执行。这使得我们启动一个线程的方式变得很简单:// A demo我们这样就派生出了一个执行流task。出于C++语法解析的原因,有的时候我们会遇上定义-声明混淆的问题。比如说假使我们传递了一个自己定义的仿函数:​int main()MyTask t;这里会被认为是声明了一个带有一个参数,返回值是std::thread的名称是task的函数。

charlie114514191的博客 1086
上一篇: C++并发编程(八)设计并发代码
下一篇: C++并发编程(十)并行算法函数
Cvincent976
博客等级 码龄6年 34粉丝 20原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值