
在这个多核时代,如何充分利用每个 CPU 内核是一个绕不开的话题,从需要为成千上万的用户同时提供服务的服务端应用程序,到需要同时打开十几个页面,每个页面都有几十上百个链接的 web 浏览器应用程序,从保持着几 t 甚或几 p 的数据的系统,到手机上的一个有良好用户响应能力的 app,为了充分利用每个 CPU 内核,都会想到是否可以使用多线程技术。这里所说的“充分利用”包含了两个层面的意思,一个是使用到所有的内核,再一个是内核不空闲,不让某个内核长时间处于空闲状态。在 C++98 的时代,C++标准并没有包含多线程的支持,人们只能直接调用操作系统提供的 SDK API 来编写多线程程序,不同的操作系统提供的 SDK API 以及线程控制能力不尽相同,到了 C++11,终于在标准之中加入了正式的多线程的支持,从而我们可以使用标准形式的类来创建与执行线程,也使得我们可以使用标准形式的锁、原子操作、线程本地存储 (TLS) 等来进行复杂的各种模式的多线程编程,而且,C++11 还提供了一些高级概念,比如 promise/future,packaged_task,async 等以简化某些模式的多线程编程。
多线程可以让我们的应用程序拥有更加出色的性能,同时,如果没有用好,多线程又是比较容易出错的且难以查找错误所在,甚至可以让人们觉得自己陷进了泥潭,希望本文能够帮助您更好地使用 C++11 来进行 Linux 下的多线程编程。
首先我们应该正确地认识线程。维基百科对线程的定义是:线程是一个编排好的指令序列,这个指令序列(线程)可以和其它的指令序列(线程)并行执行,操作系统调度器将线程作为最小的 CPU 调度单元。在进行架构设计时,我们应该多从操作系统线程调度的角度去考虑应用程序的线程安排,而不仅仅是代码。
当只有一个 CPU 内核可供调度时,多个线程的运行示意如下:

我们可以看到,这时的多线程本质上是单个 CPU 的时间分片,一个时间片运行一个线程的代码,它可以支持并发处理,但是不能说是真正的并行计算。
当有多个 CPU 或者多个内核可供调度时,可以做到真正的并行计算,多个线程的运行示意如下:

从上述两图,我们可以直接得到使用多线程的一些常见场景:
需要注意一点,因为单个 CPU 内核下多个线程并不是真正的并行,有些问题,比如 CPU 缓存不一致问题,不一定能表现出来,一旦这些代码被放到了多核或者多 CPU 的环境运行,就很可能会出现“在开发测试环境一切没有问题,到了实施现场就莫名其妙”的情况,所以,在进行多线程开发时,开发与测试环境应该是多核或者多 CPU 的,以避免出现这类情况。
假设类a的一个成员是是一个指向类b的指针,在类a的头文件中使用了类b的前置声明并便宜成功,那么在a的实现中我们需要访问b的具体成员,因此需要包含头文件,那么我们应该在类a的实现部分(cpp文件)包含类b的头文件而非声明部分(h 文件)。假设类a的一个成员是是一个指向类b的指针,在类a的头文件中使用了类b的前置声明并便宜成功,那么在a的实现中我们需要访问b的具体成员,因此需要包含头文件,那么我们应该在类a的实现部分(cpp文件)包含类b的头文件而非声明部分(h文件)。不要在头文件中再包含其它头文件,除非缺少了它们就不能编译相反,要一个一个地声明所需要的类,让使用这个头文件的用户自己去包含其它的头文件。
和有些语言中定义的线程不同,C++11 所定义的线程是和操作系的线程是一一对应的,也就是说我们生成的线程都是直接接受操作系统的调度的,通过操作系统的相关命令(比如 ps -M 命令)是可以看到的,一个进程所能创建的线程数目以及一个操作系统所能创建的总的线程数目等都由运行时操作系统限定。
native_handle_type 是连接 thread 类和操作系统 SDK API 之间的桥梁,在 g++(libstdc++) for Linux 里面,native_handle_type 其实就是 pthread 里面的 pthread_t 类型,当 thread 类的功能不能满足我们的要求的时候(比如改变某个线程的优先级),可以通过 thread 类实例的 native_handle() 返回值作为参数来调用相关的 pthread 函数达到目的。thread::id 定义了在运行时操作系统内唯一能够标识该线程的标识符,同时其值还能指示所标识的线程的状态,其默认值 (thread::id()) 表示不存在可控的正在执行的线程(即空线程,比如,调用 thead() 生成的没有指定入口函数的线程类实例),当一个线程类实例的 get_id() 等于默认值的时候,即 get_id() == thread::id(),表示这个线程类实例处于下述状态之一:
空线程 id 字符串表示形式依具体实现而定,有些编译器为 0x0,有些为一句语义解释。
有时候我们需要程执行代码里面对当前调用者线程进行操作,针对这种情况,C++11 里面专门定义了一个名字空间 this_thread,其中包括 get_id() 函数可用来获取当前调用者线程的 id,yield() 函数可以用来将调用者线程跳出运行状态,重新交给操作系统进行调度,sleep_until 和 sleep_for 函数则可以让调用者线程休眠若干时间。get_id() 函数实际上是通过调用 pthread_self() 函数获得调用者线程的标识符,而 yield() 函数则是通过调用操作系统 API sched_yield() 进行调度切换。
和 pthread_create 不同,使用 thread 类创建线程可以使用一个函数作为入口,也可以是其它的 Callable 对象,而且,可以给入口传入任意个数任意类型的参数:
我们也可以传入一个Lambda表达式作为入口,比如:

一个类的成员函数也可以作为线程入口:
cout<<"create "<<anything<<endl;
虽然 thread 类的初始化可以提供这么丰富和方便的形式,其实现的底层依然是创建一个 pthread 线程并运行之,有些实现甚至是直接调用 pthread_create 来创建。
创建一个线程之后,我们还需要考虑一个问题:该如何处理这个线程的结束?一种方式是等待这个线程结束,在一个合适的地方调用 thread 实例的 join() 方法,调用者线程将会一直等待着目标线程的结束,当目标线程结束之后调用者线程继续运行;另一个方式是将这个线程分离,由其自己结束,通过调用 thread 实例的 detach() 方法将目标线程置于分离模式。一个线程的 join() 方法与 detach() 方法只能调用一次,不能在调用了 join() 之后又调用 detach(),也不能在调用 detach() 之后又调用 join(),在调用了 join() 或者 detach() 之后,该线程的 id 即被置为默认值(空线程),表示不能继续再对该线程作修改变化。如果没有调用 join() 或者 detach(),那么,在析构的时候,该线程实例将会调用 std::terminate(),这会导致整个进程退出,所以,如果没有特别需要,一般都建议在生成子线程后调用其 join() 方法等待其退出,这样子最起码知道这些子线程在什么时候已经确保结束。
在 C++11 里面没有提供 kill 掉某个线程的能力,只能被动地等待某个线程的自然结束,如果我们要主动停止某个线程的话,可以通过调用 Linux 操作系统提供的 pthread_kill 函数给目标线程发送信号来实现,示例如下:
// 确保子线程已经在运行posix 线程池。
cout<<"thread destroyed."<<endl;
1.不能操作线程和线程api(线程api指非线程对象的方法如notify,wait等),2.不能操作awt,3.不能实现服务器功能,4.不能对静态属生存取,5.不能使用io操作直接存取文件系统,6.不能加载本地库.,7.不能将this作为变量和返回,8.不能循环调用。当操作2完成时,我们立即在第2个线程中调用它的完成处理程序(当线程1在忙着响应操作1的处理程序时,线程2可以自由处理任何新进来的操作)。为了区别是否是创建控件的线程访问该控件,windows应用程序中每一个控件对象都有一个invokerequired属性,用来检查是否需要通过调用invoke方法完成其他线程对该控件的操作,如果该属性为true.说明是其他线程操作该控件,这时可以创建一个委托实例,然后调用控件对象的invoke方法,并传入需要的参数完成相应操作,否则可以直接对该控件对象进行操作,从而保证了其他线程安全操作本线程中的控件.。
内核线程(kernel thread, klt)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。在用户空间模拟操作系统对进程的调度,来调用一个进程中的线程,每个进程中都会有一个运行时系统,用来调度线程。在引入线程的操作系统中,线程是进程中的一个实体,是被系统独立调度和分派的基本单位。
而如果是return,编译器将使其调用进程退出的代码(如_exit()),从而导致进程及其所有线程结束运行。7.对进程中的各个线程来说,exitprocess和terminateprocess都将导致对线程的terminatethread调用,因此进程的main函数结束前,尽量确保工作线程都正常退出。主线程如果先结束,默认调用exit函数,exit会结束整个进程,函数线程也会随之结束,那这并不是我们想要看到的结果,我们的函数线程的工作还没有完,就强行结束,这是万万不能的。
线程实例互换使用 swap 函数,示例如下:
互换和转移很类似,但是互换仅仅进行实例(以 id 作标识)的互换,而转移则在进行实例标识的互换之前,还进行了转移目的实例(如下例的t2)的清理,如果 t2 是可聚合的(joinable() 方法返回 true),则调用 std::terminate(),这会导致整个进程退出,比如下面这个例子:
cout<<"should not reach here"<<endl;
所以,在进行线程实例转移的时候,要注意判断目的实例的 id 是否为空值(即 id())。
13.9 示例:类string的拷贝构造函数和拷贝赋值函数。第二点比较好理解,第三点是因为内联函数在编译器处理阶段会进行内联替换,将内联函数的代码直接拷贝替换,此时的内联函数将作为调用其的内部代码,而不是一个真正意义上的函数,所以其没有函数地址,也就无法进行vtable的构造,所以将其设置为虚是不会起作用的。cppcheck扫描规则_图文没有调用 positioning 函数 内存泄露 常见的 realloc 错误:调用失败的空值没有释放 使用 memset 类包含一个虚拟方法是不安全的,因为构 造函数、 析构函数和拷贝...。
// disable copy constructors
因为 thread 类的析构函数不是虚析构函数,在上例中,需要避免出现下面这种情况:

MyThread* tc = new MyThread(…);
…
thread* tp = tc;
…
delete tp;
这种情况会导致 MyThread 的析构函数没有被调用。
我们可以调用 this_thread::yield() 将当前调用者线程切换到重新等待调度,但是不能对非调用者线程进行调度切换,也不能让非调用者线程休眠(这是操作系统调度器干的活)。
unsignedintprocs=thread::hardware_concurrency(),// 获取物理线程数目
ta 线程因为需要经常切换去重新等待调度,它运行的时间要比 tb 要多,比如在作者的机器上运行得到如下结果:
本文来自电脑杂谈,转载请注明本文网址:
https://www.pc-fly.com/a/jisuanjixue/article-100741-1.html
英国已经与我国和好
离开制造业这个国民经济的基石
图片上的虫明显是未加工过的活虫