windows创建线程、IO模型、同步异步

本博客内容:
一、线程创建函数CreateThread()
二、等待线程返回函数
三、windows下另一个线程函数_beginThreadex()
四、多线程编程之windows同步方式
五、Linux五种IO模型
六、同步、异步、阻塞、非阻塞
七、并发与并行的理解
八、select/poll/epoll的区别

一、线程创建函数CreateThread()

参考:https://www.cnblogs.com/ay-a/p/8762951.html

头文件:#include
原型:
HANDLE WINAPI CreateThread()
参数6个:       默认值   代表含义
安全性          NULL    线程安全,不被继承
栈空间          0       默认分配1M
线程函数        函数 
传给线程的参数   无参数使用0    假如要传for循环中的参量i,填写&i即可
是否立即调度     0       创建后立即调度
返回线程的ID号   0       不需要返回ID号

创建成功返回新线程的句柄,失败返回NULL

举例:最简单的使用

#include
#include
DWORD WINAPI  ThreadFunc(LPVOID);
void  main()
{
    HANDLE hThread;
    DWORD threadId;
    hThread=CreateThread(NULL,0,ThreadFunc,0,0,&threadId);
    printf("我是主线程,pid=%d\n",GetCurrentThreadId()); 
    Sleep(100);
}
DWORD WINAPI ThreadFunc(LPVOID)
{
    Sleep(100);
    printf("我是子线程,pid=%d\n",GetCurrentThreadId());
    return 0;
}
//运行结果很随机

二、等待线程返回函数

多线程编程后,我们需要等待某一线程完成了特定操作后再继续做其他事情,要实现该目的,可使用windows函数WaitForSingleObject,或者WaitForMultiObjects。这2个函数都会等待object被标为有信号(signaled)时才返回。只要是Windows创建的object都会赋予一个状态量。如果被Object被激活了,或正在使用,那么该Object就是无信号,不可用的。

第一个函数:等待单个线程返回
函数原型:
DWORD WINAPI WaitForSingleObject()
参数2个:   (只要是句柄对象都可以,包括event,semaphore、thread、mutex)
句柄
等待毫秒       0: 不等待   INFINITE 无限等待

返回值:
WAIT_OBJECT_0 指定的对象有信号状态
WAIT_TIMEOUT  等待超时

举例:

#include
#include
DWORD WINAPI  ThreadFunc(LPVOID);
int  main()
{
    HANDLE hThread;
    DWORD threadId;
    hThread=CreateThread(NULL,0,ThreadFunc,0,0,&threadId);
      printf("我是主线程, pid = %d\n", GetCurrentThreadId());  //输出主线程pid
    WaitForSingleObject(hThread,0); //不等待,直接返回
    return 0;
}
DWORD WINAPI ThreadFunc(LPVOID)
{
    Sleep(200);
    printf("我是子线程,pid=%d\n",GetCurrentThreadId());
    return 0;
}
第二个函数:等待多个线程返回
函数原型: DWORD WINAPI WaitForMultiObjects()
4个参数:
等待个数       064中的一个值
句柄数组指针   存放被等待的内核对象的句柄的数组
bool              是否等到所有内核对象为已通知状态后才返回,如果为true,则是这样,如果为false,只要有一个对象为已通知状态时就可以返回。
等待时间       

举例:

#include 
#include 

const  int THREAD_NUM = 10;
DWORD WINAPI  ThreadFunc(LPVOID);

int main()
{
     printf("我是主线程, pid = %d\n", GetCurrentThreadId());  //输出主线程pid
     HANDLE hThread[THREAD_NUM];
     for(int i=0;i0,ThreadFunc,&i,0,NULL);//创建线程
     }
     WaitForMultipleObjects(THREAD_NUM,hThread,false,INFINITE); //只要有一个线程返回就结束 //如果为true,则必须要将所有的都返回才结束
     return 0;
}
DWORD WINAPI ThreadFunc(LPVOID p)
{
    int n=*(int*)p;
    Sleep(1000*n); //第n个线程睡眠n秒
     printf("我是, pid = %d 的子线程\n", GetCurrentThreadId());   //输出子线程pid
     printf(" pid = %d 的子线程退出\n", GetCurrentThreadId());   

    return 0;
}

