Linux 多线程高并发测试技巧详解 (linux 多线程高并发测试)

随着计算机硬件性能的提升和云计算技术的普及,现在越来越多的应用需要处理高并发的请求。为了保证应用在高并发下能够具备良好的性能和可靠性,开发人员通常需要进行多线程高并发测试。本文将从如何进行多线程高并发测试的角度出发,向大家介绍一些在 Linux 下进行多线程高并发测试的技巧和工具。

1. 多线程高并发测试的原理

多线程高并发测试的目的是模拟许多用户同时对应用进行访问,在负载增加的情况下测试应用的性能。多线程测试的基本原理是开启多个线程,每个线程模拟一个用户,并向应用发送请求。这样,就可以在并行的环境下测试应用的并发性能。

2. Linux 多线程高并发测试的工具

在 Linux 下进行多线程高并发测试,通常使用下面这些工具:

(1) Apache ab 工具

Apache ab 工具 (Apache HTTP server benchmarking tool) 是 Apache 基金会开发的一个简单而高效的 HTTP 压力测试工具。它的优点是使用简单,可以测试 Web 应用。在 Ubuntu 下可以通过 apt-get 命令进行安装。

(2) Siege 工具

Siege 工具是一款基于 HTTP 的高并发测试工具,它可以模拟大量用户访问 Web 应用,可以测试 Web 应用的性能和资源占用情况,比 Apache ab 工具更强大。在 Ubuntu 下可以通过 apt-get 命令进行安装。

(3) JMeter 工具

JMeter 是一款面向 Web 应用的自动化测试工具,它可以测试 Web 应用的性能和负载能力。它支持多线程测试,可以模拟大量的用户进行访问,支持图形化界面,使用起来比较简单。但是,它的功能相对较为复杂,在一些需求简单的场景中使用不太方便。

(4) wrk 工具

wrk 工具是目前较为热门的一款 HTTP 压力测试工具,它使用 C 语言编写,性能和精度比 Apache ab 工具和 Siege 工具更高,支持多线程测试,可以模拟高并发访问。与其它工具相比,wrk 速度更快,使用起来更简单。

3. Linux 多线程高并发测试的技巧

要进行多线程高并发测试,需要掌握一些技巧和注意事项。下面是一些 Linux 下进行多线程高并发测试的技巧:

(1) 合理设置测试环境

在进行多线程高并发测试前,需要设置好测试环境。需要合理设置服务器性能参数、调整内核参数、优化数据库配置等。

(2) 设置合理的压力测试参数

在进行多线程高并发测试时,需要设置测试参数。例如,测试请求数、并发请求数、请求的 URL 等。

(3) 设置较大的内存

在进行多线程高并发测试时,需要设置较大的内存。因为测试过程中需要存储很多数据,如果内存过小,就可能导致测试结果不准确。

(4) 模拟真实请求

在进行多线程高并发测试时,需要模拟真实的请求。例如,需要发送随机数据而不是固定的数据,以确保测试过程更具有代表性。

(5) 关闭不必要的服务

在进行多线程高并发测试时,需要关闭不必要的服务。例如,关闭 X 窗口管理器、关闭图形界面等,以减少系统资源的消耗。

4.

多线程高并发测试是现代应用开发中不可忽视的一个重要环节。本文向大家介绍了一些在 Linux 下进行多线程高并发测试的常用工具和技巧,希望能够对大家进行多线程高并发测试有所帮助。无论是开发人员还是测试人员,在进行多线程高并发测试时,都需要注意数据的真实性和环境的合理性,以确保测试的准确性和有效性。

相关问题拓展阅读:

浅谈linux 多线程编程和 windows 多线程编程的异同

很早以前就想写写linux下多线程编程和windows下的多线程编程了,但是每当写时又不知道从哪个地方写起,怎样把自己知道的东西都写出来,下面我就谈世罩让谈linux多线程及线程同步,并将它和windows的多线程进行比较,看看他们之间有什么相同点和不同的地方。

其实最开始我是搞windows下编程的,包括windows编程,windows 驱动,包括u驱动,ndis驱动,pci驱动,1394驱动等等,同时也一条龙服务,做windows下的应用程序开发,后面慢慢的我又对linux开发产生比较深的兴趣和爱好,就转到搞linux开发了。在接下来的我还会写一些博客,主要是写linux编程和windows编程的区别吧,现在想写的是linux下u驱动和windows下u驱动开发的区别,这些都是后话,等我将linux多线程和windows多线程讲解完后,我再写一篇u驱动,谈谈windows 和linux u驱动的东东。好了,言归正传。开始将多线程了。

首先我们讲讲为什么搜局要采用多线程编程,其实并不是所有的程序都必须采用多线程,有些时候采用多线程,性能还没有单线程好。所以我们要搞清楚,什么时候采用多线程。采用多线程的好处如下:

(1)因为多线程彼此之间采用相同的地址空间,共享大部分的数据,这样和多进程相比,代价比较节俭,因为多进程的话,启动新的进程必须分配给它独立的地址空间,这样需要数据表来维护代码段,数据段和堆栈段等等。

