C++并发学习笔记

并发的途径

一个经典的比喻:
当两个程序员在两个独立的办公室一起做一个软件项目,他们可以安静地工作、不互相干扰,并且他们人手一套参考手册。但是,他们沟通起来就有些困难,比起可以直接互相交谈,他们必须使用电话、电子邮件或到对方的办公室进行直接交流。并且,管理两个办公室需要有一定的经费支出,还需要购买多份参考手册。

假设,让开发人员同在一间办公室办公,他们可以自由的对某个应用程序设计进行讨论,也可以在纸或白板上轻易的绘制图表,对设计观点进行辅助性阐释。现在,你只需要管理一个办公室,只要有一套参考资料就够了。遗憾的是,开发人员可能难以集中注意力,并且还可能存在资源共享的问题(比如,“参考手册哪去了?”) [^1]: c++并发编程

以上内容描述了并发的两种基本途径,即

  1. 多进程并发 ,(进程的相关概念)每个进程都有自己的资源划分,多进程并发需要进行进程间通信。多进程间通信往往速度较慢开销较大,而且开启多进程有额外的系统开销。但是进程间有操作系统的保护,会更加安全。ROS中不同节点就属于多进程并发,节点间通过TCP/IP,socket进行通信(订阅和发布);
  2. 多线程并发 ,(线程的相关概念)多个线程共享进程的资源,容易产生冲突。要格外注意线程间的共享资源的保护(互斥锁,自旋锁,读写锁)和进程间通信(互斥量,信息量)。多线程相关开销远小于多进程,有更高的灵活性和效率。;

线程的状态:
1)就绪:参与调度,等待被执行,一旦被调度选中,立即开始执行
2)运行:占用CPU,正在运行中
3)休眠:暂不参与调度,等待特定事件发生
4)中止:已经运行完毕,等待回收线程资源
线程的全局资源:
1)代码区:这意味着当前进程空间内所有的可见的函数代码,对于每个线程来说,也是可见的
2)静态存储区:全局变量,静态空间
3)动态存储区:堆空间
线程内典型的局部资源:
1)本地栈空间:存放本线程的函数调用栈,函数内部的局部变量等
2)部分寄存器变量:线程下一步要执行代码的指针偏移量

进程与线程

待老夫总结一下,主要参考《unix环境高级编程》

线程管理

主要介绍< thread >的用法。

  1. 线程管理thread的构造:
void do_some_work();
std::thread my_thread(do_some_work); //使用do_some_work()作为函数入口
class background_task
{
public:
  void operator()() const
  {
    do_something();
    do_something_else();
  }
};

background_task f;
std::thread my_thread(f);//使用函数对象作为函数入口
std::thread my_thread2{background_task()};//使用c++11中的大括号进行右值初始化。
  1. 向线程入口函数传递参数
class background_task
{
public:
	void operator()(int times, std::string& s) {
		do_some_work_else(times, s);
	}
};
std::thread my_bt{background_task(),10,str}; //对函数对象传参
void f(int i, std::string const& s);
std::thread t(f, 3, "hello");  //对函数传参
//要注意的一点是,在向thread的入口函数传参时,默认参数会被拷贝到线程独立内存中。及时传的是引用
//考虑如下代码:
#include<iostream>
#include<thread>
#include<string>
#include<string>
void do_some_work_else(int times, std::string& s )
{
	int k = times;
	s[2] = '0';
	for (int i = 0; i < k; i++)
	{
		std::cout << s << std::endl;
	}
}
class background_task
{
public:
	void operator()(int times, std::string& s) {
		do_some_work_else(times, s);
	}
};