三、windows下另一个线程函数_beginThreadex()

也是windows提供的AP头文件:process.h
尽量多用这个函数原因
要从标准C运行库与多线程的矛盾说起,标准C运行库在1970年就实现了,当时没任何一个操作系统提供对多线程的支持,因此编写标准C运行库的程序员根本没考虑多线程程序使用标准C运行库的情况,比如标准C运行库的全局变量errno,很多运行库中的函数会在出错时会将错误代码赋值给这个全局变量,这样可以方便调试,如果有这个代码片段:

if(system("notepad.exe readme.txt")==-1)
{
switch(errno)
    ...//错误处理代码
}

假设某个线程A在执行上面的代码,该线程在调用system()之后尚未调用switch()语句时另外一个线程B启动了,这个线程B也调用了标准C运行库的函数,将错误代号写入了errno中。这样线程A一旦开始执行switch()语句时,它将访问一个被B线程改动了的errno,这种情况需要避免,因为不单单是这个变量会出问题,其他像sterror()、asctime()等函数也会遇到这种由多个线程访问修改导致的数据覆盖问题。
为了解决该问题,windows操作系统提供了这样的解决方案-每个线程都将拥有自己的一块内存区域来供标准C运行库中所有有需要的函数使用
_beginthreadex()函数在创建新线程时会分配并初始化一个_tiddata块。这个_tiddata块自然是用来存放一些需要线程独享的数据。新线程运行时会首先将_tiddata块与自己进一步关联起来。然后新线程调用标准C运行库函数如strtok()时就会先取得_tiddata块的地址再将需要保护的数据存入_tiddata块中。这样每个线程就只会访问和修改自己的数据而不会去篡改其它线程的数据了。因此,如果在代码中有使用标准C运行库中的函数时,尽量使用_beginthreadex()来代替CreateThread()
举例:

#include  //_beginthreadex()的头文件
#include
#include
using namespace std;
unsigned int _stdcall ThreadFun(PVOID pM)
{
     printf("线程ID 为 %d 的子线程输出: Hello World\n", GetCurrentThreadId());
     return 0;
}
int main()
{
    const int THREAD_NUM=5;
    HANDLE handle[THREAD_NUM];
    for(int i=0;i0,ThreadFun,NULL,0,NULL);
        WaitForMultipleObjects(THREAD_NUM,handle,TRUE,INFINITE);
    }
    return 0;
}

四、多线程编程之windows同步方式

参考:https://www.cnblogs.com/cyyljw/p/8006819.html
针对多线程同步与互斥:主要包括4种方式:
临界区/关键段:CRITICAL_SECTION
互斥量:Mutex
信号量:Semaphore
事件对象:Event

(1)临界区
每个进程中访问临界资源的那段代码称为临界区(临界资源是一次仅允许一个进程使用的共享资源)。每次只准许一个进程进入临界区,进入后不允许其他进程进入。不论是硬件临界资源/软件资源,多个进程必须互斥地对他们访问。

四个函数
CRITICAL_SECTION CriticalSection;
InitializeCriticalSection(&CriticalSection);
EnterCriticalSection(&CriticalSection);
LeaveCriticalSection(&CriticalSection);
DeleteCriticalSecton(&CriticalSection);

//关键段使用举例:

