学习笔记:C++ 多线程(1)

本次学习主要是为了记录自己学习C++多线程的过程。

线程管理的基础

开启线程

线程在 std::thread 对象创建(为线程指定任务)时启动。所创建的任务通常是无参数无返回(void-returning)的函数。这种函数在其所属线程上运行,直到函数执行完毕,线程也就结束了。在一些极端情况下,线程运行时,任务中的函数对象需要通过某种通讯机制进行参数的传递,或者执行一系列独立操作;可以通过通讯机制传递信号,让线程停止
构造线程std::thread对象:

void do_some_work_();
std::thread my_thread(do_some_work);
class background_task
{
public:
	void operator()() const
	{
		do_something();
		do_something_else();
	}
};
background_task f;
std::thread my_thread(f);

代码中提供的函数对象会复制到新线程的存储空间中,函数对象的执行和调用都在线程的内存空间中进行。函数对象的副本应与元吃函数对象保持一致,否则得到的结果会与我们的期望不同

当我们把函数对象传入到线程构造函数中时。如果我们传递一个临时变量额,而不是一个命名的变量,C++编译器会将其解析为函数声明,而不是类型对象的定义。

eg:

std::thread my_thread_(background_task());

这里相当于声明了一个名为my_thread的函数,这个函数带有一个参数(函数指针指向没有参数并返回background_task对象的函数)。

当然我们有方法避免这种情况的,我们可以使用多组括号,或使用新统一的歘实话语法,可以避免这个问题。
eg:

//方式一
std::thread my_thread((background_task()));
方式二
std::thread my_thread{(background_task())};

我们也可以使用lambda表达式来解决这个问题 。lambda表达式是C++的一个新特性,它允许使用一个可以捕捉局部变量的局部函数。

std::thread my_thread_([]{
	do_something();
	do_something_task();
});

启动了线程,你需要明确是要等待线程结束(加入式),还是让其自主运行(分离式)。如果std::thread对象销毁之前还没有做出决定,程序就会终止(std::thread的析构函数就会调用std::terminate())。因此,即便有异常存在,也需要确保线程能够正确的加入(joined)或分离(detached)。需要注意的是,必须在 std::thread 对象销毁之前做出决定——加入或分离线程之前。如果线程就已经结束,想再去分离它,线程可能会在 std::thread 对象销毁之后继续运行下去。

这种情况很可能发生在线程还没结束,函数已经退出的时候,这时线程函数还持有函数局部变量的指针或引
用。下面的清单中就展示了这样的一种情况。
eg:

struct func
{
    int& i;
    func(int& i_) : i(i_) {}
    void operator() ()
    {
        for (unsigned j=0 ; j<1000000 ; ++j)
        {
            do_something(i); // 1. 潜在访问隐患:悬空引用
        } }
};
void oops()
{
    int some_local_state=0;
    func my_func(some_local_state);
    std::thread my_thread(my_func);
    my_thread.detach(); // 2. 不等待线程结束
} // 3. 新线程可能还在运行

这个例子中,已经决定不等待线程结束(使用了detach()②),所以当oops()函数执行完成时③,新线程中的函数可能还在运行。如果线程还在运行,它就会去调用do_something(i)函数①,这时就会访问已经销毁的变量。如同一个单线程程序——允许在函数完成后继续持有局部变量的指针或引用;当然,这从来就不是一个好主意——这种情况发生时,错误并不明显,会使多线程更容易出错。

处理这种情况的常规方法:使线程函数的功能齐全,将数据复制到线程中,而非复制到共享数据中。如果使用一个可调用的对象作为线程函数,这个对象就会复制到线程中,而后原始对象就会立即销毁。但对于对象中包含的指针和引用还需谨慎,例如这个例子。使用一个能访问局部变量的函数去创建线程是一个糟糕的主意(除非十分确定线程会在函数完成前结束)。此外,可以通过加入的方式来确保线程在函数完成前结束。

等待线程完成