(2)多线程和多进程相比,一个明显的优点就是线程之间的通信了,对不同进程来说,它们具有独立的数据空间,要进行数据的传递只能通过通信的方式进行,这种方式不仅费时,而且很不方便。但是对于多线程就不一样了。他们之间可以直接共享数据,比如最简单的方式就是共享全局变量。但是共享全部变量也要注意哦,呵呵,必须注意同步,不然后果你知道的。呵呵。

(3)在多cpu的情况下,不同的线程可以运行不同的cpu下,这样就完全并行了。

反正我觉得在这种情况下,采用多线程比较理想。比如说你要做一个任务分2个步骤,你为提高工作效率,你可以多线程技术,开辟2个线程,之一个线程就做之一步的工作,第2个线程就做第2步的工作。但是你这个时候要注意同步了。因为只有之一步做完才能做第2步的工作。这时,我们可以采用同步技术进行线程之间的通信。

针对这种情况,我们首先讲讲多线程之间的通信,在windows平台下,多线程之间通信采用的方法主要有:

(1)共享全局变量,这种闷仔方法是最容易想到的,呵呵,那就首先讲讲吧,比如说吧,上面的问题,之一步要向第2步传递收据,我们可以之间共享全局变量,让两个线程之间传递数据,这时主要考虑的就是同步了,因为你后面的线程在对数据进行操作的时候,你之一个线程又改变了数据的内容,你不同步保护,后果很严重的。你也知道,这种情况就是读脏数据了。在这种情况下,我们最容易想到的同步方法就是设置一个bool flag了,比如说在第2个线程还没有用完数据前,之一个线程不能写入。有时在2个线程所需的时间不相同的时候,怎样达到更大效率的同步,就比较麻烦了。咱们可以多开几个缓冲区进行操作。就像生产者消费者一样了。如果是2个线程一直在跑的,由于时间不一致,缓冲区迟早会溢出的。在这种情况下就要考虑了,是不让数据写入还是让数据覆盖掉老的数据,这时候就要具体问题具体分析了。就此打住,呵呵。就是用bool变量控制同步,linux 和windows是一样的。

既然讲道了这里,就再讲讲其它同步的方法。同样 针对上面的这个问题,共享全局变量同步问题。除了采用bool变量外,最容易想到的方法就是互斥量了。呵呵,也就是传说中的加锁了。windows下加锁和linux下加锁是类似的。采用互斥量进行同步,要想进入那段代码,就先必须获得互斥量。

linux上互斥量的函数是:

windows下互斥量的函数有:createmutex 创建一个互斥量,然后就是获得互斥量waitforsingleobject函数,用完了就释放互斥量ReleaseMutex(hMutex),当减到0的时候 内核会才会释放其对象。下面是windows下与互斥的几个函数原型。

HANDLE WINAPI CreateMutex(

__in LPSECURITY_ATTRIBUTES lpMutexAttributes,

__in BOOL bInitialOwner,

__in LPCTSTR lpName

);

可以可用来创建一个有名或无名的互斥量对象

之一参数 可以指向一个结构体SECURITY_ATTRIBUTES 一般可以设为null;

第二参数 指当时的函数是不是感应感应状态 FALSE为当前拥有者不会创建互斥

第三参数 指明是否是有名的互斥对象 如果是无名 用null就好。

DWORD WINAPI WaitForSingleObject(

__in HANDLE hHandle,

__in DWORD dwMilliseconds

);

之一个是 创建的互斥对象的句柄。第二个是 表示将在多少时间之后返回 如果设为宏INFINITE 则不会返回 直到用户自己定义返回。

对于linux操作系统,互斥也是类似的,只是函数不同罢了。在linux下,和互斥相关的几个函数也要闪亮登场了。

pthread_mutex_init函数:初始化一个互斥锁;

pthread_mutex_destroy函数:注销一个互斥锁;

pthread_mutex_lock函数:加锁,如果不成功,阻塞等待;

pthread_mutex_unlock函数:解锁;

pthread_mutex_trylock函数:测试加锁,如果不成功就立即返回,错误码为EBUSY;

至于这些函数的用法,google上一搜,就出来了,呵呵,在这里不多讲了。windows下还有一个可以用来保护数据的方法,也是线程同步的方式

就是临界区了。临界区和互斥类似。它们之间的区别是,临界区速度快,但是它只能用来同步同一个进程内的多个线程。临界区的获取和释放函数如下:

EnterCriticalSection() 进入临界区; LeaveCriticalSection()离开临界区。 对于多线程共享内存的东东就讲到这里了。

(2)采用消息机制进行多线程通信和同步,windows下面的的消息机制的函数用的多的就是postmessage了。Linux下的消息机制,我用的较少,就不在这里说了,如果谁熟悉的,也告诉我,呵呵。

(3)windows下的另外一种线程通信方法就是事件和信号量了。同样针对我开始举得例子,2个线程同步,他们之间传递信息,可以采用事件(Event)或信号量(Semaphore),比如之一个线程完成生产的数据后,就必须告诉第2个线程,他已经把数据准备好了,你可以来取走了。第2个线程就把数据取走。呵呵,这里可以采用消息机制,当之一个线程准备好数据后,就直接postmessage给第2个线程,按理说采用postmessage一个线程就可以搞定这个问题了。呵呵,不是重点,省略不讲了。

对于linux,也有类似的方法,就是条件变量了,呵呵,这里windows和linux就有不同了。要特别讲讲才行。