#include 
#include 
#include 
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//关键段变量声明
CRITICAL_SECTION  g_csThreadParameter, g_csThreadCode;
int main()
{
    printf("     经典线程同步 关键段\n");
    printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");

    //关键段初始化
    InitializeCriticalSection(&g_csThreadParameter);
    InitializeCriticalSection(&g_csThreadCode);

    HANDLE  handle[THREAD_NUM]; 
    g_nNum = 0; 
    int i = 0;
    while (i < THREAD_NUM) 
    {
        EnterCriticalSection(&g_csThreadParameter);//进入子线程序号关键区域
        handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
        ++i;
    }
    WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
    DeleteCriticalSection(&g_csThreadCode);
    DeleteCriticalSection(&g_csThreadParameter);
    return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
    int nThreadNum = *(int *)pPM; 
    LeaveCriticalSection(&g_csThreadParameter);//离开子线程序号关键区域

    Sleep(50);//some work should to do

    EnterCriticalSection(&g_csThreadCode);//进入各子线程互斥区域
    g_nNum++;
    Sleep(0);//some work should to do
    printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);
    LeaveCriticalSection(&g_csThreadCode);//离开各子线程互斥区域
    return 0;
}

输出结果:可以实现子线程间的互斥,但是主线程和子线程间的同步还是有问题的。
加断点调试后发现:主线程能多次进入这个关键区域。
原因
关键段是有“线程所有权”概念的。关键段会记录拥有该关键段的线程句柄,事实上用第4个参数来记录获准进入关键区域的线程句柄。,EnterCriticalSection()会更新第三个参数RecursionCount以记录该线程进入的次数并立即返回让该线程进入。其它线程调用EnterCriticalSection()则会被切换到等待状态,一旦拥有线程所有权的线程调用LeaveCriticalSection()使其进入的次数为0时,系统会自动更新关键段并将等待中的线程换回可调度状态。
回到这个经典线程同步问题上,主线程可以重复进入关键代码区域从而导致子线程在接收参数之前主线程就已经修改了这个参数。所以关键段可以用于线程间的互斥,但不可以用于同步
总结:

1.关键段4个函数
2.可以解决线程间的互斥,但是因为具有“线程所有权”概念,无法解决同步问题。
3.推荐关键段和旋转锁配合使用。

(2)互斥量 内核对象
互斥量也是一个内核对象,它用来确保一个线程独占一个资源的访问。。互斥量与关键段的行为非常相似,且互斥量可以用于不同进程中的线程互斥访问资源。

CreateMutex  
参数3个:
第一个参数表示安全控制,一般直接传入NULL。
第二个参数用来确定互斥量的初始拥有者。如果传入TRUE表示互斥量对象内部会记录创建它的线程的线程ID号并将递归计数设置为1,由于该线程ID非零,所以互斥量处于未触发状态。如果传入FALSE,那么互斥量对象内部的线程ID号将设置为NULL,递归计数设置为0,这意味互斥量不为任何线程占用,处于触发状态。

OpenMutex
第一个参数表示访问权限,对互斥量一般传入MUTEX_ALL_ACCESS。详细解释可以查看MSDN文档。
第二个参数表示互斥量句柄继承性,一般传入TRUE即可。
第三个参数表示名称。某一个进程中的线程创建互斥量后,其它进程中的线程就可以通过这个函数来找到这个互斥量。
成功返回一个表示互斥量的句柄,失败返回NULL。

