C++的并发世界(一)——线程管理

0.守护线程

每个程序至少有一个线程:执行main()函数的线程,其余线程有其各自的入口函数。线程与原 始线程(以main()为入口函数的线程)同时运行。如同main()函数执行完会退出一样,当线程执 行完入口函数后,线程也会退出。在为一个线程创建了一个 std::thread 对象后,需要等待这 个线程结束;不过,线程需要先进行启动。在一些极端情况下,线程运行时,任务中的函数对象需要 通过某种通讯机制进行参数的传递,或者执行一系列独立操作;可以通过通讯机制传递信号, 让线程停止。总之,使用C++线程库启 动线程,可以归结为构造 std::thread 对象。如果 std::thread 对象销毁之前还没有做出决定,程序就会终止 ( std::thread 的析构函数会调用 std::terminate() )。因此,即便是有异常存在,也需要确保线程能够正确的加入(joined)或分离(detached)。如果不等待线程,就必须保证线程结束之前,可访问的数据得有效性。这不是一个新问题 ——单线程代码中,对象销毁之后再去访问,也会产生未定义行为——不过,线程的生命周 期增加了这个问题发生的几率。 这种情况很可能发生在线程还没结束,函数已经退出的时候,这时线程函数还持有函数局部变量的指针或引用。

如果需要等待线程,相关的 std::tread 实例需要使用join()。join()是简单粗暴的等待线程完成或不等待。当你需要对等待中的线程有更灵活的控制时,比 如,看一下某个线程是否结束,或者只等待一段时间(超过时间就判定为超时)。想要做到这 些,你需要使用其他机制来完成,比如条件变量和期待(futures),。这意味着,只能对一个线程使用一次join();一旦已经使用过 join(), std::thread 对象就不能再次加入了,当对其使用joinable()时,将返回否(false)。如果想要分离一 个线程,可以在线程启动后,直接使用detach()进行分离。如果打算等待对应线程,则需要细 心挑选调用join()的位置。当在线程运行之后产生异常,在join()调用之前抛出,就意味着很这 次调用会被跳过。 避免应用被抛出的异常所终止,就需要作出一个决定。通常,当倾向于在无异常的情况下使 用join()时,需要在异常处理过程中调用join(),从而避免生命周期的问题。使用detach()会让线程在后台运行,这就意味着主线程不能与之产生直接交互。也就是说,不 会等待这个线程结束;如果线程分离,那么就不可能有 std::thread 对象能引用它,分离线程的确在后台运行,所以分离线程不能被加入。不过C++运行库保证,当线程退出时,相关资源 的能够正确回收,后台线程的归属和控制C++运行库都会处理。

通常称分离线程为守护线程(daemon threads),UNIX中守护线程是指,且没有任何用户接口, 并在后台运行的线程。这种线程的特点就是长时间运行;线程的生命周期可能会从某一个应 用起始到结束,可能会在后台监视文件系统,还有可能对缓存进行清理,亦或对数据结构进 行优化。另一方面,分离线程的另一方面只能确定线程什么时候结束,"发后即忘"(fire and forget)的任务就使用到线程的这种方式。不能对没有执行线程 的 std::thread 对象使用detach(),也是join()的使用条件,并且要用同样的方式进行检查—— 当 std::thread 对象使用t.joinable()返回的是true,就可以使用detach()。

1.向线程函数传递参数

默认参数要拷贝到线程独立内存中,即使参数是引用的形式,也可以在新线程中进行访问。

#include <thread>
#include <iostream>
#include <string>

const void test(const int i, const std::string str)
{
	std::cout << str << " is " << i << std::endl;
}

void main()
{
	std::thread task(test, 4, "the number");
	task.join();
}

当显式使用 std::move() 创建t2后②,t1的所有权就转移给了t2。之后,t1和执行线程已经没 有关联了;执行some_function的函数现在与t2关联。

void some_function();
void some_other_function();
std::thread t1(some_function); // 1
std::thread t2=std::move(t1); // 2
t1=std::thread(some_other_function); // 3
std::thread t3; // 4
t3=std::move(t2); // 5
t1=std::move(t3); // 6 赋值操作将使程序崩溃

t3使用默认构造方式创建④,与任何执行线程都没有关联。调用 std::move() 将与t2关联线程 的所有权转移到t3中⑤,显式的调用 std::move() ,是因为t2是一个命名对象。移动操作⑤完 成后,t1与执行some_other_function的线程相关联,t2与任何线程都无关联,t3与执行 some_function的线程相关联。 最后一个移动操作,将执行some_function线程的所有权转移⑥给t1。这时,t1已经有了一个 关联的线(执行some_other_function的线程),所以这里可以直接调用 std::terminate() 终止 程序继续运行。终止操作将调用 std::thread 的析构函数,销毁所有对象(与C++中异常的处 理方式很相似)。2.1.1节中,需要在线程对象被析构前,显式的等待线程完成,或者分离它; 进行复制时也需要满足这些条件(说明:不能通过赋一个新值给 std::thread 对象的方式来"丢弃"一个线程)。

std::thread::hardware_concurrency() 在新版C++标准库中是一个很有用的函数。这个函数将 返回能同时并发在一个程序中的线程数量。例如,多核系统中,返回值可以是CPU核芯的数 量。返回值也仅仅是一个提示,当系统信息无法获取时,函数也会返回0。但是,这也无法掩盖这个函数对启动线程数量的帮助。

线程标识类型是 std::thread::id ,可以通过两种方式进行检索。第一种,可以通过调 用 std::thread 对象的成员函数 get_id() 来直接获取。如果 std::thread 对象没有与任何执 行线程相关联, get_id() 将返回 std::thread::type 默认构造值,这个值表示“没有线程”。第 二种,当前线程中调用 std::this_thread::get_id() (这个函数定义在 头文件中)也可 以获得线程标识。

 

猜你喜欢

转载自blog.csdn.net/qq_35789421/article/details/114597204