int main()//主线程
{	
	background_task bt;
	std::string str_raw{ "do_in_main" };
	std::string str_thread{ std::string{"do_in_thread"} };
	//std::string &str = str_raw;
	//std::string &strt = str_thread;
	do_some_work_else(3, str_raw);
	std::thread my_bt{background_task(),10,str_thread};//可以使用右值初始化;线程入口函数传递参数。
	//std::thread my_thread(do_some_work);
	my_bt.join();
	//my_thread.detach();
	std::cout << str_raw <<',' <<"main end" <<std::endl;
	std::cout << str_thread <<','<< "thread output" << std::endl;
}
//往my_bt里传引用和往函数do_some_work_else()里传引用。
//最后输出结果如下, 可以看出在my_bt线程里对传入的引用的修改并没有生效。通过debug也可以发现,进入my_bt后,string 引用的地址变了。
do0in_main
do0in_main
do0in_main
do0in_thread
do0in_thread
do0in_thread
do0in_thread
do0in_thread
do0in_thread
do0in_thread
do0in_thread
do0in_thread
do0in_thread
do0in_main,main end
do_in_thread,thread output
  1. 线程的join()和detach()
    线程启动后,要明确启动的线程的状态是join()还是detach()。
    若std::thread对象销毁前没有选择状态,则在std::thread的析构函数中会调用std::terminate()终止程序。
    若是join()状态,意味着当前线程要等待join()线程完成后再往下执行,线程只能join()一次;
    若是detach(),则detach()线程会进入后台运行,脱离与当前线程的关系。detach()线程通常用作守护线程,在后台长时间运行。如何判断当前线程可以detach()? 同join()一样,使用.joinable()进行判断。
    相比较来说,使用detach()可以更加随意,启动线程后,就可以把线程进行detach()分离。而join()则需要更加注意,将join()放置在你想要等待的位置,然而又必须保证不会因为异常产生等原因导致join()被跳过。有如下两种解决方式:
//当前线程捕获到异常时,进行join(),正常运行时也要join()
int some_local_state=0;
  func my_func(some_local_state);
  std::thread t(my_func);
  try
  {
    do_something_in_current_thread();
  }
  catch(...)
  {
    t.join();  // 1
    throw;
  }
  t.join();  // 2
//将join()放在析构函数里,构建一个thread_guard进行包装,使用该类进行包装。在thread_guard类的析构里做必要的操作。有点类似guard_lock。
//特别注意禁用各种存在复制风险的行为,thread的管理有所有权的概念,同unique_ptr,可以使用move语义进行转移,而不能复制。
class thread_guard
{
  std::thread t;
public:
  explicit thread_guard(std::thread& t_):t(t_)//禁止隐式复制构造函数使用。
  {}
  ~thread_guard()
  {
    if(t.joinable()) // join()只能调用一次,这里要做一次判断
    {
      t.join();      // 2
    }
  }
  thread_guard(thread_guard const&)=delete;   // 禁止默认复制构造和默认赋值。
  thread_guard& operator=(thread_guard const&)=delete;
};
struct func; //函数对象
void f()
{
  int some_local_state=0;
  func my_func(some_local_state);
  std::thread t(my_func);
  thread_guard g(t);
  do_something_in_current_thread();//thread_guard析构的时候确保了join()的调用。
}  
  1. 线程所有权转移
    当构造一个thread类时,这个thread类就拥有这个线程的管理权。线程thread类似于unique_ptr,可移动所有权,而不能复制。样例如下:
void some_function();
void some_other_function();
std::thread t1(some_function);		// t1 线程启动,以some_function为线程入口
std::thread t2=std::move(t1);		//由于t1是左值,使用move()进行所有权转移,所有权转移后,t1为空
t1=std::thread(some_other_function);	//t1启动以some_other_function为入口的线程。
std::thread t3;							
t3=std::move(t2);		//将t2,即some_function线程转移给t3			
t1=std::move(t3);    //将t3,即some_function线程转移给t1,然而t1已经绑定有线程。std::terminate()被调用,程序结束。
//由于支持move语义,thread可以作为函数返回值,也可以作为参数进行传递。

线程安全的stack

#include<vector>
#include<stack>
#include<exception>
#include<iostream>

struct empty_stack : std::exception
{
	const char* what() const throw()
	{
		return "empty stack!";
	}
};
template<typename T>
class threadsafe_stack
{
private:
	std::stack<T> data;
	mutable std::mutex m; //互斥量通常与被保护的数据放在同一个类中。
						  //mutable 只能用来修饰类的数据成员,使其可被const成员函数访问和修改。
public:
	threadsafe_stack()
		: data(std::stack<T>()) {}//默认构造

	threadsafe_stack(const threadsafe_stack& other) //拷贝构造
	{
		std::lock_guard<std::mutex> lock(other.m);//既然从别的地方拷贝数据过来,那当然要用别的互斥量来进行保护咯
		data = other.data;
	}