ReleaseMutex
访问互斥资源前应该要调用等待函数,结束访问时就要调用ReleaseMutex()来表示自己已经结束访问,其它线程可以开始访问了。
CloseHandle
使用时需要借助:WaitforSingleObject
//经典线程同步问题 互斥量Mutex
#include 
#include 
#include 
long g_nNum;
unsigned int __stdcall Fun(void * pPM);
const int THREAD_NUM =10;
//互斥量与关键段
HANDLE g_hThreadParameter;
CRITICAL_SECTION g_csThreadCode;
int main()
{
    printf("     经典线程同步 互斥量Mutex\n");
    printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
    //初始化互斥量与关键段 第二个参数为TRUE表示互斥量为创建线程所有
    g_hThreadParameter = CreateMutex(NULL, FALSE, NULL);
    InitializeCriticalSection(&g_csThreadCode);

    HANDLE  handle[THREAD_NUM]; 
    g_nNum = 0; 
    int i = 0;
    while (i < THREAD_NUM) 
    {
        handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
        WaitForSingleObject(g_hThreadParameter, INFINITE); //等待互斥量被触发
        i++;
    }
    WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
    //销毁互斥量和关键段
    CloseHandle(g_hThreadParameter);
    DeleteCriticalSection(&g_csThreadCode);
    for (i = 0; i < THREAD_NUM; i++)
        CloseHandle(handle[i]);
    return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
    int nThreadNum = *(int *)pPM;
    ReleaseMutex(g_hThreadParameter);//触发互斥量  先执行子线程这里触发一次,然后主线程可以向下执行一次

    Sleep(50);//some work should to do

    EnterCriticalSection(&g_csThreadCode);
    g_nNum++;
    Sleep(0);//some work should to do
    printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);
    LeaveCriticalSection(&g_csThreadCode);
    return 0;
}

与关键段类似,互斥量也不能解决线程间的同步问题
因为它也有线程所有权。互斥量多用于多进程之间的线程互斥。比关键段还多了一个特性-“遗弃”情况的处理。比如有一个占用互斥量的线程在调用ReleaseMutex()触发互斥量前就意外终止了(相当于该互斥量被“遗弃”了),那么所有等待这个互斥量的线程是否会由于该互斥量无法被触发而陷入一个无穷的等待过程中了?这显然不合理。因为占用某个互斥量的线程既然终止了那足以证明它不再使用被该互斥量保护的资源,所以这些资源完全并且应当被其它线程来使用。因此在这种“遗弃”情况下,系统自动把该互斥量内部的线程ID设置为0,并将它的递归计数器复置为0,表示这个互斥量被触发了。然后系统将“公平地”选定一个等待线程来完成调度
总结:
1.互斥量是内核对象,它与关键段都有线程所有权概念,不能用于线程的同步。
2.互斥量能够用于多个进程之间的线程之间的互斥。并且可以解决某进程意外终止噪声的遗弃问题。
(3)事件Event
用来解决线程同步问题

1.CreateEvent
4个参数:      默认值
安全属性     NULL
置位       TRUE   手动置位(打开后,全部都可以)  自动置位(每次只有一个)
初始状态    TRUE(表示已被触发)
事件名称    NULL(匿名事件)

2.OpenEvent  获得一个事件句柄
3个参数:
访问权限  一般传入EVENT_ALL_ACCESS
句柄继承性   TRUE
函数表示名称     不同进程中的各线程可以通过名称来确保它们访问同一个事件

3.SetEvent
触发事件

4.ResetEvent
将事件设置为未触发
CloseHandle()

举例子:完美解决线程同步问题

#include 
#include 
#include 
long g_nNum;
unsigned int __stdcall Fun(void *pPM);
const int THREAD_NUM = 10;
//事件与关键段
HANDLE  g_hThreadEvent;
CRITICAL_SECTION g_csThreadCode;
int main()
{
    printf("     经典线程同步 事件Event\n");
    printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");
    //初始化事件和关键段 自动置位,初始无触发的匿名事件
    g_hThreadEvent=CreateEvent(NULL,FALSE,FALSE,NULL);
    HANDLE handle[THREAD_NUM];
    g_nNum=0;
    int i=0;
    while(iNULL,0,Fun,&i,0,NULL);
        WaitForSingleObject(g_hThreadEvent,INFINITE);
        i++;
    }
    WaitForMultipleObjects(THREAD_NUM,handle,TRUE,INFINITE);
    //销毁事件和关键段
    CloseHandle(g_hThreadEvent);
    DeleteCriticalSection(&g_csThreadCode);
    return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
    int nThreadNum = *(int *)pPM;
    SetEvent(g_hThreadEvent); //触发事件

    Sleep(50);//some work should to do

    EnterCriticalSection(&g_csThreadCode);
    g_nNum++;
    Sleep(0);//some work should to do
    printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum); 
    LeaveCriticalSection(&g_csThreadCode);
    return 0;
}