如果需要等待线程,相关的std::thread实例需要使用过join()。在上一个例子中,我们将my_thread.detach()替换成my_thread.join(),就可以确保局部变量在县城完成后,才被销毁。在这种情况下,因为原线程在其生命周期并没有做什么事,是的用一个独立的线程去执行函数变得收益甚微,但在实际编程中,原始线程要么有自己的工作要做:要么hi启动多个子线程来做一些有用的工作,等待这些线程结束。

join()是简单粗暴的等待线程完成或不等待。当你需要对等待中的线程有更灵活的控制时,比如,看一下某个线程是否结束,或者只等待一段时间(超过时间就判定为超时)。想要做到这些,你需要使用其他机制来完成,比如条件变量和期待(futures)。调用join()的行为,还清理了线程相关的存储部分,这样 std::thread 对象将不再与已经完成的线程有任何关联。这意味着,只能对一个线程使用一次join();一旦已经使用过join(),std::thread 对象就不能再次加入了,当对其使joinable()时,将返回否(false)

特殊情况下的等待

如前所述,需要对一个还未销毁std::thread 对象使用join()或detach()。如果想要分离一个线程,可以在线程启动后,直接使用detach()进行分离。如果打算等待对应线程,则需要细心挑选调用join()的位置。当在线程运行之后产生异常,在join()调用之前抛出,就意味着很这次调用会被跳过。避免应用被抛出的异常所终止,就需要作出一个决定。通常,当倾向于在无异常的情况下使用join()时,需要在异常处理过程中调用join(),从而避免生命周期的问题。下面的程序清单是一个例子

struct func; // 完整到吗在上一个例子
void f()
{
	int some_local_state=0;
	func my_func(some_local_state);
	std::thread t(my_func);
try
{
	do_something_in_current_thread();
}
catch(...)
{
	t.join(); // 1
	throw;
}
	t.join(); // 2
}

代码使用了 try/catch 块确保访问本地状态的线程退出后,函数才结束。当函数正常退出时,会执行到②处;当函数执行过程中抛出异常,程序会执行到①处。 try/catch 块能轻易的捕获轻量级错误,所以这种情况,并非放之四海而皆准。如需确保线程在函数之前结束——查看是否因为线程函数使用了局部变量的引用,以及其他原因——而后再确定一下程序可能会退出的途径,无论正常与否,可以提供一个简洁的机制,来做解决这
个问题。

后台运行线程

使用detach()会让线程在后台运行,这就意味着主线程不能与之直接交互,也就是说,不会等待这个线程结束;如果线程分离,那么就不可能有std::thread对象引用他,分离线程的却在后台运行,所以分离线程不能被加入。不过C++运行库保证,当线程退出时,相关资源的能够正确回收,后台线程的归属和控制C++运行库都会处理。

通常称分离线程为守护线程,UNIX中守护线程是指,且没有任何用户接口,并在后台运行的线程。这种线程的特点就是长时间运行。线程的生命周期可能会从某一方面,分离线程的另一方面智能确定线程什么时候结束。“发后即忘”(fire and forget)的任务就使用到线程的这种方式。

调用 std::thread 成员函数1detach()来分离一个线程。之后,相应的 std::thread 对象就与实际执行的线程无关了,并且这个线程也无法进行加入。
eg:

std::thread t(do_background_work);
t.detach();
assert(!t.joinable());

为了从 std::thread 对象中分离线程(前提是有可进行分离的线程):不能对没有执行线程的 std::thread 对象使用
detach(),也是join()的使用条件,并且要用同样的方式进行检查——当 std::thread 对象使用t.joinable()返回
的是true,就可以使用t.detach()。

试想如何能让一个文字处理应用同时编辑多个文档。无论是用户界面,还是在内部应用内部进行,都有很多的解决方法。虽然,这些窗口看起来是完全独立的,每个窗口都有自己独立的菜单选项,但他们却运行在同一个应用实例中。一种内部处理方式是,让每个文档处理窗口拥有自己的线程;每个线程运行同样的的代码,并隔离不同窗口处理的数据。如此这般,打开一个文档就要启动一个新线程。因为是对独立的文档进行操作,所以没有必要等待其他线程完成。因此,这里就可以让文档处理窗口运行在分离的线程上。

