C++ 11 多线程

说到多线程编程,那么就不得不提并行并发,多线程是实现并发(并行)的一种手段。并行是指两个或多个独立的操作同时进行。注意这里是同时进行,区别于并发,在一个时间段内执行多个操作。在单核时代,多个线程是并发的,在一个时间段内轮流执行;在多核时代,多个线程可以实现真正的并行,在多核上真正独立的并行执行。例如现在常见的4核4线程可以并行4个线程;4核8线程则使用了超线程技术,把一个物理核模拟为2个逻辑核心,可以并行8个线程。

并发编程的方法

通常,要实现并发有两种方法:多进程和多线程。

多进程并发

使用多进程并发是将一个应用程序划分为多个独立的进程(每个进程只有一个线程),这些独立的进程间可以互相通信,共同完成任务。由于操作系统对进程提供了大量的保护机制,以避免一个进程修改了另一个进程的数据,使用多进程比多线程更容易写出安全的代码。但这也造就了多进程并发的两个缺点:

  • 在进程件的通信,无论是使用信号、套接字,还是文件、管道等方式,其使用要么比较复杂,要么就是速度较慢或者两者兼而有之。
  • 运行多个线程的开销很大,操作系统要分配很多的资源来对这些进程进行管理。

由于多个进程并发完成同一个任务时,不可避免的是:操作同一个数据和进程间的相互通信,上述的两个缺点也就决定了多进程的并发不是一个好的选择。

多线程并发

多线程并发指的是在同一个进程中执行多个线程。有操作系统相关知识的应该知道,线程是轻量级的进程,每个线程可以独立的运行不同的指令序列,但是线程不独立的拥有资源,依赖于创建它的进程而存在。也就是说,同一进程中的多个线程共享相同的地址空间,可以访问进程中的大部分数据,指针和引用可以在线程间进行传递。这样,同一进程内的多个线程能够很方便的进行数据共享以及通信,也就比进程更适用于并发操作。由于缺少操作系统提供的保护机制,在多线程共享数据及通信时,就需要程序员做更多的工作以保证对共享数据段的操作是以预想的操作顺序进行的,并且要极力的避免死锁(deadlock)

C++ 11的多线程初体验

#include std::thread
#include <iostream>
#include <thread> using namespace std; void output(int i)
{
cout << i << endl;
} int main()
{ for (uint8_t i = 0; i < 4; i++)
{
thread t(output, i);
t.detach();
} getchar();
return 0;
}
thread t(output, i)outputoutputt.detach
0 \n 1 \n 2 \n 3 \n

但是在并行多线程下,其执行的结果就多种多样了,下图是代码一次运行的结果:

可以看出,首先输出了01,并没有输出换行符;紧接着却连续输出了2个换行符。不是说好的并行么,同时执行,怎么还有先后的顺序?这就涉及到多线程编程最核心的问题了资源竞争。CPU有4核,可以同时执行4个线程这是没有问题了,但是控制台却只有一个,同时只能有一个线程拥有这个唯一的控制台,将数字输出。将上面代码创建的四个线程进行编号:t0,t1,t2,t3,分别输出的数字:0,1,2,3。参照上图的执行结果,控制台的拥有权的转移如下:

  • t0拥有控制台,输出了数字0,但是其没有来的及输出换行符,控制的拥有权却转移到了t1;(0)
  • t1完成自己的输出,t1线程完成 (1\n)
  • 控制台拥有权转移给t0,输出换行符 (\n)
  • t2拥有控制台,完成输出 (2\n)
  • t3拥有控制台,完成输出 (3\n)

由于控制台是系统资源,这里控制台拥有权的管理是操作系统完成的。但是,假如是多个线程共享进程空间的数据,这就需要自己写代码控制,每个线程何时能够拥有共享数据进行操作。共享数据的管理以及线程间的通信,是多线程编程的两大核心。

线程管理

mainstd::threadstd::thread

启动一个线程

