【C++ 并发与多线程】std::thread类-多线程的基本用法

什么叫并发 concurrency?

一遍走路一边说话;你打球我游泳

单核计算机上的并发是个假象,其实只是任务切换(task switching)需要上下文切换 
多处理器或一个处理器上有多个核上的并发才是自然的并发,叫硬件并发

【C++ 并发与多线程】std::thread类-多线程的基本用法_第1张图片

并发种类: 
1,多进程并发 
这些进程间可通过正常的进程通信渠道(信号,套接字,文件,管道等) 
缺点:1;通信建立较复杂或者慢;2;操作系统需要花时间启动进程和管理进程资源等 
优点:1;更容易写安全的并发代码比如Erlang语言 2;可以运行在不同的机器上 

【C++ 并发与多线程】std::thread类-多线程的基本用法_第2张图片
2,多线程并发 
线程像轻量级的进程;每个线程互相独立,一个进程的所有线程分享同一个地址空间,多数数据可以共享,比如全局变量,指针和引用可以在线程间传递; 
优点:共享地址空间和没有数据保护使得使用多线程程序比多进程程序代价小 
【C++ 并发与多线程】std::thread类-多线程的基本用法_第3张图片

 

C++11中引入了多线程编程,一般教科书中都没有涉及到这个概念,但是在工作中多线程却又是必不可少的。本文会展示最基本的Hello World, Concurrency程序。

C++11之后,多线程终于被支持了,C++开发者再也不需要借助boost等第三方库的力量,来实现跨平台的并行编程。语言层面上的支持,能够让开发者更加专注于业务逻辑,从而减少对系统平台的关注。C++11多线程的引入,让C++编程更加高效和简便。

最近一直研究多线程编程,忽然有感,如果我每掌握一个知识点,便分享出来,这比我蒙头死学更加高效。如果我的使用不正确,也有机会被指出来。所以便想整理网络或者书籍上所能学到的东西,写一个教程出来,力求能让刚入门不久的C++开发者,也能轻松的掌握并发编程的奥妙。

本教程不涉及太多原理性东西,需要那些知识的可以百度,或者买本《C++并发编程实战》,网路上或书籍里原理性的东西已经够多了,我想写的是让读者看完之后,能够很轻松的知道,该怎么用C++的多线程机制。其中,很多内容都是来自网路或者书籍上,感谢各位大神慷慨的分享。

如果没有特殊提及,本教程的所以代码都是基于Visual Studio 2015,如果更换其他编译器有可能无法运行。


Hello World

经典的Hello World式开端。

#include 
#include 
void hello()
{
	std::cout << "Hello world, concurrency" << std::endl;
}
int main()
{
	std::thread t(hello);
	t.join(); // 没有这句话,会Debug Error的
	return 0;
}

-------------------------------------------
输出:
Hello world, concurrency
请按任意键继续. . .

这段代码很简单,如果用过boost多线程编程,那么应该对这个了如指掌了。首先包含线程库头文件,然后定义一个线程对象t,线程对象负责管理以hello()函数作为初始函数的线程,join()等待线程函数执行完成——这儿是阻塞的。

这个最简单的Hello World已经包含了多线程编程的基本结构,至于线程管理、数据共享、线程同步、原子操作,以及一系列进阶操作都会在后面详细阐述。

std::thread::join

阻塞当前线程,直至 *this 所标识的线程完成其执行。*this 所标识的线程的完成同步于从 join() 的成功返回。

参数
(无)

返回值
(无)

后置条件
joinable 为 false

异常
若错误发生则为 std::system_error 。

错误条件
若 this->get_id() == std::this_thread::get_id() (检测到死锁)则为 resource_deadlock_would_occur
若线程非法则为 no_such_process
若 joinable 为 false 则为 invalid_argument
示例

-----------------------------------------------
#include 
#include 
#include 