还有一个PluseEvent()函数
功能:将事件触发后立即将事件设置为未触发,相当于触发一个事件脉冲
不常用的事件函数,此函数相当于SetEvent()后立即调用ResetEvent(),此时情况可以分为两种:
它对于手动和自动置位的情况是不同的。
(4)信号量
三个函数:

CreateSemaphore
参数:
线程安全属性    NULL
初始资源数量   
最大并发数量
信号量的名称    NULL(匿名信号量)
OpenSemaphore    打开信号量
访问权限:一般传入   SEMAPHORE_ALL_ACCESS
参数2: 信号量的句柄继承性   TRUE
参数3:   不同进程中的各进程可以通过名称确保它们访问同一个信号量
ReleaseSemaphore
第一个参数:信号量句柄
参数2:增加的个数 >0
参数3:可以用来传出先前的资源计数,设为NULL,表示不需要传出

CloseHandle()

注意:如果当前资源数量大于0,表示信号量处于触发,等于0表示资源已经耗尽信号量处于未触发。在对信号量调用等待函数时,等待函数会检查信号量的当前资源计数,如果大于0(即信号量处于触发状态),减1后返回让调用线程继续执行。一个线程可以多次调用等待函数来减小信号量。

#include 
#include 
#include 
long g_nNum;
unsigned int _stdcall Fun(void * pPM);
const int THREAD_NUM =10;

//信号量与关键段
HANDLE            g_hThreadParameter;
CRITICAL_SECTION  g_csThreadCode;
int main()
{
    printf("     经典线程同步 信号量Semaphore\n");
    printf(" -- by MoreWindows( http://blog.csdn.net/MoreWindows ) --\n\n");

    //初始化信号量和关键段
    g_hThreadParameter = CreateSemaphore(NULL, 0, 1, NULL);//当前0个资源,最大允许1个同时访问
    InitializeCriticalSection(&g_csThreadCode);
    HANDLE  handle[THREAD_NUM]; 
    g_nNum = 0;
    int i = 0;
    while (i < THREAD_NUM) 
    {
        handle[i] = (HANDLE)_beginthreadex(NULL, 0, Fun, &i, 0, NULL);
        WaitForSingleObject(g_hThreadParameter, INFINITE);//等待信号量>0
        ++i;
    }
    WaitForMultipleObjects(THREAD_NUM, handle, TRUE, INFINITE);
    //销毁信号量和关键段
    DeleteCriticalSection(&g_csThreadCode);
    CloseHandle(g_hThreadParameter);
    for (i = 0; i < THREAD_NUM; i++)
        CloseHandle(handle[i]);
    return 0;
}
unsigned int __stdcall Fun(void *pPM)
{
    int nThreadNum=*(int *)pPM;
    ReleaseSemaphore(g_hThreadParameter, 1, NULL);//信号量++
    Sleep(50);//some work should to do

    EnterCriticalSection(&g_csThreadCode);
    ++g_nNum;
    Sleep(0);//some work should to do
    printf("线程编号为%d  全局资源值为%d\n", nThreadNum, g_nNum);
    LeaveCriticalSection(&g_csThreadCode);
    return 0;
}