向线程函数传递参数

向 std::thread 构造函数中的可调用对象,或函数传递一个参数很简单。需要注意的是,默认参数要拷贝到线程独立内存中,即使参数是引用的形式,也可以在新线程中进行访问。

void f(int i, std::string const& s);
std::thread t(f, 3, "hello");

代码创建了一个调用f(3, “hello”)的线程。注意,函数f需要一个std::string对象作为第二个参数,但这里使用的字符串的字面值,也就是char const* 类型。之后,在线程的上下文中完成字面值向std::string对象的转化,需要特别注意,当指向动态变量的指针作为参数传递给线程的情况。
代码如下:

void f(int i, std::string const& s);
void oops(int some_param){
	char buffer[1024];	//1
	sprintf(buffer, "%i", some_param);
	std::thread t(f, 666, buffer)	//2
	t.detach();
}

这种情况下,buffer②是一个指针变量,指向本地的变量,然后本地变量通过buffer传递到新线程中②。并且,函数有很大的可能,会在字面值转化成std::string对象之前崩溃,从而导致线程的一些未定义行为。解决方案就是在传递到std::thread构造函数之前就将字面值转化为std::string对象。

void f(int i, std::string const& s);
void not_oops(int some_param){
	char buffer[1024];
	sprintf(buffer, "%i", some_param);
	std::thread t(f, 666, std::string(buffer));
	t.detach();
	
}

这种情况下的问题是,想要依赖隐式转换将字面值转换为函数期待的 std::string 对象,但因 std::thread 的构
造函数会复制提供的变量,就只复制了没有转换成期望类型的字符串字面值。

不过也有成功的情况:复制一个引用。成功的传递已给引用,会发生在线程更新数据结构时。

void update_data_for_widget(widget_id w,widget_data& data); // 1
void oops_again(widget_id w)
{
	widget_data data;
	std::thread t(update_data_for_widget,w,data); // 2
	display_status();
	t.join();
	process_widget_data(data); // 3
}

虽然update_data_for_widget①的第二个参数期待传入一个引用,但是 std::thread 的构造函数②并不知晓;构
造函数无视函数期待的参数类型,并盲目的拷贝已提供的变量。当线程调用update_data_for_widget函数时,传
递给函数的参数是data变量内部拷贝的引用,而非数据本身的引用。因此,当线程结束时,内部拷贝数据将会在
数据更新阶段被销毁,且process_widget_data将会接收到没有修改的data变量③。使用 std::bind ,就可以解决
这个问题,使用 std::ref 将参数转换成引用的形式。这种情况下,可将线程的调用,改成以下形式:

std::thread t(update_data_for_widget,w,std::ref(data));

在这之后,update_data_for_widget就会接收到一个data变量的引用,而非一个data变量拷贝的引用。
如果你熟悉 std::bind ,就应该不会对以上述传参的形式感到奇怪,因为 std::thread 构造函数和 std::bind 的操作都在标准库中定义好了,可以传递一个成员函数指针作为线程函数,并提供一个合适的对象指针作为第一个
参数:

class X
{
	public:
	void do_lengthy_work();
};
X my_x;
std::thread t(&X::do_lengthy_work,&my_x); // 1

这段代码中,新线程将my_x.do_lengthy_work()作为线程函数;my_x的地址①作为指针对象提供给函数。也可以为成员函数提供参数: std::thread 构造函数的第三个参数就是成员函数的第一个参数,以此类推(代码如下,译者自加)。

关键字的说明

join():阻塞主线程,等待其他子线程执行完毕,正常退出.

detach():会使线程失去我们的控制,但是如果希望子线程做的事情与我们控制与否不影响,那么就可以使用detach(),因为子线程会跑到后台运行.

joinable():用来判断是否能够调用join()或者detach(),可以返回true,不可以返回false.也就是调用过join就不能在后面调用detach,调用过detach就不能在后面调用join.

本偏博文,只是自己为了学习而记录的,主要知识来源是:
极客学院:https://wiki.jikexueyuan.com/project/cplusplus-concurrency-action/content/chapter2/2.1-chinese.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值