void foo()
{
	// 模拟线程耗时操作
	std::this_thread::sleep_for(std::chrono::seconds(1));
}
void bar()
{
	// 模拟线程耗时操作
	std::this_thread::sleep_for(std::chrono::seconds(1));
}
int main()
{
	std::cout << "starting first helper...\n";
	std::thread helper1(foo);

	std::cout << "starting second helper...\n";
	std::thread helper2(bar);

	std::cout << "waiting for helpers to finish..." << std::endl;
	helper1.join();
	helper2.join();

	std::cout << "done!\n";
}
-----------------------------------------------------
输出:
starting first helper...
starting second helper...
waiting for helpers to finish...
done!
请按任意键继续. . .

std::thread::detach

从 thread 对象分离执行的线程,允许执行独立地持续。一旦线程退出,则释放所有分配的资源。调用 detach 后, *this 不再占有任何线程。

参数
(无)

返回值
(无)

后置条件
joinable 为 false

异常
若 joinable() == false 或错误发生时为 std::system_error 。

示例
----------------------------------------------------
#include 
#include 
#include 

void independentThread()
{
	std::cout << "Starting concurrent thread.\n";
	std::this_thread::sleep_for(std::chrono::seconds(2));
	std::cout << "Exiting concurrent thread.\n";
}
void threadCaller()
{
	std::cout << "Starting thread caller.\n";
	std::thread t(independentThread);
	t.detach();
	std::this_thread::sleep_for(std::chrono::seconds(1));
	std::cout << "Exiting thread caller.\n";
}

int main()
{
	threadCaller();
	std::this_thread::sleep_for(std::chrono::seconds(5));
}
----------------------------------------------------
输出:
Starting thread caller.
Starting concurrent thread.
Exiting thread caller.
Exiting concurrent thread.
请按任意键继续. . .

std::thread::joinable

检查 thread 对象是否标识活跃的执行线程。具体是返回 true if get_id() != std::thread::id() 。故默认构造的 thread 不可合并。完成执行代码,但未被合并的线程仍被认为是活跃线程,从而可合并。

参数
(无)

返回值
若 thread 对象标识活跃的执行线程则为 true ,否则为 false

示例
--------------------------------------
#include 
#include 
#include 
 