对于windows,采用事件和信号量同步时候,都会使用waitforsingleobject进行等待的,这个函数的之一个参数是一个句柄,在这里可以是Event句柄,或Semaphore句柄,第2个参数就是等待的延迟,最终等多久,单位是ms,如果这个参数为INFINITE,那么就是无限等待了。释放信号量的函数为ReleaseSemaphore();释放事件的函数为SetEvent。当然使用这些东西都要初始化的。这里就不讲了。Msdn一搜,神马都出来了,呵呵。神马都是浮云!

对于linux操作系统,是采用条件变量来实现类似的功能的。Linux的条件变量一般都是和互斥锁一起使用的,主要的函数有:

pthread_mutex_lock ,

pthread_mutex_unlock,

pthread_cond_init

pthread_cond_signal

pthread_cond_wait

pthread_cond_timewait

为了和windows操作系统进行对比,我用以下表格进行比较:

对照以上表格,总结如下:

(1) Pthread_cleanup_push,Pthread_cleanup_pop:

这一对函数push和pop的作用是当出现异常退出时,做一些清除操作,即当在push和pop函数之间异常退出,包括调用pthread_exit退出,都会执行push里面的清除函数,如果有多个push,注意是是栈,先执行后面的那个函数,在执行前面的函数,但是注意当在这2个函数之间通过return 退出的话,执不执行push后的函数就看pop函数中的参数是不是为0了。还有当没有异常退出时,等同于在这里面return退出的情况,即:当pop函数参数不为0时,执行清除操作,当pop函数参数为0时,不执行push函数中的清除函数。

(2)linux的pthread_cond_signal和SetEvent的不同点

Pthread_cond_singal释放信号后,当没有Pthread_cond_wait,信号马上复位了,这点和SetEvent不同,SetEvent是不会复位的。详解如下:

条件变量的置位和复位有2种常用模型:之一种模型是当条件变量置位时(signaled)以后,如果当前没有线程在等待,其状态会保持为置位(signaled),直到有等待的线程进入被触发,其状态才会变为unsignaled,这种模型以采用Windows平台上的Auto-set Event 为代表。

第2种模型则是Linux平台的pthread所采用的模型,当条件变量置位(signaled)以后,即使当前没有任何线程在等待,其状态也会恢复为复位(unsignaled)状态。

条件变量在Linux平台上的这种模型很难说好坏,在实际应用中,我们可以对

代码稍加改进就可以避免这种差异的发生。由于这种差异只会发生在触发没有被线程等待在条件变量的时刻,因此我们只需要掌握好触发的时机即可。最简单的做法是增加一个计数器记录等待线程的个数,在决定触发条件变量前检查该变量即可。

示例 使用 pthread_cond_wait() 和 pthread_cond_signal()

pthread_mutex_t count_lock;

pthread_cond_t count_nonzero;

unsigned count;

decrement_count()

{

pthread_mutex_lock(&count_lock);

while (count == 0)

pthread_cond_wait(&count_nonzero, &count_lock);

count = count – 1;

pthread_mutex_unlock(&count_lock);

}

increment_count()

{

pthread_mutex_lock(&count_lock);

if (count == 0)

pthread_cond_signal(&count_nonzero);

count = count + 1;

pthread_mutex_unlock(&count_lock);

}

(3) 注意Pthread_cond_wait条件返回时互斥锁的解锁问题

extern int pthread_cond_wait __P ((pthread_cond_t *__cond,pthread_mutex_t *__mutex));

   调用这个函数时,线程解开mutex指向的锁并被条件变量cond阻塞。线程可以被函数pthread_cond_signal和函数 pthread_cond_broadcast唤醒线程被唤醒后,它将重新检查判断条件是否满足,如果还不满足,一般说来线程应该仍阻塞在这里,被等待被下一次唤醒。如果在多线程中采用pthread_cond_wait来等待时,会首先释放互斥锁,当等待的信号到来时,再次获得互斥锁,因此在之后要注意手动解锁。举例如下:

#include

#include

#include

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; /*初始化互斥锁*/

pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //初始化条件变量

void *thread1(void *);

void *thread2(void *);

int i=1;

int main(void)

{

pthread_t t_a;

pthread_t t_b;

pthread_create(&t_a,NULL,thread1,(void *)NULL);/*创建进程t_a*/

pthread_create(&t_b,NULL,thread2,(void *)NULL); /*创建进程t_b*/

pthread_join(t_b, NULL);/*等待进程t_b结束*/

pthread_mutex_destroy(&mutex);

pthread_cond_destroy(&cond);

exit(0);

}

void *thread1(void *junk)

