1. 线程池的原理及实现
多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元 的闲置时间,增加处理器单元的吞吐能力。
假设一个服务器完成一项任务所需时间为:
T1 创建线程时间,
T2 在线程中执行任务的时间,
T3 销毁线程时间。
如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。
一个线程池包括以下四个基本组成部分:
1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线 程池,添加新任务;
2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环 的执行任务;
3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它 主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;
4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。
线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。 它把T1,T3分别安排在服务器程序的启动和结束的时间段或者一些空闲的时间段,这 样在服务器程序处理客户请求时,不会有T1,T3的开销了。
线程池不仅调整T1,T3产生的时间段,而且它还显著减少了创建线程的数目。
代码实现:
condition.h
#ifndef _CONDITION_H_
#define _CONDITION_H_
#include <pthread.h>
//封装一个互斥量和条件变量作为状态
typedef struct condition
{
pthread_mutex_t pmutex;
pthread_cond_t pcond;
}condition_t;
//对状态的操作函数
int condition_init(condition_t *cond);
int condition_lock(condition_t *cond);
int condition_unlock(condition_t *cond);
int condition_wait(condition_t *cond);
int condition_timedwait(condition_t *cond, const struct timespec *abstime);
int condition_signal(condition_t* cond);
int condition_broadcast(condition_t *cond);
int condition_destroy(condition_t *cond);
#endif
condition.c
#include "condition.h"
//初始化
int condition_init(condition_t *cond)
{
int status;
if((status = pthread_mutex_init(&cond->pmutex, NULL)))
return status; if((status = pthread_cond_init(&cond->pcond, NULL)))
return status;
return 0;
}
//加锁
int condition_lock(condition_t *cond)
{
return pthread_mutex_lock(&cond->pmutex);
}
//解锁
int condition_unlock(condition_t *cond)
{
return pthread_mutex_unlock(&cond->pmutex);
}
//等待
int condition_wait(condition_t *cond)
{
return pthread_cond_wait(&cond->pcond, &cond->pmutex);
}
//固定时间等待
int condition_timedwait(condition_t *cond, const struct timespec *abstime)
{
return pthread_cond_timedwait(&cond->pcond, &cond->pmutex, abstime);
}
//唤醒一个睡眠线程
int condition_signal(condition_t* cond)
{
return pthread_cond_signal(&cond->pcond);
}
//唤醒所有睡眠线程
int condition_broadcast(condition_t *cond)
{
return pthread_cond_broadcast(&cond->pcond);
}
//释放
int condition_destroy(condition_t *cond)
{
int status;
if((status = pthread_mutex_destroy(&cond->pmutex)))
return status;
if((status = pthread_cond_destroy(&cond->pcond)))
return status;
return 0;
}
threadpool.h
#ifndef _THREAD_POOL_H_
#define _THREAD_POOL_H_
//线程池头文件
#include "condition.h"
//封装线程池中的对象需要执行的任务对象
typedef struct task
{
void *(*run)(void *args); //函数指针,需要执行的任务
void *arg; //参数
struct task *next; //任务队列中下一个任务
}task_t;
//下面是线程池结构体
typedef struct threadpool
{
condition_t ready; //状态量
task_t *first; //任务队列中第一个任务
task_t *last; //任务队列中最后一个任务
int counter; //线程池中已有线程数
int idle; //线程池中kongxi线程数
int max_threads; //线程池最大线程数
int quit; //是否退出标志
}threadpool_t;
//线程池初始化
void threadpool_init(threadpool_t *pool, int threads);
//往线程池中加入任务
void threadpool_add_task(threadpool_t *pool, void *(*run)(void *arg), void *arg);
//摧毁线程池
void threadpool_destroy(threadpool_t *pool);
#endif
threadpool.c
#include "threadpool.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <time.h>
//创建的线程执行
void *thread_routine(void *arg)
{
struct timespec abstime;
int timeout;
printf("thread %d is starting\n", (int)pthread_self());
threadpool_t *pool = (threadpool_t *)arg;
while(1)
{
timeout = 0;
//访问线程池之前需要加锁
condition_lock(&pool->ready);
//空闲
pool->idle++;
//等待队列有任务到来 或者 收到线程池销毁通知
while(pool->first == NULL && !pool->quit)
{
//否则线程阻塞等待
printf("thread %d is waiting\n", (int)pthread_self());
//获取从当前时间,并加上等待时间, 设置进程的超时睡眠时间
clock_gettime(CLOCK_REALTIME, &abstime);
abstime.tv_sec += 2;
int status;
//该函数会解锁,允许其他线程访问,当被唤醒时,加锁
status = condition_timedwait(&pool->ready, &abstime);
if(status == ETIMEDOUT)
{
printf("thread %d wait timed out\n", (int)pthread_self());
timeout = 1;
break;
}
}
pool->idle--;
if(pool->first != NULL)
{
//取出等待队列最前的任务,移除任务,并执行任务
task_t *t = pool->first;
pool->first = t->next;
//由于任务执行需要消耗时间,先解锁让其他线程访问线程池
condition_unlock(&pool->ready);
//执行任务
t->run(t->arg);
//执行完任务释放内存
free(t);
//重新加锁
condition_lock(&pool->ready);
}
//退出线程池
if(pool->quit && pool->first == NULL)
{
pool->counter--;//当前工作的线程数-1
//若线程池中没有线程,通知等待线程(主线程)全部任务已经完成
if(pool->counter == 0)
{
condition_signal(&pool->ready);
}
condition_unlock(&pool->ready);
break;
}
//超时,跳出销毁线程
if(timeout == 1)
{
pool->counter--;//当前工作的线程数-1
condition_unlock(&pool->ready);
break;
}
condition_unlock(&pool->ready);
}
printf("thread %d is exiting\n", (int)pthread_self());
return NULL;}
//线程池初始化
void threadpool_init(threadpool_t *pool, int threads)
{
condition_init(&pool->ready);
pool->first = NULL;
pool->last =NULL;
pool->counter =0;
pool->idle =0;
pool->max_threads = threads;
pool->quit =0;
}
//增加一个任务到线程池
void threadpool_add_task(threadpool_t *pool, void *(*run)(void *arg), void *arg)
{
//产生一个新的任务
task_t *newtask = (task_t *)malloc(sizeof(task_t));
newtask->run = run;
newtask->arg = arg;
newtask->next=NULL;//新加的任务放在队列尾端
//线程池的状态被多个线程共享,操作前需要加锁
condition_lock(&pool->ready);
if(pool->first == NULL)//第一个任务加入
{
pool->first = newtask;
}
else
{
pool->last->next = newtask;
}
pool->last = newtask; //队列尾指向新加入的线程
//线程池中有线程空闲,唤醒
if(pool->idle > 0)
{
condition_signal(&pool->ready);
}
//当前线程池中线程个数没有达到设定的最大值,创建一个新的线性
else if(pool->counter < pool->max_threads)
{
pthread_t tid;
pthread_create(&tid, NULL, thread_routine, pool);
pool->counter++;
}
//结束,访问
condition_unlock(&pool->ready);
}
//线程池销毁
void threadpool_destroy(threadpool_t *pool)
{
//如果已经调用销毁,直接返回
if(pool->quit)
{
return;
}
//加锁
condition_lock(&pool->ready);
//设置销毁标记为1
pool->quit = 1;
//线程池中线程个数大于0
if(pool->counter > 0)
{
//对于等待的线程,发送信号唤醒
if(pool->idle > 0)
{
condition_broadcast(&pool->ready);
}
//正在执行任务的线程,等待他们结束任务
while(pool->counter)
{
condition_wait(&pool->ready);
}
}
condition_unlock(&pool->ready);
condition_destroy(&pool->ready);
}
一线互联网大厂完整面试题及答案 关注公众号零声学院免费领取!
2. C++ 线程同步的四种方式
(1)事件(Event);
(2)信号量(semaphore);
(3)互斥量(mutex);
(4)临界区(Critical section);
1.事件
事件(Event)是WIN32提供的最灵活的线程间同步方式,事件可以处于激发状态 (signaled or true)或未激发状态(unsignal or false)。根据状态变迁方式的不同, 事件可分为两类:
(1)手动设置:这种对象只可能用程序手动设置,在需要该事件或者事件发生时,采 用SetEvent及ResetEvent来进行设置。
(2)自动恢复:一旦事件发生并被处理后,自动恢复到没有事件状态,不需要再次设 置。
使用”事件”机制应注意以下事项:
(1)如果跨进程访问事件,必须对事件命名,在对事件命名的时候,要注意不要与系 统命名空间中的其它全局命名对象冲突;
(2)事件是否要自动恢复;
(3)事件的初始状态设置。
由于event对象属于内核对象,故进程B可以调用OpenEvent函数通过对象的名字获得 进程A中event对象的句柄,然后将这个句柄用于ResetEvent、SetEvent和 WaitForMultipleObjects等函数中。此法可以实现一个进程的线程控制另一进程中线 程的运行,例如:
#include "stdafx.h"
#include<windows.h>
#include<iostream>
using namespace std;
int number = 1; //定义全局变量
HANDLE hEvent; //定义事件句柄
unsigned long __stdcall ThreadProc1(void* lp)
{
while (number < 100)
{
WaitForSingleObject(hEvent, INFINITE); //等待对象为有信号状态
cout << "thread 1 :"<<number << endl;
++number;
_sleep(100);
SetEvent(hEvent);
}
return 0;
}
unsigned long __stdcall ThreadProc2(void* lp)
{
while (number < 100)
{
WaitForSingleObject(hEvent, INFINITE); //等待对象为有信号状态
cout << "thread 2 :"<<number << endl;
++number;
_sleep(100);
SetEvent(hEvent);
}
return 0;
}
int main()
{
CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);
hEvent = CreateEvent(NULL, FALSE, TRUE, "event");
Sleep(10*1000);
system("pause");
return 0;
}
2.信号量
信号量是维护0到指定最大值之间的同步对象。信号量状态在其计数大于0时是有信号 的,而其计数是0时是无信号的。信号量对象在控制上可以支持有限数量共享资源的访 问。
信号量的特点和用途可用下列几句话定义:
(1)如果当前资源的数量大于0,则信号量有效;
(2)如果当前资源数量是0,则信号量无效;
(3)系统决不允许当前资源的数量为负值;
(4)当前资源数量决不能大于最大资源数量。
#include "stdafx.h"
#include<windows.h>
#include<iostream>
using namespace std;
int number = 1; //定义全局变量
HANDLE hSemaphore; //定义信号量句柄
unsigned long __stdcall ThreadProc1(void* lp)
{
long count;
while (number < 100)
{
//等待信号量为有信号状态
WaitForSingleObject(hSemaphore, INFINITE);
cout << "thread 1 :"<<number << endl;
++number;
_sleep(100);
ReleaseSemaphore(hSemaphore, 1, &count);
}
return 0;
}
unsigned long __stdcall ThreadProc2(void* lp)long count;
while (number < 100)
{
//等待信号量为有信号状态
WaitForSingleObject(hSemaphore, INFINITE);
cout << "thread 2 :"<<number << endl;
++number;
_sleep(100);
ReleaseSemaphore(hSemaphore, 1, &count);
}
return 0;
}
int main()
{
hSemaphore = CreateSemaphore(NULL, 1, 100, "sema");
CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);
Sleep(10*1000);
system("pause");
return 0;
}
3.互斥量
采用互斥对象机制。 只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对 象只有一个,所以能保证公共资源不会同时被多个线程访问。互斥不仅能实现同一 应用程序的公共资源安全共享, 还能实现不同应用程序的公共资源安全共享。
#include "stdafx.h"
#include<windows.h>
#include<iostream>
using namespace std;
int number = 1; //定义全局变量
HANDLE hMutex; //定义互斥对象句柄
unsigned long __stdcall ThreadProc1(void* lp)
{
while (number < 100)
{
WaitForSingleObject(hMutex, INFINITE);
cout << "thread 1 :"<<number << endl;
++number;
_sleep(100);
ReleaseMutex(hMutex);
}
return 0;
}
unsigned long __stdcall ThreadProc2(void* lp)
{
while (number < 100)
{
WaitForSingleObject(hMutex, INFINITE);
cout << "thread 2 :"<<number << endl;
++number;
_sleep(100);
ReleaseMutex(hMutex);
}
return 0;
}
int main()
{
hMutex = CreateMutex(NULL, false, "mutex"); //创建互斥对象
CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);
Sleep(10*1000);
system("pause");
return 0;
}
4.临界区
临界区(Critical Section)是一段独占对某些共享资源访问的代码,在任意时刻只允 许一个线程对共享资源进行访问。如果有多个线程试图同时访问临界区,那么在有一个 线程进入后其他所有试图访问此临界区的线程将被挂起,并一直持续到进入临界区的线 程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享 资源的目的。
临界区在使用时以CRITICAL_SECTION结构对象保护共享资源,并分别用 EnterCriticalSection()和LeaveCriticalSection()函数去标识和释放一个临界 区。所用到的CRITICAL_SECTION结构对象必须经过InitializeCriticalSection() 的初始化后才能使用,而且必须确保所有线程中的任何试图访问此共享资源的代码都处 在此临界区的保护之下。否则临界区将不会起到应有的作用,共享资源依然有被破坏的 可能。
#include "stdafx.h"
#include<windows.h>
#include<iostream>
using namespace std;
int number = 1; //定义全局变量
CRITICAL_SECTION Critical; //定义临界区句柄
unsigned long __stdcall ThreadProc1(void* lp)
{
while (number < 100)
{
EnterCriticalSection(&Critical);
cout << "thread 1 :"<<number << endl;
++number;
_sleep(100);
LeaveCriticalSection(&Critical);
}
return 0;
}
unsigned long __stdcall ThreadProc2(void* lp)
{
while (number < 100)
{
EnterCriticalSection(&Critical);
cout << "thread 2 :"<<number << endl;
++number;
_sleep(100);
LeaveCriticalSection(&Critical);
}
return 0;
}
int main()
{
InitializeCriticalSection(&Critical); //初始化临界区对象
CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);
Sleep(10*1000);
system("pause");
return 0;
}
3. 同一个IP同一个端口可以同时建立tcp和udp的连接吗
可以,同一个端口虽然udp和tcp的端口数字是一样的,但实质他们是不同的端口,所 以是没有影响的,从底层实质分析,对于每一个连接内核维护了一个五元组,包含了源 ip,目的ip、源端口目的端口、以及传输协议,在这里尽管前4项都一样,但是传输 协议是不一样的,所以内核会认为是2个不同的连接,在ip层就会进行开始分流,tcp 的走tcp,udp走udp。
4. 堆和栈的区别
1.从管理方式上,
栈是由编译器自动管理,无需我们手动控制;
对于堆,开辟和释放工作由程序员控制,所以有内存泄漏等情况的发生。
2.从申请大小上,
栈是有高地址向低地址扩展的,是一块连续的内存区域,所以栈的栈顶地址或者大小 是 一开始就分配好的。在使用过程中,比如递归调用层数过多,那么就有可能造成栈溢出, 所以栈能获得的空间比较少;
堆是向高地址扩展的,是链表组织的方式,所以有可能是不连续的,他的大小只受限于 有效的虚拟内存大小,所以堆能开辟的空间较大。
3.从碎片问题上,
栈是没有碎片的情况,因为他有严格的出栈入栈,不会存在一个内存块从栈的中间位置 弹出;
堆有碎片的情况,频繁的调用new/delete分配释放内存,必然会造成内存碎片。
4.从分配方式上,
堆都是动态分配的
栈大多是静态分配的,也可以动态分配,可以由alloc函数分配。
5.从分配效率上,
计算机会在底层对栈提供支持,比如有专门的寄存器分配,用来存放栈的地址,压栈出 栈的指令等;