	threadsafe_stack& operator =(const threadsafe_stack&) = delete;//删除其赋值操作

	void push(T new_value)
	{
		std::lock_guard<std::mutex> lock(m);
		data.push(new_value);
	}
	//这里用了两种方式进行pop
	//stl中,弹出栈顶元素用top+pop两步走战略。
	//若直接获取栈顶元素保存下来然后删除栈顶的话,若函数返回时存储空间不够了,抛出异常
	//那么栈顶的元素就丢失了。
	//采用先top再pop的策略,就能最大限度保证数据的安全。
	//如下的代码中采用传引用和返回智能指针的方式,也能有效避免上述问题。
	std::shared_ptr<T> pop()
	{
		std::lock_guard<std::mutex> lock(m);
		if (data.empty()) throw empty_stack();

		std::shared_ptr<T> const res(std::make_shared<T>(data.top()));
		data.pop();
		return res;
	}

	void pop(T& value)
	{
		std::lock_guard<std::mutex> lock(m);
		if (data.empty()) throw empty_stack();

		value = data.top();
		data.pop();
	}

	bool empty() const
	{
		std::lock_guard<std::mutex> lock(m);
		return data.empty();
	}

};

tips:考虑交换两个stack的函数swap( threadSafestack1, threadSafestack2),那么需要在swap内部对stack1stack2对应的两个互斥量进行加锁。当然,由于加锁的顺序性得到保证(用的都是同一个swap函数,那么加锁顺序都是固定的),很大程度避免死锁的发生。但是还有一种更优的方法:

void swap(threadSafeStack stack1, threadSafeStack stack2)
{
	std::lock(stack1.mutex, stack2.mutex)// std::lock()支持同时锁住多个互斥量
	std::lock_guard<std::mutex> lock1(stack1.mutex,  std::adopt_lock); //因为stack1.mutex和stack2.mutex都已经被锁着了,所以需要标记std::adopt_lock,表示不再创建新锁。
	std::lock_guard<std::mutex> lock1(stack2.mutex,  std::adopt_lock);//加入这样的lock_guard主要是借用其优良特性(自动上锁解锁,异常安全)
	sssswap( stack1, stack2);//在这里安全的进行交换
}

有了以上的线程安全的stack类,做一个简单的实验验证一下是否真的安全。不安全又会发生什么鬼

void stackTest(threadsafe_stack<int>& stack, volatile int& count)
{
	
	while ( !stack.empty())
	{
		std::shared_ptr<int> res = stack.pop();
		std::cout << *res << std::endl;
		count += 1;
	}
	std::cout << "safeTest end " << std::endl;
}
//
void stackTest2(std::stack<int>& stack, volatile int& count)
{
	
	while (!stack.empty())
	{
		int res = stack.top();
		stack.pop();
		std::cout << res<< std::endl;
		count += 1;
	}
	std::cout << "Test end " << std::endl;
}
int main()
{
	threadsafe_stack<int> safe_stack;
	std::stack<int> stack;

	for (int i = 0; i < 100; i++)
	{
		safe_stack.push(i);
		stack.push(i);
	}
	volatile int count1 = 0;//使得每次获取count的值的时候,都要往内存区取。
	volatile int count2 = 0;
	std::thread t1(stackTest, std::ref(safe_stack), std::ref(count1));
	std::thread t2(stackTest, std::ref(safe_stack), std::ref(count1));

	std::thread ut1(stackTest2, std::ref(stack), std::ref(count2));
	std::thread ut2(stackTest2, std::ref(stack), std::ref(count2));
	
	t1.join();
	t2.join();
	std::cout << "safeStack : " << count1 << std::endl;
	
	ut1.join();
	ut2.join();
	std::cout << "std::Stack : " << count2 << std::endl;
}

以上代码,大概率会崩溃。当打印出safeStack的pop次数后,由于std::stack的empty()不是线程安全的,当stack中只剩一个元素了,两个线程同时进行pop,则会出错。而safeStack每次都能确保正确。同时,由于safeStack的pop也是线程安全的,所以可以看到每个元素都只输出一次,而且不会有遗漏(虽然输出顺序无法保证,因为没有用互斥量保护cout语句)。但是在std::stack上,则会产生元素重复输出和遗漏。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值