Linux C与C++一线开发实践之五 多线程基本编程

Posted 夜色魅影

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux C与C++一线开发实践之五 多线程基本编程相关的知识,希望对你有一定的参考价值。

在多核时代,如何充分利用每个CPU内核是一个绕不开的话题,从需要为成千上万的用户同时提供服务的服务端程序,到需要同时打开十几个页面,每个页面都有十几,上百个链接的Web应用程序,从需要支持并发访问的数据库系统,到手机上的一个有良好用户响应能力的App,为了充分利用每个CPU内核,都会想到是否可以使用多线程技术。
多核机器下,多线程一般可以让我们得应用程序拥有更加出色的性能,但是也不是无节制使用,对开的数量,数据的同步等都是需要注意的,比较容易出错且难以查找错误所在。过多的线程只会造成,更多的内存和CPU开销,但是对提升QPS确毫无帮助,使用多线程应该在正确的场景下通过设置正确个数的线程来充分的利用 CPU 和 I/O 最大化程序的运行速度。
实际某种意义上来看,多线程主要是运用在I/O密集程序的场景。

如何设置正确的线程数

从两个方面分析:

  • CPU 密集型程序
  • I/O 密集型程序

一、CPU 密集型程序:一个完整请求,I/O操作可以在很短时间内完成, CPU还有很多运算要处理,也就是说 CPU 计算的比例占很大一部分,线程等待时间接近0。

  1. 单核CPU,所有线程都在等待 CPU 时间片。按照理想情况来看,n个线程执行的时间总和与一个线程独自完成是相等的,但是实际上我们还忽略了n个线程上下文切换的开销。所以,对于单核CPU处理CPU密集型程序,这种情况并不太适合使用多线程。
  2. 多核CPU,比如四核四线程,每个线程都有 CPU 来运行,并不会发生等待 CPU 时间片的情况,也没有线程切换的开销。理论情况来看效率提升了 4 倍。因此理论上线程数量 = CPU 核数最合适,但是实际上,数量一般会设置为 CPU 核数 + 1。
    计算(CPU)密集型的线程恰好在某时因为发生一个页错误或者因其他原因而暂停,刚好有一个“额外”的线程,可以确保在这种情况下CPU周期不会中断工作。

所以对于CPU密集型程序,总的来说,最佳线程数就是等于CPU核心数(或者+1)。

二、I/O密集型程序:与 CPU 密集型程序相对,一个完整请求,CPU运算操作完成之后还有很多 I/O 操作要做,也就是说 I/O 操作占比很大部分,等待时间较长。在进行 I/O 操作时,CPU是空闲状态,所以我们要最大化的利用 CPU,不能让其是空闲状态。对于I/O密集型程序有前辈总结出一个公式:最佳线程数 = CPU核心数 * (1 + (I/O耗时/CPU耗时))。既如果我们的服务器CPU核数为4核,一个任务线程cpu耗时为20ms,线程等待(网络IO、磁盘IO)耗时80ms,那最佳线程数目:4 * ( 1 + 80 / 20 ) = 20,也就是设置20个线程数最佳。

总结
1、多线程不一定就比单线程高效,比如大名鼎鼎的 Redis ,因为它是基于内存操作,这种情况下,单线程可以很高效的利用CPU。而多线程的使用场景一般是存在相当比例的I/O或网络操作。
2、在开始没有任何数据之前,我们可以使用上文提到的经验值作为一个伪标准,最终还是得结合实际来逐步的调优(综合 CPU,内存,硬盘读写速度,网络状况等)
3、盲目的增加 CPU 核数也不一定能解决我们的问题,这就要求我们严格的编写并发程序代码。

C++多线程开发的两种方式

在Linux C++开发环境中,通常有两种方式来开发多线程程序,一种是利用POSIX多线程API函数来开发(包含头文件pthread.h,需要安装pthread库),另一种是直接用C++11自带的线程类来开发。
一个进程内的各子线程共享进程内的全局数据。

一:POSIX多线程API函数进行多线程开发

  1. 线程创建
/*
	pid:指向创建成功后的线程ID
	attr:指向线程属性结构体pthread_attr_t的指针,如果为NULL,则使用默认属性
	start_routing:函数指针,指向线程函数,线程创建后将要指向的函数
	arg:指向传给线程函数的参数
	return:如果执行成功返回0
*/
int pthread_create(pthread_t *pid, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg);

创建完子线程后,主线程会继续往后执行,这就有可能出现子线程还没执行完,主线程就先结束了,主线程结束就意味着进程结束了。在这种情况下,我们就需要让主线程等待,等待子线程全部运行结束后再继续执行。POSIX提供了函数pthread_join来等待子线程结束,即子线程执行完毕后,它才会返回,是一个阻塞函数。

/*
	pid:子线程的ID
	value_ptr:通常设置为NULL,如果不为NULL,则它能接收子线程退出后的返回值,*value_ptr指向这个值
	return: 执行成功返回0,否则返回错误码
*/
int pthread_join(pthread_t pid, void **value_ptr);

下面来看一个示例:创建一个线程,并传递结构体作为参数

// 需要安装pthread库
#include <pthread.h>
#include <stdio.h>
// 定义结构体类型
typedef struct

	int n;
	char *str;
 MYSTRUCT;

// 定义线程函数
void *thfunc(void *arg)

	MYSTRUCT *p = (MYSTRUCT*)arg;
	printf("in thfunc:n=%d, str=%s\\n", p->n, p->str);
	return (void*)0;


int main(int argc, char *argv[])

	pthread_t tidp;
	int ret;
	MYSTRUCT myStruct;
	myStruct.n = 110;
	myStruct.str = "hello world";

	ret = pthread_create(&tidp, NULL, thfunc, (void *)&myStruct);
	if(ret)
		printf("pthread_create failed:%d\\n", ret);
		return -1;
	
	pthread_join(tidp, NULL);  //可连续线程,只有调用了join,对应子线程返回后才会销毁
	printf("in main:thread is created\\n");
	return 0;

2.线程属性
线程属性主要包括分离状态,调度策略,栈尺寸,线程结束及清理等,这里细节就不深入写了,自行科普。有点要注意线程退出后一定要确保清理工作,这里很容易出现内存泄漏。

二: C++11中的线程类
这种方式使用线程其实接口和传统的POSIX多线程API接口差不多,常使用的列举如下:

/*
thread:构造函数,有4种thread(thfunc, arg...)
get_id:获取线程ID
joinable:判断线程对象是否可连接
join:阻塞函数,等待线程结束
...
*/

这块可自行std::thread中查看源码~原理和传统线程创建差不多。

以上是关于Linux C与C++一线开发实践之五 多线程基本编程的主要内容,如果未能解决你的问题,请参考以下文章

Linux C与C++一线开发实践之六 多线程高级编程

Linux C与C++一线开发实践之六 多线程高级编程

Linux C与C++一线开发实践之三 Linux多进程

Linux C与C++一线开发实践之三 Linux多进程

Linux C与C++一线开发实践之一 Linux概述与Linux C++开发

Linux C与C++一线开发实践之四 Linux进程间的通信