{

for(i=1;i

{

printf(“IN one\n”);

pthread_mutex_lock(&mutex);//

if(i%3==0)

pthread_cond_signal(&cond);/*,发送信号,通知t_b进程*/

else

printf(“thead1:%d\n”,i);

pthread_mutex_unlock(&mutex);//*解锁互斥量*/

printf(“Up Mutex\n”);

sleep(3);

}

}

void *thread2(void *junk)

{

while(i

{

printf(“IN two \n”);

pthread_mutex_lock(&mutex);

if(i%3!=0)

pthread_cond_wait(&cond,&mutex);/*等待*/

printf(“thread2:%d\n”,i);

pthread_mutex_unlock(&mutex);

printf(“Down Mutex\n”);

sleep(3);

}

}

输出如下:

IN one

thead1:1

Up Mutex

IN two

IN one

thead1:2

Up Mutex

IN one

thread2:3

Down Mutex

Up Mutex

IN one

thead1:4

Up Mutex

IN two

IN one

thead1:5

Up Mutex

IN one

Up Mutex

thread2:6

Down Mutex

IN two

thread2:6

Down Mutex

IN one

thead1:7

Up Mutex

IN one

thead1:8

Up Mutex

IN two

IN one

Up Mutex

thread2:9

Down Mutex

注意蓝色的地方,有2个thread2:6,其实当这个程序多执行几次,i=3和i=6时有可能多打印几个,这里就是竞争锁造成的了。

(4)另外要注意的Pthread_cond_timedwait等待的是绝对时间,这个和WaitForSingleObject是不同的,Pthread_cond_timedwait在网上也有讨论。如下:这个问题比较经典,我把它搬过来。

thread_a :

pthread_mutex_lock(&mutex);

//do something

pthread_mutex_unlock(&mutex)

thread_b:

pthread_mutex_lock(&mutex);

//do something

pthread_cond_timedwait(&cond, &mutex, &tm);

pthread_mutex_unlock(&mutex)

有如上两个线程thread_a, thread_b,现在如果a已经进入了临界区,而b同时超时了,那么b会从pthread_cond_timedwait返回吗?如果能返回,那岂不是a,b都在临界区?如果不能返回,那pthread_cond_timedwait的定时岂不是就不准了?

大家讨论有价值的2点如下:

(1) pthread_cond_timedwait (pthread_cond_t *cv, pthread_mutex_t *external_mutex, const struct timespec *abstime) — This function is a time-based variant of pthread_cond_wait. It waits up to abstime amount of time for cv to be notified. If abstime elapses before cv is notified, the function returns back to the caller with an ETIME result, signifying that a timeout has occurred. Even in the case of timeouts, the external_mutex will be locked when pthread_cond_timedwait returns.

(2) 2.1 pthread_cond_timedwait行为和pthread_cond_wait一样,在返回的时候都要再次lock mutex.

2 .2pthread_cond_timedwait所谓的如果没有等到条件变量,超时就返回,并不确切。

如果pthread_cond_timedwait超时到了,但是这个时候不能lock临界区,pthread_cond_timedwait并不会立即返回,但是在pthread_cond_timedwait返回的时候,它仍在临界区中,且此时返回值为ETIMEDOUT。

关于pthread_cond_timedwait超时返回的问题,我也认同观点2。

附录:

 int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict_attr,void*(*start_rtn)(void*),void *restrict arg);

  返回值:若成功则返回0,否则返回出错编号

  返回成功时,由tidp指向的内存单元被设置为新创建线程的线程ID。attr参数用于制定各种不同的线程属性。新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无指针参数arg,如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg的参数传入。

linux下用C开发多线程程序,Linux系统下的多线程遵循POSIX线程接口,称为pthread。

由 restrict 修饰的指针是最初唯一对指针所指向的对象进行存取的方法,仅当第二个指针基于之一个时,才能对对象进行存取。对对象的存取都限定于基于由 restrict 修饰的指针表达式中。 由 restrict 修饰的指针主要用于函数形参,或指向由 malloc() 分配的内存空间。restrict 数据类型不改变程序的语义。 编译器能通过作出 restrict 修饰的指针是存取对象的唯一方法的假设,更好地优化某些类型的例程。

  之一个参数为指向线程标识符的指针。

  第二个参数用来设置线程属性。

  第三个参数是线程运行函数的起始地址。

  第四个参数是运行函数的参数。

因为pthread不是linux系统的库,所以在编译时注意加上-lpthread参数,以调用静态链接库。

终止线程:

如果在进程中任何一个线程中调用exit或_exit,那么整个进行会终止,线程正常的退出方式有:

(1) 线程从启动例程中返回(return)

(2) 线程可以被另一个进程终止(kill);

(3) 线程自己调用pthread_exit函数

#include

pthread_exit

线程等待:

int pthread_join(pthread_t tid,void **rval_ptr)

函数pthread_join用来等待一个线程的结束。函数原型为:

  extern int pthread_join __P (pthread_t __th, void **__thread_return);

之一个参数为被等待的线程标识符,第二个参数为一个用户定义的指针,它可以用来存储被等待线程的返回值。这个函数是一个线程阻塞的函数,调用它的函数将一直等待到被等待的线程结束为止,当函数返回时,被等待线程的资源被收回。

对于windows线程的创建东西,就不列举了,msdn上 一搜就出来了。呵呵。今天就讲到这里吧,希望是抛砖引玉,大家一起探讨,呵呵。部分内容我也是参考internet的,特此对原作者表示感谢!

如何看懂《Linux多线程服务端编程

一:进程和线程

每个进程有自己独立的地址空间。“在同一个进程”还是“不在同一个进程”是系统功能划分的重要决策点。《Erlang程序设计》把进程比喻为人:

每个人有自己的记忆(内存),人与人通过谈话(消息传递)来交流,谈话既可以是面谈姿并野(同一台服务器),也可以在里谈(不同的服务器,有网络通信)。面谈和谈的区别在于,面谈可以立即知道对方是否死了(crash,SIGCHLD),而谈只能通过周期性的心跳来判断对方是否还活着。

有了这些比喻,设计分布式系统时可以采取“角色扮演”,团队里的几个人各自扮演一个进程,人的角色由进程的代码决定(管登录的、管消息分发的、管买卖的等等)。每个人有自己的记忆,但不知道别人的记忆,要想知道别人的看法,只能通过交谈(暂不考虑共享内存这种IPC)。然后就可以思考:迹喊

·容错:万一有人突然死了

·扩容:新人中途加进来

·负载均衡:把甲的活儿挪给乙做

·退休:甲要修复bug,先别派新任务,等他做完手上的事情就把他重启

等等各种场景,十分便利。

线程的特点是共享地址空间,从而可以高效地共享数据。一台机器上的多个进程能高效地共享代码段(操作系统可以映射为同样的物理内存),但不能共享数据。如果多个进程大量共享内存,等于是把多进程程序当成多线程来写,掩耳盗铃。

“多线程”的价值,我认为是为了更好地发挥多核处理器(multi-cores)的效能。在单核时代,多线程没有多大价值(个人想法:如果要完成的任务是CPU密集型的,那多线程没有优势,甚至因为线程切换的开销,多线程反而更慢;如果要完成的任务既有CPU计算,又有磁盘或网络IO,则使用多线程的好处是,当某个线程因为IO而阻塞时,OS可以调度其他线程执行,虽然效率确实要比任务的顺序执行效率要高,然而,这种类型的任务,可以通过单线程的”non-blocking IO+IO multiplexing”的模型(事件驱动)来提高效率,采用多线程的方式,带来的可能仅仅是编程上的简单而已)。Alan Cox说过:”A computer is a state machine.Threads are for people who can’t program state machines.”(计算机是一台状态机。线程是给那些不能编写状态机程序的人准备的)如果只有一块CPU、一个执行单元,那么确实如Alan Cox所说,按状态机的思路去写程序是最蔽谨高效的。

二:单线程服务器的常用编程模型

据我了解,在高性能的网络程序中,使用得最为广泛的恐怕要数”non-blocking IO + IO multiplexing”这种模型,即Reactor模式。

在”non-blocking IO + IO multiplexing”这种模型中,程序的基本结构是一个事件循环(event loop),以事件驱动(event-driven)和事件回调的方式实现业务逻辑:

view plain copy

//代码仅为示意,没有完整考虑各种情况

while(!done)

{

int timeout_ms = max(1000, getNextTimedCallback());

int retval = poll(fds, nfds, timeout_ms);

if (retval0){

处理IO事件,回调用户的IO event handler

}

}

}