void foo()
{
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
int main()
{
    std::thread t;
    std::cout << "before starting, joinable: " << std::boolalpha << t.joinable()
              << '\n';
 
    t = std::thread(foo);
    std::cout << "after starting, joinable: " << t.joinable() 
              << '\n';
 
    t.join();
    std::cout << "after joining, joinable: " << t.joinable() 
              << '\n';
}
--------------------------------------
输出:
before starting, joinable: false
after starting, joinable: true
after joining, joinable: false
请按任意键继续. . .

创建线程

上文中的经典hello world例子使用了最基本的线程创建方法,也是我们最常用的方法。std::thread对象的构造参数需要为Callable Object,可以是函数、函数对象、类的成员函数或者是Lambda表达式。接下来我们会给出这四种创建线程的方法。

以函数作为参数

上文中的Hello C++ Concurrency程序,就是最好的以函数为参数构造std::thread的例子,这里不再赘述。

以函数对象作为参数

函数对象利用了C++类的调用重载运算符,实现了该重载运算符的类对象可以当成函数一样进行调用。如下例:

#include 
#include 

class hello
{
public:
	hello() { }
	void operator()()const
	{
		std::cout << "Hello world" << std::endl;
	}
};

int main()
{
	hello h;
	std::thread t1(h);
	t1.join();
	return 0;
}

----------------------------------
Hello world
请按任意键继续. . .

这里需要注意一点:如果需要直接传递临时的函数对象,C++编译器会将std::thread对象构造解析为函数声明:

std::thread t2(hello()); // error, compile as std::thread t2(hello(*)()); 
std::thread t3((hello())); // ok
std::thread t4{ hello() }; // ok
t2.join();   // compile error: expression must have class type
t3.join();   // ok
t4.join();   // ok

以类的成员函数作为参数

为了作为std::thread的构造参数,类的成员函数名必须唯一,在下例中,如果world1()和world2()函数名都是world,则编译出错,这是因为名字解析发生在参数匹配之前。

#include 
#include 
#include 

class hello
{
public:
	hello() { }
	void world1()
	{
		std::cout << "Hello world" << std::endl;
	}
	void world2(std::string text)
	{
		std::cout << "Hello world, " << text << std::endl;
	}
};

int main()
{
	hello h;
	std::thread t1(&hello::world1, &h);//必须使用&,表明是同一个对象
	std::thread t2(&hello::world2, &h, "lee");
	t1.join();
	t2.join();
	return 0;
}
-------------------------------------
Hello world
Hello world, lee
请按任意键继续. . .

or

Hello worldHello world, lee

请按任意键继续. . .
or。。。。。

以lambda对象作为参数

#include 
#include 
#include 

int main()
{
	std::thread t([](std::string text){std::cout << "hello world, " << text << std::endl;}, "lee");
	t.join();
	return 0;
}
-------------------------------
hello world, lee
请按任意键继续. . .

创建线程对象时需要切记,使用一个能访问局部变量的函数去创建线程是一个糟糕的主意。


等待线程

join()等待线程完成,只能对一个线程对象调用一次join(),因为调用join()的行为,负责清理线程相关内容,如果再次调用,会出现Runtime Error。对join()的调用,需要选择合适的调用时机。如果线程运行之后父线程产生异常,在join()调用之前抛出,就意味着这次调用会被跳过。解决办法是,在无异常的情况下使用join()——在异常处理过程中调用join()

#include 
#include 
#include 

int main()
{
	std::thread t([](std::string text) {
		std::cout << "hello world, " << text << std::endl;
	}, "lee");
	try
	{
		throw std::exception("test");
	}
	catch (std::exception e)
	{
		std::cout << e.what() << std::endl;
		t.join();
	}
	if (t.joinable())
	{
		t.join();
	}
	return 0;
}

上面并非解决这个问题的根本方法,如果其他问题导致程序提前退出,上面方案无解,最好的方法是所谓的RAII(“Resource Acquisition is Initialization”C++中的RAII介绍)

#include 
#include 
#include 

class thread_guard
{
public:
	explicit thread_guard(std::thread &_t)
		: t(std::move(_t))
	{
		if (!t.joinable())
			throw std::logic_error("No Thread");
	}

	~thread_guard()
	{
		if (t.joinable())
		{
			t.join();
		}
	}
	thread_guard(thread_guard const&) = delete;
	thread_guard& operator=(thread_guard const &) = delete;
private:
	std::thread t;
};

void func()
{
	thread_guard guard(std::thread([](std::string text) {
		std::cout << "hello world, " << text << std::endl;
	}, "lee"));
	try
	{
		throw std::exception("test");
	}
	catch (...)
	{
		throw;
	}
}

int main()
{
	try
	{
		func();
	}
	catch (std::exception e)
	{
		std::cout << e.what() << std::endl;
	}
	return 0;
}
------------------------------------
hello world, lee
test
请按任意键继续. . .

分离线程

detach()将子线程和父线程分离。分离线程后,可以避免异常安全问题,即使线程仍在后台运行,分离操作也能确保std::terminate在std::thread对象销毁时被调用。

通常称分离线程为守护线程(deamon threads),这种线程的特点就是长时间运行;线程的生命周期可能会从某一个应用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进行优化。

#include 
#include 
#include 
#include 
#include 

int main()
{
	std::thread t([](std::string text) {
		std::cout << "hello world, " << text << std::endl;
		std::this_thread::sleep_for(std::chrono::seconds(2));
	}, "lee");

	if (t.joinable())
	{
		t.detach();
	}
	assert(!t.joinable());
	std::this_thread::sleep_for(std::chrono::seconds(5));

	return 0;
}
---------------------------------------------------
hello world, lee
请按任意键继续. . .

上面的代码中使用到了joinable()函数,不能对没有执行线程的std::thread对象使用detach(),必须要使用joinable()函数来判断是否可以加入或分离。


引用来源:

C++11 并发与多线程(一)

cppreference.com

C++并发编程0 - 欢迎来到多线程的世界

 

你可能感兴趣的:(C++笔记)