计算当前剩余量并根据当前剩余量与零比较来决定信号量是处于触发状态或是未触发状态,应用很广泛。
信号量也可以解决线程之间的同步问题。
注:
关键段不是内核对象。
总结:
线程同步的主要任务:
引入多线程之后,由于线程执行的异步性,会给系统造成混乱,特别是在急用临界资源时,如多个线程急用一台打印机,会将打印结果交织在一起,难于区分。当多个线程急用共享变量,表格,链表时,可能会导致数据处理出错,因此线程同步的主要任务是使并发执行的各线程之间能够有效地共享资源和相互合作,从而使程序的执行具有可再现性。
1.同步与互斥:
当线程并发执行时,由于资源共享和线程协作,使用线程之间会存在2种依赖关系:
(1).间接相互制约。一个系统中的多个线程必然要共享某种系统资源,如共享CPU,共享I/O设备,所谓间接相互制约即源于这种资源共享,打印机就是最好的例子,线程A在使用打印机时,其它线程都要等待。
(2).直接相互制约。这种制约主要是因为线程之间的合作,如有线程A将计算结果提供给线程B做进一步处理,那么线程B在线程A将数据送达之前都将处于阻塞状态。
前者称为互斥:后者称为同步。对于互斥可以这样理解,线程A和线程B互斥访问某个资源则它们之间就产生了一个顺序问题-要么线程A等待B,要么B等待A操作完毕,这其实就是线程的同步。互斥是一种特殊的同步。

五、Linux五种IO模型

参考:
https://blog.csdn.net/hguisu/article/details/7453390
https://www.jianshu.com/p/486b0965c296
阻塞I/O 非阻塞I/O I/O复用 信号驱动I/O 异步I/O
前4种是同步 最后一个是异步IO
(一):阻塞I/O模型
进程会一直阻塞,直到数据拷贝完成,Linux下socket默认是阻塞的。阻塞就是进程“被”休息,CPU处理其他进程去了。
windows创建线程、IO模型、同步异步_第1张图片
描述:
用户进程调用recv()/recvfrom()这个系统调用,kernel就开始了IO的第一个阶段:准备数据。(对于网络IO来说,一般此时数据还没有到达),这个过程需要等待,就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞。第二个阶段:当kernel一直等到数据准备好了,就会将数据从kernel拷贝到用户内存,然后kernel返回结果。用户进程解除block状态,继续运行。
重点:阻塞IO特点: IO执行的2个阶段都被阻塞了
优点:1.能够及时返回数据,无延迟2.对内核开发者来说省事
缺点:等待时间

(二)非阻塞I/O
需要不断的轮询,是否准备好了,
每隔一会儿瞄一眼的轮询方式,设备是以非阻塞的方式打开的。意味着IO操作不会立即完成,read操作可能会返回一个错误代码,说明这个命令不能立即满足。
就是,非阻塞的recfrom系统调用之后,进程并没有被阻塞,内核马上返回给进程,如果没准备好,会返回一个error。进程在返回之后,可以干点别的,然后再发起recvfrom系统调用。循环重复的进行recvfrom系统调用。该过程称为“轮询”,拷贝数据整个过程,进程仍然是阻塞状态
windows创建线程、IO模型、同步异步_第2张图片
描述:当用户发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它马上就将数据拷贝到了用户内存,然后返回。
特点:用户进程需要不断的主动询问kernel数据是否准备好。
非阻塞相比阻塞:

优点:能够在等待任务完成的时间里干其他活了,包括提交其他任务,“后台”可以有多个任务同时执行。
缺点:任务完成的响应延迟增大了,因为每过一段时间去轮询一次read操作,而任务可能在2次轮询之间的任意时间内完成。这会导致整体数据吞吐量的降低。

(三)IO多路复用
windows创建线程、IO模型、同步异步_第3张图片
有些地方也称这种IO方式为event driven IO。
select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。
它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
  当用户进程调用了select,那么整个进程会被block,而同时,kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。
特点:通过一种机制使得一个进程能同时等待IO文件描述符,内核监视这些文件描述符(套接字描述符),其中的任意一个进入读就绪状态,这三个函数就可以返回。
  这里看似和阻塞IO没什么区别,但是阻塞IO只调用了一个system call,(recvfrom),此处有2个系统调用(select也要算上)。select优势在于可以处理多个连接。
  在IO多路复用中,实际上,对于每一个socket,一般都设置为非阻塞的,但是,如上图所示,整个用户的进程是一直被block的。只不过是process是被select这个函数block,而不是socket的IO block,所以,IO多路复用是阻塞在select,epoll这样的系统调用上,而没有阻塞在真正的IO系统调用如recvfrom上。
  在I/O编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者I/O多路复用技术进行处理。
  I/O多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降底了系统的维护工作量,节省了系统资源,I/O多路复用的主要应用场景如下:

服务器需要同时处理多个处于监听状态或者多个连接状态的套接字。
服务器需要同时处理多种网络协议的套接字。

IO多路复用:
在阻塞到select阶段时,用户进程是主动等待并调用select函数获取数据就绪状态就绪信息,并且其进程状态为阻塞。
前三种区别:

第一个阶段不同,但是第二个阶段都是阻塞的。

从整个IO过程来看,他们都是顺序执行的,因为可以归为同步模型,都是进程主动等待且向内核检查状态。
(四)信号驱动式IO
windows创建线程、IO模型、同步异步_第4张图片
(五)异步非阻塞IO
不是顺序执行。用户进程进行aio_read系统调用后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等待socket数据准备好了,内核直接赋值数据给进程,然后从内核向进程发送通知,IO2个阶段,都是非阻塞的。
windows创建线程、IO模型、同步异步_第5张图片
Linux提供了AIO库函数实现异步,但是用的很少,目前有很多的开源异步IO库,例如libevent,libev,libuv。异步过程如上图。

什么场合用到阻塞方式
1.业务逻辑需要的是做完一件事后去做另一件事。
2.为了降低响应延迟,如果采用非阻塞方式,一个任务A被提交到后台,就开始做另一件事B,但是A处理的很快,B没做完,A就好了,保存B的中间状态是耗时的。
windows创建线程、IO模型、同步异步_第6张图片

六、同步、异步、阻塞、非阻塞

参考:https://www.jianshu.com/p/aed6067eeac9
https://blog.csdn.net/hguisu/article/details/7453390

1.同步、异步

同步:一个任务的完成需要依赖另一个任务时,只有等待被依赖的任务完成后,才能执行下去。
异步:不需要等待被依赖的任务完成,它也会立即执行。

2.消息通知

当一个同步调用发出后,调用者要一直等待返回消息(结果)通知后,才能进行后续的执行;当一个异步过程调用发出后,调用者不能立刻得到返回消息(结果),实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

3.阻塞与非阻塞

阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度来说的。
注意:阻塞和同步调用,实际是不同的
原因对于同步调用来说,很多时候,当前线程可能还是激活的,只是从逻辑上当前函数没有返回而已,此时,这个线程可能也会处理其他消息。
(1)如果这个线程在等待当前函数返回时,仍在执行其他消息处理,那这种情况就是同步非阻塞。
(2)如果这个线程在等待当前函数返回时,没有执行其他消息处理,而是处于挂起等待状态,叫做同步阻塞。

4.同步/异步与阻塞/非阻塞

1.同步阻塞:
效率最低(银行专心排队)
2.异步阻塞:
异步可以是被阻塞的,只不过它不是在处理消息时阻塞,而是在等待消息通知时阻塞,比如select函数(在银行取了小票,等待被叫)
3.同步非阻塞:
效率低下(一遍打电话一遍抬头看队伍,需要来回切换)
4.异步非阻塞
效率更高(打电话是你(等待者),柜台(消息触发机制))
windows创建线程、IO模型、同步异步_第7张图片
windows创建线程、IO模型、同步异步_第8张图片
  从上面的例子来看:同步似乎等价于阻塞,异步则等价于非阻塞。其实有些狭义,但不可否认的是,在一定情况下,确实可以这么认为;因为同步一定存在着阻塞状态,而异步一定不存在非阻塞的状态。 但是不是就是说 同步调用 == 阻塞调用呢?然并不是;*阻塞和非阻塞强调的是程序在等待调用结果(消息,返回值)时的状态.* 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。 对于同步调用来说,很多时候当前线程还是激活的状态,只是从逻辑上当前函数没有返回而已,即同步等待时什么都不干,白白占用着资源同步和异步强调的是消息通信机制 (synchronous communication/ asynchronous communication)。所谓同步,就是在发出一个”调用”时,在没有得到结果之前,该“调用”就不返回。但是一旦调用返回,就得到返回值了。换句话说,就是由“调用者”主动等待这个“调用”的结果。而异步则是相反,”调用”在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在”调用”发出后,”被调用者”通过状态、通知来通知调用者,或通过回调函数处理这个调用

