C++11 多线程编程使用实例

最新研究下C++11中线程的知识,基本内容如下:

1、C++11中创建线程的几种方式


在C11中,我们可以通过创建std::thread类的对象来创建额外的线程。每个thread对象可以跟具体的某个线程关联,从而达到多线程并发的目的。 
必须 #include 
那么,std::thread 对象如何执行呢?很简单,给它一个回调入口(callback),当线程启动时就会自动执行callback。 
简单来说,callback可以有一下三种模式。 
- 函数指针(Function pointer) 
- 函数对象(Function Objece) 
- Lambda 表达式 
std::thread可以以这种方式来创建一个对象: std::thread throbj(); 
throbj对象创建后,即可以开始执行了,调用callback。 
如果某个线程A需要等待其它线程B结束,那么可以在A线程中调用join来实现。 
接下来看几个示例。

1.1、使用函数指针来创建(Function Pointer)
#include <unistd.h>
#include <iostream>
#include <thread>
using namespace std;

// 普通函数作为线程入口
void thread_function(const std::string& data) {
  for (int i = 0; i <= 5; i++) {
    cout << "in thread_function, data=" << data << ", i=" << i << endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
}

// 成员函数指针作为线程入口
class Task {
 public:
  Task() {}
  ~Task() {}
  // 普通成员函数
  void execute(const std::string& data) {
    for (int i = 0; i <= 3; i++) {
      cout << "in Task::execute"
           << ", data:" << data << ", i:" << i << endl;
      std::this_thread::sleep_for(std::chrono::seconds(1));
    }
  }

  // 静态成员函数
  static void execute_static(const std::string& data) {
    for (int i = 0; i <= 3; i++) {
      cout << "in Task::execute_staticF"
           << ", data:" << data << ", i:" << i << endl;
      std::this_thread::sleep_for(std::chrono::seconds(1));
    }
  }
};

int main() {
  std::string data = "hello";
  // 以普通函数作为线程入口
  std::thread thrObj1(&thread_function, data);

  // 以普通成员函数作为线程入口
  Task* task = new Task();
  std::thread thrObj2(&Task::execute, task, data);

  // 以静态成员函数作为程序入口
  std::thread thrObj3(&Task::execute_static, data);

  thrObj1.join();
  thrObj2.join();
  thrObj3.join();

  delete task;
  return 0;
}

1.2、使用函数对象来创建(Function Object)
// 函数对象作为线程入口
class DisplayThread {
 public:
  void operator()() {
    for (int i = 0; i <= 3; i++) {
      cout << "in DisplayThread(), i=" << i << endl;
      std::this_thread::sleep_for(std::chrono::seconds(1));
    }
  }
};

// in main threae
std::thread thrObj4((DisplayThread()));
thrObj4.join();

1.3、使用Lambda来创建(Lambda)
std::string data = "hello";
std::thread thrObj5([data]() {
    cout << "thread in Lambda, data=" << data << endl;
    for (int i = 0; i <= 3; i++) {
      cout << "thread runs in Lambda, i=" << i << endl;
    }
  });
thrObj5.join();


2、线程的join和detach


2.1、线程的join
主线程创建子线程A后,可以调用A.join()来等待其完成,这是有点简单而粗暴的方法,因为在线程A完成之前,主线程会一直阻塞在A.join()而什么都不能做。当线程A完成所有执行任务后其join()才会返回。主线程调用A.join()也包含了对线程A的内存空间清理等工作(每个线程有自己的调用栈)。 
在调用join()之前,子线程的状态是joinable的,而调用之后变成non-joinable了,因此线程只能调用**1次**join操作,否则会出现异常。

void func() {
  // do lots of work here....
  return;
}
int main() {
  std::thread thr(func);
  // do other work in main
  thr.join(); // OK, 等待thr的结束,清理其空间,使其可以安全销毁
  thr.join(); // error! 已经调用过一次join了,thr变成 non-joinable 了,会导致程序出错!
  return 0;
}

假如主线程未调用thr.join(不管是未显式调用,还是因为异常原因导致未调用),都会使thr线程对象仍然处于joinable状态,那么thr对象析构时会调用std::terminate(),默认行为会终止程序执行。

int main() {
  std::thread thr([] {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    cout << "child thread return" << endl;
  });
  std::this_thread::sleep_for(std::chrono::seconds(3));
  return 0;
}

上面的程序中,main线程结束前thr虽然早已执行完毕,但由于thr的join未被调用,使得thr仍然是joinable状态,thr的析构函数就会调用std::terminate(),导致程序异常终止,而不是正常结束。

2.2、线程的detach
如果在主线程里不希望因为调用join而被阻塞(例如,想让子线程自己成为一个后台线程,自己负责资源回收;或者主线程不知道该何时调用join),这种情况可以调用线程的detach()。

int main() {
  std::thread thr([] {
    std::this_thread::sleep_for(std::chrono::seconds(1));
    cout << "child thread return" << endl;
  });
  thr.detach(); // 调用了detach后,主线程就能正常退出
  std::this_thread::sleep_for(std::chrono::seconds(3));
  return 0;
}

调用了thr.detach()后,主线程就可以正常退出。同样的,detach也最多只能调用1次,如多长调用会导致程序异常。 
简而言之,对于std::thread对象,一定要确保如下: 
- 要么调用其join,要么调用其detach,不能两者都不调用 
- join和detach 都不能多次调用 
这样才能确保线程对象析构时不会调用std::terminate()异常终止程序。 
为保证这点,该如何做比较合适呢? 
很容易想到(RAII),即RESOURCE ACQUISITION IS INITIALIZATION。主要思想是在类对象构造时初始化资源,在对象析构时做资源清理工作。 
如下示例:

class ThreadRAII {
  std::thread& m_thread;

 public:
  ThreadRAII(std::thread& threadObj) : m_thread(threadObj) {}
  ~ThreadRAII() {
    // Check if thread is joinable then detach the thread
    if (m_thread.joinable()) {
      m_thread.detach();
    }
  }
};
void thread_func() {
  for (int i = 0; i <= 3; i++) {
    std::cout << "thread_function Executing" << std::endl;
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }
}

int main() {
  std::thread threadObj(&thread_func);

  // wrapperObj初始化时获得threadObj的引用
  // wrapperObj析构时自动调用threadObj.detach()
  ThreadRAII wrapperObj(threadObj);
  return 0;
}


3、给线程传递参数


3.1、简单参数传递
给线程传递参数,只需要在构造std::thread对象的时候把参数列表写上即可。 
每个线程都有自己的栈空间,默认情况下,传给线程的所有参数都会被拷贝到该线程的私有栈空间,再使用,即使用的是它们的拷贝。

void threadCallback(int x, std::string str) {
  std::cout << "x = " << x << std::endl;      // 10
  std::cout << "str = " << str << std::endl;  // Sample String
}
int main() {
  int x = 10;
  std::string str = "Sample String";
  std::thread threadObj(threadCallback, x, str);
  threadObj.join();
  return 0;
}

3.2、传递指针务必要注意
当然也可以给线程传递指针作为参数。不过千万要注意的是,要确保指针所指对象的生存期。否则会出问题。如下示例:


#include <iostream>
#include <thread>
void newThreadCallback(int* p) {
  std::chrono::milliseconds dura(2000);
  std::this_thread::sleep_for(dura);
  *p = 19;
}

int main() {
  {
    int i = 10;
    std::thread t(newThreadCallback, &i);
    t.detach();
  }
  // 变量i的生存期已经结束,但是线程t还在执行
  // 里面使用的指针p已经无效
  std::chrono::milliseconds dura(3000);
  std::this_thread::sleep_for(dura);
  return 0;
}

3.3、如果给线程传递引用
安装函数调用时传引用的用法,很容易想到给线程传递引用的方式如下:

#include <iostream>
#include <thread>

void threadCallback(int const &x) {
  int &y = const_cast<int &>(x);
  y++;
  std::cout << "Inside Thread x = " << x << std::endl;
}

int main() {
  int x = 9;
  std::thread threadObj(threadCallback, x);
  threadObj.join();
  std::cout << "Outside Thread x = " << x << std::endl;
  return 0;
}

然而,很遗憾,这种传递没达到预期。 
程序实际输出: 
Inside Thread x = 10 
Outside Thread x = 9 
虽然threadCallback线程接收引用,但是线程内部对变量的改动对线程外还是不可见。原因很简单,每个线程有自己的调用栈,线程内使用的这是参数的拷贝(副本)。就上面例子而言,threadCallback的x参数只是对一个临时对象的引用而已,该临时对象是main线程中x的拷贝。 
那么,如何做才能解决传引用的问题? 
很简单,使用 std::ref()

#include <iostream>
#include <thread>
void threadCallback(int const &x) {
  int &y = const_cast<int &>(x);
  y++;
  std::cout << "Inside Thread x = " << x << std::endl;
}
int main() {
  int x = 9;
  std::thread threadObj(threadCallback, std::ref(x));
  threadObj.join();
  std::cout << "After Thread Joins x = " << x << std::endl;
  return 0;
}

3.4、给类的成员函数传递参数
将类成员函数地址作为线程的回调地址,将对象地址作为第二个参数。其它要传递的参数紧跟其后即可。 
如果是类的静态成员变量,则只需要将静态函数地址作为线程的回调地址,不需要传对象地址。 
示例:

// 成员函数指针作为线程入口
class Task {
 public:
  Task() {}
  ~Task() {}
  // 普通成员函数
  void execute(const std::string& data) {
    for (int i = 0; i <= 3; i++) {
      cout << "in Task::execute"
           << ", data:" << data << ", i:" << i << endl;
      std::this_thread::sleep_for(std::chrono::seconds(1));
    }
  }

  // 静态成员函数
  static void execute_static(const std::string& data) {
    for (int i = 0; i <= 3; i++) {
      cout << "in Task::execute_staticF"
           << ", data:" << data << ", i:" << i << endl;
      std::this_thread::sleep_for(std::chrono::seconds(1));
    }
  }
};

int main() {
  Task task;
  std::thread thrObj1(&Task::execute, &task, "execute");
  std::thread thrObj2(&Task::execute_static, "execute_static");
  thrObj1.join();
  thrObj2.join();
}
 

转至:https://blog.csdn.net/weixin_40747540/article/details/78759359

猜你喜欢

转载自blog.csdn.net/a1173356881/article/details/85040028