多线程基础之七:多线程遇上printf的“延迟写”策略


0. 运行库提供的IO读写函数采用“延迟写”策略的原因

编程时经常会用到printf()函数,但是由于printf()函数涉及到和显示器或磁盘等外设进行交互,所以操作涉及到从“用户态–>内核态–>返回用户态”的一系列内核转换过程,但是从用户态通过中断使用系统调用涉及到内核从用户态切换到内核态,上下文切换是间很费时的操作。更糟糕的是,如果printf()的目标设备是显示器这种字符设备(单次传输一个字节),那么显然不能每输入一个字节就通过中断调用系统API输出一个字节,这显然会导致CPU浪费掉大量的时间在无意义的上下文切换(内核态转换)。

再一次的参考IT的万能定律—银弹,如果一个问题不好解决,那就加一层抽象层。所以平常使用的printf()函数并非是操作系统直接提供的API,而是由运行库在操作系统API (如write()系统调用 )基础上封装来的函数。简单地说,运行库为IO读写添加的一层抽象层的基本策略是在系统调用( read()或write() )基础上额外配备缓冲区和采用“延迟写”策略,如果你对细节感兴趣可以参考我的另一篇文章——运行库:Windows下MSVC CRT运行库封装fread()函数解析。其中printf()函数的”批量预读“和”延迟写“策略在单线程的环境下并不会影响我们的正常使用,但是printf()函数的”延迟写“碰上多线程环境则可能让最后输出结果并不能按照预想的那样。

以printf函数为例,”延迟写“策略思想是指用户态下调用printf()函数,除非满足以下四种条件,否则并不立即从用户态转入内核态调用系统API输出,而是先保存在缓冲区(缓冲操作依旧运行在用户态下,故而免去了内核切换状态),直到满足以下四种条件,才切换到内核态,进行集中输出
1. 缓冲区填满;
2. 写入的字符中有”\n” “\r”;
3. 调用fflush手动刷新缓冲区;
4. 调用scanf从缓冲区中立即读取数据,也会隐式调用fflush。
通过以下的小程序,可以测试在在Linux系统上为IO写操作配备的缓冲区大小
printfBuffer.cpp

#include 
#include 

int main (int argc, char* argv[])
{
    int i = 0x61; //0x61是a的ACSCII码
    while (1)
    {
        printf("%c", i);
        i++;
        i = (i - 0x61) % 26 + 0x61;
        usleep(1000);
    }
    return 0;
}

在屏幕第一次输出信息时,立即按下crtl+C,然后数一数屏幕中的字母个数便可以判断buffer大小,可以数一数,buffer_size = 1024 byte。这个程序改装后在Windows下运行并不会得到合适的效果(Windows下是在实时输出的,没有Linux下明显的停顿后一次性刷出一屏幕的效果,然后过一会再刷出一屏幕)。
多线程基础之七:多线程遇上printf的“延迟写”策略_第1张图片


1.printf()函数的“延迟写”策略和多线程的碰撞

multiple_thread_use_printf.cpp

#include 
#include 

using namespace std;

DWORD WINAPI ThreadProc1(__in  LPVOID lpParameter);
DWORD WINAPI ThreadProc2(__in  LPVOID lpParameter);

int main()
{
    HANDLE hThread[2] = {0}; 
    hThread[0] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
    hThread[1] = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);

    WaitForMultipleObjects(2,hThread,TRUE,INFINITE);

    for (int i=0; i<2; i++)
    CloseHandle( hThread[i] );

    return 0;
}

DWORD WINAPI ThreadProc1(__in  LPVOID lpParameter)
{
    //cout<<" the first thread:"<<" Hello "<1);
    printf("***The First Thread Range***\n");
    printf("该线程的线程编号NO.%d\n", GetCurrentThreadId());
    return 0;
}

DWORD WINAPI ThreadProc2(__in  LPVOID lpParameter)
{
    //cout<<" the second thread:"<<" World"<"***The Second Thread Range***\n");
    printf("the current thread NO.%d\n", GetCurrentThreadId());
    return 0;
}

编译命令g++ multiple-thread-use-printf.cpp -lpthread -finput-charset=GB2312
正常情况下应该输出
这里写图片描述
或者输出
这里写图片描述
但是实际调用过程中,结果会看到偶尔会出现两个线程的输出顺序彼此交叉(但是很少看到printf单步调用的内容彼此交叉,不过为了保险起见还是如果printf单步调用的内容中没有使用\n,还是应该会用fflush(stdout))。
这里写图片描述

所以应该使用锁机制,保证使用标准输出stdout字符设备时按照设定的顺序批量输出。

multiple_thread_use_printf_with_mutex.cpp

#include 
#include 

using namespace std;

DWORD WINAPI ThreadProc1(__in  LPVOID lpParameter);
DWORD WINAPI ThreadProc2(__in  LPVOID lpParameter);
HANDLE g_hMutex = NULL;

int main()
{
    g_hMutex = CreateMutex(NULL,TRUE,NULL);

    HANDLE hThread[2] = {0}; 
    hThread[0] = CreateThread(NULL, 0, ThreadProc1, NULL, 0, NULL);
    hThread[1] = CreateThread(NULL, 0, ThreadProc2, NULL, 0, NULL);

    ReleaseMutex(g_hMutex);//释放互斥量  
    WaitForMultipleObjects(2,hThread,TRUE,INFINITE);

    for (int i=0; i<2; i++)
    CloseHandle( hThread[i] );

    CloseHandle( g_hMutex );
    return 0;
}

DWORD WINAPI ThreadProc1(__in  LPVOID lpParameter)
{
    WaitForSingleObject(g_hMutex, INFINITE);//等待互斥量
    printf("***The First Thread Range***\n");
    printf("该线程的线程编号NO.%d\n", GetCurrentThreadId());
    ReleaseMutex(g_hMutex);//释放互斥量
    return 0;
}

DWORD WINAPI ThreadProc2(__in  LPVOID lpParameter)
{
    WaitForSingleObject(g_hMutex, INFINITE);//等待互斥量
    printf("***The Second Thread Range***\n");
    printf("the current thread NO.%d\n", GetCurrentThreadId());
    ReleaseMutex(g_hMutex);//释放互斥量
    return 0;
}

你可能感兴趣的:(多线程基础)