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);
}

 参考资料:

https://chenxiaowei.gitbook.io/c-concurrency-in-action-second-edition-2019/

猜你喜欢

转载自www.cnblogs.com/duan-shui-liu/p/11430290.html
今日推荐