C++11多线程学习笔记<一>

C++11多线程学习笔记<一>

修改日期 2017年12月17日

一、简述

对于多线程,我想各位并不陌生。在提高时钟频率代价变得昂贵的今天,多线程成为了一种趋势。

许多人把多线程看做邪恶的黑魔法,因为多线程在提高程序效率的同时,一方面降低了程序的可阅读性,另一方面增加了程序维护的难度,对程序员提出了更高的要求。
所幸,新的C++标准的出现,使用户可以不在类似于QT之类的平台下写出多线程代码。

二、开始

下面以一个简单的C++ 多线程例子开始对C++11多线程的学习。

#include<iostream>
/*在标准C++库中对多线程支持的声明在新的头文件中,用于管理线程和函数的类在<thread>中声明*/
#include<thread>
void hello()//每个新线程必须要有一个初始函数,新线程的执行在此开始。
{
	std::cout << "Hello,Concurrent World" << std::endl;
}
int main()//主线程main()
{
	std::thread t(hello);//此时对象t 具有新函数hello()做为其初始函数。
	//t.join();//如果不调用join()主线程就会结束,不管新线程是否执行完成。
	return 0;
}

1 当调用join()时,输出Hello,Concurrent World.

2 当不调用join()时,输出H.注意此时输出结果与用户实际情况相关。

Attention: 为了避免命名空间冲突建议之后不要写 using namespace std 而是使用std::

二、管理线程

每个C++ 程序都至少具有一个线程,它是由C++在运行时启动的,该线程运行着main()函数。

1.启动线程

std::thread可以与任何可调用对象(callable)一同工作,在C++ 中可调用对象包括
函数 函数指针 lambda表达式 bind 创建的对象 重载了()运算符的类
下面用一个重载()运算符的类的实例传递给std::thread的构造函数。

#include<iostream>
#include<thread>
class background_task
{
public:
	background_task(int temp_a,int temp_b):a(temp_a),b(temp_b){}
	void do_something();
	void operator ()()		
	{
		this->do_something();
	}
private:
	int a;
	int b;
};
void background_task::do_something()
{
	std::cout << a + b << std::endl;
}
void main()
{
	std::thread my_thread(background_task(5,7));
	my_thread.join();
}
```以上启动了一个线程并将两个`int`类型的对象传递给新线程,输出结果`a + b = 12`。

###### 2.等待线程完成
> 如果要等待线程完成,可以通过在相关联的 `std::thread`实例上调用`join()`来实现。
然而`join()`很简单也很暴力-要么等待一个线程完成要么就不等。在上文`Hello concurrency World `程序中,主线程调用`join()`意味着在等待新线程完成过程中,主线程没有任何操作,在这种情况下,意味着在独立线程上运行函数是没有意义的。在实际代码中,主线程可能要么有自己的工作去做,要么 是在等待所有线程完成之前就要启动多个线程来做有用的工作。如果要对等待线程进行更细粒度的控制,比如检查线程是否完成,或者只是在一段特定的时间间隔内进行等待,那么就要采用条件变量,future等替代机制。

###### 3.在异常环境下的等待
> 使用RAII`(资源获取即初始化 resource acquisition is initialization)`机制等待线程完成
具体可以参考`Effective C++`

include

include

int sum = 0;
struct thread_guard
{
public:
explicit thread_guard(std::thread &t_) :t(t_) {}
~thread_guard() {
if (t.joinable())//判断对象是否是可以结合的,只能对一个给定的线程调用一次join(),
//一旦调用了join(),此std::thread对象不再是可结合的,并返回false

	{
		t.join();
	}
	std::cout << sum << std::endl;
}
thread_guard(thread_guard const &) = delete;//禁止编译器自动生成拷贝构造函数
thread_guard &operator=(thread_guard const &) = delete;//禁止编译器自动生成拷贝赋值运算符

private:
std::thread &t;
};
struct func
{
int i;
func(int i_) :i(i_) {}
void operator()() {
for (size_t j = 0; j < 5; j++)
{
sum += i;
}
}
};

void main()
{
int some_local_state = 1;
func my_func(some_local_state);
std::thread my_thread(my_func);
thread_guard my_guard(my_thread);
//do_some_other();
}//当执行到此时,局部对象会按照构造函数的逆序被销毁,
//因此 my_guard 首先被销毁,即使在do_some_other 处发生异常也会发生

> 输出结果为 `5`
###### 4.在后台运行线程
> 在`std::thread`对象上调用`detach() `会把线程丢在后台运行,没有直接的方式与其通信。
其所有权和控制权被转交给C++运行时库,来确保与线程相关联的资源在线程退出后能够正确的回收。

> 参照UNIX守护进程(daemon process)概念,被分离的线程称之为守护线程(daemon threads),这样的线程通常长时间运行在后台。

> Attention : 在主线程结束后,与其关联的所有线程(即使它调用了`detcach()`)都将被销毁。
下面举一例子说明。

include

include

void func1() {

while (true)
{
	std::cout << "func1" << std::endl;
}

}
void func2() {
while (true)
{
std::cout << "func2" << std::endl;
}
}
void main()
{
std::thread t1(func1);
std::thread t2(func2);
t1.detach();
t2.detach();
}

> 输出结果`fu`,注意此结果与用户实际情况有关。

include

include

void func1() {

while (true)
{
	std::cout << "func1" << std::endl;
}

}
void func2() {
std::thread test(func1);
test.detach();
while (true)
{
std::cout << "func2" << std::endl;
}
}
void main()
{
std::thread t2(func2);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}

> 此时`func1`与`func2`交替输出,当且仅当主线程结束时结束。

###### 5.传递参数给线程函数

include

include

include

class X
{
public:
void do_length_work(std::string &test)
{
std::cout << test.length() << std::endl;
test = "last";
}
};
X my_x;
void main()
{
std::string temp = "hello";
std::thread t(&X::do_length_work, &my_x, temp);
//第一个参数是一个对象指针
//第二个参数是成员函数的指针
//第三个函数作为成员函数的第一个参数
std::this_thread::sleep_for(std::chrono::milliseconds(10));
std::cout <<temp<< std::endl;
}

> 结果为`5 hello` 可以看出即便函数中相关参数期待着引用,最终参数会以默认的方式被复制到内部存储空间。 除非使用`std::ref`包装确实被引用的参数

> Attention : 如果一个局部变量的指针被传递给新线程,如果包含该局部变量的函数在新线程上被转化为`std::string`之前退出,会导致未定义的行为。

void f(int i, std::string const &s);
void oops(int some_param)
{
char buffer[1024];
std::thread t(f, 3, buffer);
t.detach();
}

> 修改后

void f(int i, std::string const &s);
void oops(int some_param)
{
char buffer[1024];
std::thread t(f, 3, string(buffer));//使用std::string 避免悬浮指针
t.detach();
}

> Attention : 这里提供的参数只能被移动,不能被复制,一个对象内保存的数据被转移到另一个对象,使原来的对象变成"空壳",移动构造函数和移动赋值运算符是C++11的新特性。

### 本节结束
### 未完待续
### 参考资料C++ 并发编程实战(C++ Concurrency IN ACTION)
posted @ 2018-04-25 20:16  zezezeze  阅读(435)  评论(2)    收藏  举报