std::threadstd::thread
do_task();
std::thread(do_task);
std::thread
  • lambda表达式

使用lambda表达式启动线程输出数字

for (int i = 0; i < 4; i++)
{
thread t([i]{
cout << i << endl;
});
t.detach();
}
  • 重载了()运算符的类的实例

使用重载了()运算符的类实现多线程数字输出

class Task
{
public:
void operator()(int i)
{
cout << i << endl;
}
}; int main()
{ for (uint8_t i = 0; i < 4; i++)
{
Task task;
thread t(task, i);
t.detach();
}
}
std::threadstd::thread
std::thread t(Task());
thread
std::thread t{Task()};
thread
  • detach方式,启动的线程自主在后台运行,当前的代码继续往下执行,不等待新线程结束。前面代码所使用的就是这种方式。
  • join方式,等待启动的线程完成,才会继续往下执行。假如前面的代码使用这种方式,其输出就会0,1,2,3,因为每次都是前一个线程输出完成了才会进行下一个循环,启动下一个新线程。
threadt.joint.detach
auto fn = [](int *a){
for (int i = 0; i < 10; i++)
cout << *a << endl;
}; []{
int a = 100; thread t(fn, &a); t.detach();
}();

在lambda表达式中,使用fn启动了一个新的线程,在装个新的线程中使用了局部变量a的指针,并且将该线程的运行方式设置为detach。这样,在lamb表达式执行结束后,变量a被销毁,但是在后台运行的线程仍然在使用已销毁变量a的指针,其输出结果如下:

只有第一个输出是正确的值,后面输出的值是a已被销毁后输出的结果。所以在以detach的方式执行线程时,要将线程访问的局部数据复制到线程的空间(使用值传递),一定要确保线程没有使用局部变量的引用或者指针,除非你能肯定该线程会在局部作用域结束前执行结束。当然,使用join方式的话就不会出现这种问题,它会在作用域结束前完成退出。

异常情况下等待线程完成

threaddetachthreadthreadjoinjointhreadjoin
void func() {
thread t([]{
cout << "hello C++ 11" << endl;
}); try
{
do_something_else();
}
catch (...)
{
t.join();
throw;
}
t.join();
}
joinfunc
join
class thread_guard
{
thread &t;
public :
explicit thread_guard(thread& _t) :
t(_t){} ~thread_guard()
{
if (t.joinable())
t.join();
} thread_guard(const thread_guard&) = delete;
thread_guard& operator=(const thread_guard&) = delete;
}; void func(){ thread t([]{
cout << "Hello thread" <<endl ;
}); thread_guard g(t);
}
gjoin

向线程传递参数

thread
void func(int *a,int n){}
int buffer[10];
thread t(func,buffer,10);
t.join();

需要注意的是,默认的会将传递的参数以拷贝的方式复制到线程空间,即使参数的类型是引用。例如:

void func(int a,const string& str);
thread t(func,3,"hello");
funcstring &const char*string

如果在线程中使用引用来更新对象时,就需要注意了。默认的是将对象拷贝到线程空间,其引用的是拷贝的线程空间的对象,而不是初始希望改变的对象。如下:

class _tagNode
{
public:
int a;
int b;
}; void func(_tagNode &node)
{
node.a = 10;
node.b = 20;
} void f()
{
_tagNode node; thread t(func, node);
t.join(); cout << node.a << endl ;
cout << node.b << endl ;
}
nodenodestd::refnodethread t(func,std::ref(node));

也可以使用类的成员函数作为线程函数,示例如下

class _tagNode{
public:
void do_some_work(int a);
};
_tagNode node; thread t(&_tagNode::do_some_work, &node,20);
node.do_some_work(20)

转移线程的所有权

threadmove
thread t1(f1);
thread t3(move(t1));
t1.joint1.detachthread
std::thread::id
threadget_id()this_thread::get_id()

总结

本文主要介绍了C++11引入的标准多线程库的一些基本操作。有以下内容:

std::refget_id