这里select(2)/poll(2)有伸缩性方面的不足(描述符过多时,效率较低),Linux下可替换为epoll(4),其他操作系统也有对应的高性能替代品。

Reactor模型的优点很明显,编程不难,效率也不错。不仅可以用于读写socket,连接的建立(connect(2)/accept(2)),甚至DNS解析都可以用非阻塞方式进行,以提高并发度和吞吐量(throughput),对于IO密集的应用是个不错的选择。lighttpd就是这样,它内部的fdevent结构十分精妙,值得学习。

基于事件驱动的编程模型也有其本质的缺点,它要求事件回调函数必须是非阻塞的。对于涉及网络IO的请求响应式协议,它容易割裂业务逻辑,使其散布于多个回调函数之中,相对不容易理解和维护。

三:多线程服务器的常用编程模型

大概有这么几种:

a:每个请求创建一个线程,使用阻塞式IO操作。在Java 1.4引人NIO之前,这是Java网络编程的推荐做法。可惜伸缩性不佳(请求太多时,操作系统创建不了这许多线程)。

b:使用线程池,同样使用阻塞式IO操作。与第1种相比,这是提高性能的措施。

c:使用non-blocking IO + IO multiplexing。即Java NIO的方式。

d:Leader/Follower等高级模式。

在默认情况下,我会使用第3种,即non-blocking IO + one loop per thread模式来编写多线程C++网络服务程序。

1:one loop per thread

此种模型下,程序里的每个IO线程有一个event loop,用于处理读写和定时事件(无论周期性的还是单次的)。代码框架跟“单线程服务器的常用编程模型”一节中的一样。

libev的作者说:

One loop per thread is usually a good model. Doing this is almost never wrong, some times a better-performance model exists, but it is always a good start.

这种方式的好处是:

a:线程数目基本固定,可以在程序启动的时候设置,不会频繁创建与销毁。

b:可以很方便地在线程间调配负载。

c:IO事件发生的线程是固定的,同一个TCP连接不必考虑事件并发。

Event loop代表了线程的主循环,需要让哪个线程干活,就把timer或IO channel(如TCP连接)注册到哪个线程的loop里即可:对实时性有要求的connection可以单独用一个线程;数据量大的connection可以独占一个线程,并把数据处理任务分摊到另几个计算线程中(用线程池);其他次要的辅助性connections可以共享一个线程。

比如,在dbproxy中,一个线程用于专门处理客户端发来的管理命令;一个线程用于处理客户端发来的MySQL命令,而与后端数据库通信执行该命令时,是将该任务分配给所有事件线程处理的。