七、并发与并行的理解:

高并发的程序一般使用同步非阻塞方式而非多线程 + 同步阻塞方式。

参考:https://blog.csdn.net/java_zero2one/article/details/51477791
并发是指一个处理器同时处理多个任务。
并行是指多个处理器或者是多核的处理器同时处理多个不同的任务。
并发是逻辑上的同时发生(simultaneous),而并行是物理上的同时发生。
比喻:并发(1个人同时吃3个馒头) 并行(三个人同时吃三个馒头)

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。

并行:在多处理器中存在
并发:可以在单处理器也可以在多处理器中都存在。

并发:是并行的假象,并发只是要求程序假装同时执行多个操作(通过时间片轮转,多个操作快速切换执行)
多有多个线程在操作时,只有一个cpu,则它根本不可能真正同时地进行一个以上的线程,它只能把CPU运行时间划分成若干个时间段,再将时间段分配给各个线程执行,在一个时间段的线程代码运行时,其它线程处于挂起状态.这种方式我们称之为并发(Concurrent)。
windows创建线程、IO模型、同步异步_第9张图片

八、select/poll/epoll的区别

参考:https://blog.csdn.net/hguisu/article/details/7453390
epoll是Linux特有,select是POSIX所规定,一般OS均有实现。
select本质:通过设置或者检查存放fd标志位的数据结构来进行下一步处理。缺点:
1.单个进程可监听的fd数量被限制,能监听端口的大小有限。
一般这个大小和内存有关,32位机器默认为1024个。
2.对socket进行扫描是线性扫描,轮询的方式,效率较低。
3.需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时赋值开销大。
poll:
本质和select没区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完发现所有fd没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历fd,这个过程经历了多次无畏的遍历。
它没有最大的连接数的限制,原因是它是基于链表来存储的,但是也有缺点:
1.大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义。
2.水平触发,如果报告了fd后,没有被处理,那么下次poll时会再次报告该fd。
epoll:
支持水平和边缘触发,便于触发只告诉进程哪些fd刚刚变为就绪态,并且只会通知一次,还有一个特点是,epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用callback的回调机制来激活fd,epoll_wait便可以收到通知。
epoll优点:
1.没有最大并发数量的限制。
2.效率提升:不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数
3.内存拷贝:利用mmap()文件映射内存加速与内核空间的消息传递;即epoll使用mmap减少复制开销。
select、poll、epoll 区别总结:
1.支持一个进程所能打开的最大连接数:
epoll虽然连接数有上限,但是很大,1G内存的机器可以打开10万左右的连接;
2.FD剧增后带来的IO效率问题
select:因为每次调用都会对连接进行线性遍历,随着FD的增加会造成遍历速度慢的“线性下降性能问题”。
poll:同上
epoll:因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面2者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能有性能问题。
3.消息传递方式
select 内核需要将消息传递到用户空间,都需要内核拷贝操作
poll同上
epoll:通过内核和用户空间共享一块内存来实现的。
总结:
结合使用场合选择使用:
1.连接数少并且连接都十分活跃的情况下,select和poll的性能可能会更好,毕竟epoll的通知机制需要很多函数回调。
2.select低效是因为它每次都要轮询。

你可能感兴趣的:(操作系统)