c++多线程并发学习笔记(0)
多进程并发:将应用程序分为多个独立的进程,它们在同一时刻运行。如图所示,独立的进程可以通过进程间常规的通信渠道传递讯息(信号、套接字、。文件、管道等等)。
优点:1.操作系统在进程间提供附附加的保护操作和更高级别的通信机制,意味着可以编写更安全的并发代码。
2. 可以使用远程连接的方式,在不同的机器上运行独立的进程,虽然增加了通信成本,但在设计精良的系统数上,这可能是一个提高并行可用性和性能的低成本方法。
缺点:1. 这种进程间的通信通常不是设置复杂,就是速度慢,这是因为操作系统会在进程间提供了一定的保护措施,以免一个进程去修改另一个进程的数据
2. 运行多个进程所需的固定开销:需要时间启动进程,操作系统需要内部资源来管理进程
多线程并发:在单个进程中运行多个线程。线程就是轻量级的进程:每个线程独立运行,且线程可以在不同的指令序列中运行。但是进程中的所有线程都共享地址空间,并且所有线程所访问的大部分数据---全局变量仍然是全局的,指针 对象的引用或者数据可以在线程之间传递。
优点:地址空间共此昂,以及缺少线程间数据的保护,使得操作系统的记录工作量减小,所以使用多线程的开销远远小于使用多进程
缺点:共享内存的灵活性的代价:如果数据要被多个线程访问,那么程序员必须宝成每个线程访问的数据是一致的,这意味着需要对线程间的通信做大量的工作
并发与并行:
并行更加注重性能。在讨论使用当前可用硬件来提高批量数据处理的速度时,我们会讨论程序的并行性;当关注的重点在于任务分离或任务响应时,就会讨论到程序的并发性
std::thread 学习
需要包含的头文件 <thread>
初始化线程(启动线程)
一个简单的例子:
#include <iostream> #include <thread> using namespace std; void sayHello() { cout << "hello" <<endl; } int main() { thread t(sayHello); t.join(); }
初始化线程(启动线程)就是构造一个std::thread 的实例: std::thread(func)。 func 不简单的指函数,它是一个函数调用符类型,如下例子:
#include <iostream> #include <thread> using namespace std; class Test { public: void operator()() { cout << "hello" <<endl; } }; int main() { Test test; thread t(test); t.join(); }
也可以使用类的成员变量来初始化std::thread: std::thread(&Class::func, (Class)object)
#include <iostream> #include <thread> using namespace std; class Test { public: void sayHello() { cout << "hello" <<endl; } }; int main() { Test test; thread t(&Test::sayHello, &test); t.join(); }
注意:把函数对象传入到线程构造函数中时,需要避免以下情况: 如果你传递了一个临时变量,而不是一个命名的变量,c++的编译器会将其解释为函数声明,而不是类型对象的定义。例如:
std::thread myThread(func());
这里相当于声明了一个名为myThread的函数,这个函数带一个参数(函数指针指向一个没有参数并且返回func对象的函数),返回一个std::thread对象的函数,而不是启动了一个线程。
要解决这个问题,解决方法:
- 使用多组括号
std::thread myThread((func()));
- 使用大括号
std::thread myThread({func()});
- 使用lambda表达式
std::thread myThread([](){
do_something();
});
启动线程后,需要明确是要等待线程结束(加入式)还是让其自主运行(分离式),如果在对象销毁之前还没做出决定,程序就会终止(std::thread的析构函数会调用std::terminate())。即使有异常情况也要保证线程能够正确的加入(join)或者分离(detached)。
如果不等待线程,就要保证线程结束之前,可访问的数据的有效性。例如主线程往子线程中传了一个变量A的引用,子线程detach,则表示主线程可能在子线程之前结束,这样变量A便会被销毁,这时子线程再使用A的引用就会产生异常。处理这种情况的常规方法:使线程的功能齐全,将数据复制到线程中,而非复制到共享数据中。如果使用一个可调用的对象作为线程函数,这个对象就会复制到线程中,而后原始对象就会立即销毁,但对于对象中包含的指针和引用还需谨慎。最好不要使用一个访问局部变量的函数去创建线程。此外,可以通过join()函数来确保线程在函数完成前结束。
等待线程完成
使用std::thread 方法中的join()来实现等待线程完成
调用join()的行为,还清理了线程相关的存储部分,这样std::thread
对象将不再与已经完成的线程有任何关联。这意味着,只能对一个线程使用一次join();一旦已经使用过join(),std::thread
对象就不能再次加入了,当对其使用joinable()时,将返回false。
后台运行线程
使用std::thread 方法中的detach()来实现等待线程完成
使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不会等待这个线程结束;如果线程分离,那么就不可能有std::thread
对象能引用它,分离线程的确在后台运行,所以分离线程不能被加入。不过C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。当std::thread
对象使用t.joinable()返回的是true,才可以使用t.detach()
向线程函数传递参数
在初始化的时候可以进行参数传递 std::thread myThread(func, arg0, arg1,...),另外在使用类的成员函数来初始化线程时的参数传递std::thread(&Class::func, (Class)object, arg0, arg1,...)
在这里需要注意两个问题:
1. 在将指向动态变量的指针作为参数传给线程的时候,想要依赖隐式转换将字面值转换为函数期待的对象(1),但是
std::thread
的构造函数会复制提供的变量,就只复制了没有转换成期望类型的字符串字面值。解决方法是:(2)在传入之前先显示的进行转换
void f(int i,std::string const& s); void oops(int some_param) { char buffer[1024]; sprintf(buffer, "%i",some_param); //std::thread t(f,3,buffer); // 1 std::thread t(f,3,std::string(buffer)); // 2 使用std::string,避免悬垂指针 t.detach(); }
2. 期望传入一个引用,但整个对象被复制了。虽然期望传入一个引用类型的参数(1),但std::thread
的构造函数并不知晓;构造函数无视函数期待的参数类型,并盲目的拷贝已提供的变量。
解决方法是使用std::ref()来将参数转换为引用的形式(2)
void update_data_for_widget(widget_id w,widget_data& data); void oops_again(widget_id w) { widget_data data; //std::thread t(update_data_for_widget,w,data); // 1 std::thread t(update_data_for_widget,w,std::ref(data)); // 2 display_status(); t.join(); process_widget_data(data); }
转移线程所有权
首先要明确的是对于std::thread,不能将一个对象赋值给另一个对象,即赋值构造函数是被删除的。
thread(thread&) = delete;
但是我们有时需要转移线程的所有权,这时候就需要使用std::move()来实现
void some_function(); void some_other_function(); std::thread t1(some_function); // 1 std::thread t2=std::move(t1); // 2 t1=std::thread(some_other_function); // 3 隐式移动操作 std::thread t3; // 4 t3=std::move(t2); // 5 t1=std::move(t3); // 6 赋值操作将使程序崩溃
最后一个移动操作⑥,将some_function线程的所有权转移给t1。不过,t1已经有了一个关联的线程(执行some_other_function的线程),所以这里系统直接调用std::terminate()
终止程序继续运行。这样做(不抛出异常,std::terminate()
是noexcept函数)是为了保证与std::thread
的析构函数的行为一致。需要在线程对象被析构前,显式的等待线程完成,或者分离它;进行赋值时也需要满足这些条件(说明:不能通过赋一个新值给std::thread
对象的方式来"丢弃"一个线程)。
std::thread 支持移动操作,意味着它可以当做函数的返回值和参数
//作为函数返回值 std::thread g() { void some_other_function(int); std::thread t(some_other_function,42); return t; } //作为函数的参数 void f(std::thread t); void g() { void some_function(); f(std::thread(some_function)); std::thread t(some_function); f(std::move(t)); }
运行时决定线程数量
std::thread::hardware_concurrency() 这个函数会返回能并发在一个程序中的线程数量。例如,多核系统中,返回值可以是CPU核芯的数量。返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0。
标识线程
线程标识类型为std::thread::id,获取方法有两种:
1. 调用std::thread对象的成员函数get_id()来直接获取,如果std::thread
对象没有与任何执行线程相关联,get_id()
将返回std::thread::type
默认构造值,这个值表示“无线程”
2. 当前线程中调用std::this_thread::get_id()
(这个函数定义在<thread>
头文件中)也可以获得线程标识。
如果两个对象的std::thread::id
相等,那它们就是同一个线程,或者都“无线程”。如果不等,那么就代表了两个不同线程,或者一个有线程,另一没有线程。
std::thread::id 有丰富的比较方法,因此它可以当做容器的键值,做排序等等比较。标准库也提供std::hash<std::thread::id>
容器,所以std::thread::id
也可以作为无序容器的键值。
参考资料:
https://chenxiaowei.gitbook.io/c-concurrency-in-action-second-edition-2019/