对于non-trivial(有一定规模)的服务端程序,一般会采用non-blocking IO + IO multiplexing,每个connection/acceptor都会注册到某个event loop上,程序里有多个event loop,每个线程至多有一个event loop。

多线程程序对event loop提出了更高的要求,那就是“线程安全”。要允许一个线程往别的线程的loop里塞东西,这个loop必须得是线程安全的。

在dbproxy中,线程向其他线程分发任务,是通过管道和队列实现的。比如主线程accept到连接后,将表示该连接的结构放入队列,并向管道中写入一个字节。计算线程在自己的event loop中注册管道的读事件,一旦有数据可读,就尝试从队列中取任务。

2:线程池

不过,对于没有IO而光有计算任务的线程,使用event loop有点浪费。可以使用一种补充方案,即用blocking queue实现的任务队列:

view plain copy

typedef boost::functionFunctor;

BlockingQueue taskQueue; //线程安全的全局阻塞队列

//计算线程

void workerThread()

{

while (running) //running变量是个全局标志

{

Functor task = taskQueue.take(); //this blocks

task(); //在产品代码中需要考虑异常处理

}

}

// 创建容量(并发数)为N的线程池

int N = num_of_computing_threads;

for (int i = 0; i task = boost::bind(&Foo::calc,&foo);

taskQueue.post(task);

除了任务队列,还可以用BlockingQueue实现数据的生产者消费者队列,即T是数据类型而非函数对象,queue的消费者从中拿到数据进行处理。其实本质上是一样的。

3:总结

总结而言,我推荐的C++多线程服务端编程模式为:one (event) loop per thread + thread pool:

event loop用作IO multiplexing,配合non-blockingIO和定时器;

thread pool用来做计算,具体可以是任务队列或生产者消费者队列。

以这种方式写服务器程序,需要一个优质的基于Reactor模式的网络库来支撑,muduo正是这样的网络库。比如dbproxy使用的是libevent。

程序里具体用几个loop、线程池的大小等参数需要根据应用来设定,基本的原则是“阻抗匹配”(解释见下),使得CPU和IO都能高效地运作。所谓阻抗匹配原则:

如果池中线程在执行任务时,密集计算所占的时间比重为 P (0 就能立刻列出用到某服务的客户端地址(Foreign Address列),然后在客户端的机器上用netstat或lsof命令找出是哪个进程发起的连接。TCP短连接和UDP则不具备这一特性。二是通过接收和发送队列的长度也较容易定位网络或程序故障。在正常运行的时候,netstat打印的Recv-Q和Send-Q都应该接近0,或者在0附近摆动。如果Recv-Q保持不变或持续增加,则通常意味着服务进程的处理速度变慢,可能发生了死锁或阻塞。如果Send-Q保持不变或持续增加,有可能是对方服务器太忙、来不及处理,也有可能是网络中间某个路由器或交换机故障造成丢包,甚至对方服务器掉线,这些因素都可能表现为数据发送不出去。通过持续监控Recv-Q和Send-Q就能及早预警性能或可用性故障。以下是服务端线程阻塞造成Recv-Q和客户端Send-Q激增的例子:

view plain copy

$netstat -tn

Proto Recv-Q Send-Q Local Address Foreign

tcp.0.0.10:.0.0.10:#服务端连接

tcp.0.0.10:.0.0.10:#客户端连接

tcp.0.0.10:.0.0.4:

五:多线程服务器的适用场合

如果要在一台多核机器上提供一种服务或执行一个任务,可用的模式有:

a:运行一个单线程的进程;

b:运行一个多线程的进程;

c:运行多个单线程的进程;

d:运行多个多线程的进程;

考虑这样的场景:如果使用速率为50MB/s的数据压缩库,进程创建销毁的开销是800微秒,线程创建销毁的开销是50微秒。如何执行压缩任务?

如果要偶尔压缩1GB的文本文件,预计运行时间是20s,那么起一个进程去做是合理的,因为进程启动和销毁的开销远远小于实际任务的耗时。

如果要经常压缩500kB的文本数据,预计运行时间是10ms,那么每次都起进程 似乎有点浪费了,可以每次单独起一个线程去做。

如果要频繁压缩10kB的文本数据,预计运行时间是200微秒,那么每次起线程似 乎也很浪费,不如直接在当前线程搞定。也可以用一个线程池,每次把压缩任务交给线程池,避免阻塞当前线程(特别要避免阻塞IO线程)。

由此可见,多线程并不是万灵丹(silver bullet)。

1:必须使用单线程的场合

据我所知,有两种场合必须使用单线程:

a:程序可能会fork(2);

实际编程中,应该保证只有单线程程序能进行fork(2)。多线程程序不是不能调用fork(2),而是这么做会遇到很多麻烦:

fork一般不能在多线程程序中调用,因为Linux的fork只克隆当前线程的thread of control,不可隆其他线程。fork之后,除了当前线程之外,其他线程都消失了。

这就造成一种危险的局面。其他线程可能正好处于临界区之内,持有了某个锁,而它突然死亡,再也没有机会去解锁了。此时如果子进程试图再对同一个mutex加锁,就会立即死锁。因此,fork之后,子进程就相当于处于signal handler之中(因为不知道调用fork时,父进程中的线程此时正在调用什么函数,这和信号发生时的场景一样),你不能调用线程安全的函数(除非它是可重入的),而只能调用异步信号安全的函数。比如,fork之后,子进程不能调用:

malloc,因为malloc在访问全局状态时几乎肯定会加锁;

任何可能分配或释放内存的函数,比如snprintf;

任何Pthreads函数;

printf系列函数,因为其他线程可能恰好持有stdout/stderr的锁;

除了man 7 signal中明确列出的信号安全函数之外的任何函数。

因此,多线程中调用fork,唯一安全的做法是fork之后,立即调用exec执行另一个程序,彻底隔断子进程与父进程的联系。

在多线程环境中调用fork,产生子进程后。子进程内部只存在一个线程,也就是父进程中调用fork的线程的副本。

使用fork创建子进程时,子进程通过继承整个地址空间的副本,也从父进程那里继承了所有互斥量、读写锁和条件变量的状态。如果父进程中的某个线程占有锁,则子进程同样占有这些锁。问题是子进程并不包含占有锁的线程的副本,所以子进程没有办法知道它占有了哪些锁,并且需要释放哪些锁。

尽管Pthread提供了pthread_atfork函数试图绕过这样的问题,但是这回使得代码变得混乱。因此《Programming With Posix Threads》一书的作者说:”Avoid using fork in threaded code except where the child process will immediately exec a new program.”。

b:限制程序的CPU占用率;

这个很容易理解,比如在一个8核的服务器上,一个单线程程序即便发生busy-wait,占满1个core,其CPU使用率也只有12.5%,在这种最坏的情况下,系统还是有87.5%的计算资源可供其他服务进程使用。

Linux多进程和线程同步的几种方式

Linux 线程同步的橘蚂笑三种方法

线程的更大特点是资源的共享性,但资源共享中的同步问题是多线程编程的难点。linux下提供了多种方式来处理线程同步,最常用的是互斥锁、条件变量和信号量。

一、互斥锁(mutex)

通过锁机制实现线程间的同步。

初始化锁。在Linux下,线程的互斥量数据类型是pthread_mutex_t。在使用前,要对它进行初始化。

静态分配:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

动态分配:int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutex_attr_t *mutexattr);

加锁。对共享资源的访问,要对互斥量进行加锁,如果互斥量已经上了锁,调用线程会阻塞,直到互斥量被解锁。

int pthread_mutex_lock(pthread_mutex *mutex);

int pthread_mutex_trylock(pthread_mutex_t *mutex);

解锁。在完成了对共享资源的访问后,要对互斥量进行解锁。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

销毁锁。锁在是使用完成后,需要进行销毁以释放资源。

int pthread_mutex_destroy(pthread_mutex *mutex);

view plain copy

#include

#include

#include

#include

#include “iostream”

using namespace std;

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

int tmp;

void* thread(void *arg)

{

cout

#include

#include “stdlib.h”

#include “unistd.h”

pthread_mutex_t mutex;

pthread_cond_t cond;

void hander(void *arg)

{

free(arg);

(void)pthread_mutex_unlock(&mutex);

}

void *thread1(void *arg)

{

pthread_cleanup_push(hander, &mutex);

while(1)

{

printf(“thread1 is running\n”);

pthread_mutex_lock(&mutex);

pthread_cond_wait(&cond, &mutex);

printf(“thread1 applied the condition\n”);

pthread_mutex_unlock(&mutex);

sleep(4);

}

pthread_cleanup_pop(0);

}

void *thread2(void *arg)

{

while(1)

{

printf(“thread2 is running\n”);

pthread_mutex_lock(&mutex);

pthread_cond_wait(&cond, &mutex);

printf(“thread2 applied the condition\n”);

pthread_mutex_unlock(&mutex);

sleep(1);

}

}

int main()

{

pthread_t thid1,thid2;

printf(“condition variable study!\n”);

pthread_mutex_init(&mutex, NULL);

pthread_cond_init(&cond, NULL);

pthread_create(&thid1, NULL, thread1, NULL);

pthread_create(&thid2, NULL, thread2, NULL);

sleep(1);

do

{

pthread_cond_signal(&cond);

}while(1);

sleep(20);

pthread_exit(0);

return 0;

}

view plain copy

#include

#include

#include “stdio.h”

#include “stdlib.h”

static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

static pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

struct node

{

int n_number;

struct node *n_next;

}*head = NULL;

static void cleanup_handler(void *arg)

{

printf(“Cleanup handler of second thread./n”);

free(arg);

(void)pthread_mutex_unlock(&mtx);

}

static void *thread_func(void *arg)

{

struct node *p = NULL;

pthread_cleanup_push(cleanup_handler, p);

while (1)

{

//这个mutex主要是用来保证pthread_cond_wait的并发性

pthread_mutex_lock(&mtx);

while (head == NULL)

{

//这个while要特别说明一下,单个pthread_cond_wait功能很完善,为何

//这里要有一个while (head == NULL)呢?因为pthread_cond_wait里的线

//程可能会被意外唤醒,如果这个时候head != NULL,则不是我们想要的情况。

//这个时候,应该让线程继续进入pthread_cond_wait

// pthread_cond_wait会先解除之前的pthread_mutex_lock锁定的mtx,

//然后阻塞在等待对列里休眠,直到再次被唤醒(大多数情况下是等待的条件成立

//而被唤醒,唤醒后,该进程会先锁定先pthread_mutex_lock(&mtx);,再读取资源

//用这个流程是比较清楚的

pthread_cond_wait(&cond, &mtx);

p = head;

head = head->n_next;

printf(“Got %d from front of queue/n”, p->n_number);

free(p);

}

pthread_mutex_unlock(&mtx); //临界区数据操作完毕,释放互斥锁

}

pthread_cleanup_pop(0);

return 0;

}

int main(void)

{

pthread_t tid;

int i;

struct node *p;

//子线程会一直等待资源,类似生产者和消费者,但是这里的消费者可以是多个消费者,而

//不仅仅支持普通的单个消费者,这个模型虽然简单,但是很强大

pthread_create(&tid, NULL, thread_func, NULL);

sleep(1);

for (i = 0; i n_number = i;

pthread_mutex_lock(&mtx); //需要操作head这个临界资源,先加锁,

p->n_next = head;

head = p;

pthread_cond_signal(&cond);

pthread_mutex_unlock(&mtx); //解锁

sleep(1);

}

printf(“thread 1 wanna end the line.So cancel thread 2./n”);

//关于pthread_cancel,有一点额外的说明,它是从外部终止子线程,子线程会在最近的取消点,退出

//线程,而在我们的代码里,最近的取消点肯定就是pthread_cond_wait()了。

pthread_cancel(tid);

pthread_join(tid, NULL);

printf(“All done — exiting/n”);

return 0;

}

三、信号量(sem)

如同进程一样,线程也可以通过信号量来实现通信,虽然是轻量级的。信号量函数的名字都以”sem_”打头。线程使用的基本信号量函数有四个。

信号量初始化。

int sem_init (sem_t *sem , int pshared, unsigned int value);

这是对由sem指定的信号量进行初始化,设置好它的共享选项(linux 只支持为0,即表示它是当前进程的局部信号量),然后给它一个初始值VALUE。

等待信号量。给信号量减1,然后等待直到信号量的值大于0。

int sem_wait(sem_t *sem);

释放信号量。信号量值加1。并通知其他等待线程。

int sem_post(sem_t *sem);

销毁信号量。我们用完信号量后都它进行清理。归还占有的一切资源。

int sem_destroy(sem_t *sem);

view plain copy

#include

#include

#include

#include

#include

#include

#define return_if_fail(p) if((p) == 0){printf (“:func error!/n”, __func__);return;}

typedef struct _PrivInfo

{

sem_t s1;

sem_t s2;

time_t end_time;

}PrivInfo;

static void info_init (PrivInfo* thiz);

static void info_destroy (PrivInfo* thiz);

static void* pthread_func_1 (PrivInfo* thiz);

static void* pthread_func_2 (PrivInfo* thiz);

int main (int argc, char** argv)

{

pthread_t pt_1 = 0;

pthread_t pt_2 = 0;

int ret = 0;

PrivInfo* thiz = NULL;

thiz = (PrivInfo* )malloc (sizeof (PrivInfo));

if (thiz == NULL)

{

printf (“: Failed to malloc priv./n”);

return -1;

}

info_init (thiz);

ret = pthread_create (&pt_1, NULL, (void*)pthread_func_1, thiz);

if (ret != 0)

{

perror (“pthread_1_create:”);

}

ret = pthread_create (&pt_2, NULL, (void*)pthread_func_2, thiz);

if (ret != 0)

{

perror (“pthread_2_create:”);

}

pthread_join (pt_1, NULL);

pthread_join (pt_2, NULL);

info_destroy (thiz);

return 0;

}

static void info_init (PrivInfo* thiz)

{

return_if_fail (thiz != NULL);

thiz->end_time = time(NULL) + 10;

sem_init (&thiz->s1, 0, 1);

sem_init (&thiz->s2, 0, 0);

return;

}

static void info_destroy (PrivInfo* thiz)

{

return_if_fail (thiz != NULL);

sem_destroy (&thiz->s1);

sem_destroy (&thiz->s2);

free (thiz);

thiz = NULL;

return;

}

static void* pthread_func_1 (PrivInfo* thiz)

{

return_if_fail(thiz != NULL);

while (time(NULL) end_time)

{

sem_wait (&thiz->s2);

printf (“pthread1: pthread1 get the lock./n”);

sem_post (&thiz->s1);

printf (“pthread1: pthread1 unlock/n”);

sleep (1);

}

return;

}

static void* pthread_func_2 (PrivInfo* thiz)

{

return_if_fail (thiz != NULL);

while (time (NULL) end_time)

{

sem_wait (&thiz->s1);

printf (“pthread2: pthread2 get the unlock./n”);

sem_post (&thiz->s2);

printf (“pthread2: pthread2 unlock./n”);

sleep (1);

}

return;

}

返回列表

上一篇:linux 基线检查工具

linux 多线程高并发测试的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于linux 多线程高并发测试,Linux 多线程高并发测试技巧详解,浅谈linux 多线程编程和 windows 多线程编程的异同,如何看懂《Linux多线程服务端编程,Linux多进程和线程同步的几种方式的信息别忘了在本站进行查找喔。


数据运维技术 » Linux 多线程高并发测试技巧详解 (linux 多线程高并发测试)