多线程基础之七:多线程遇上printf的“延迟写”策略
来源:互联网 发布:淘宝达人文章怎么赚钱 编辑:程序博客网 时间:2024/06/03 19:19
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 <stdio.h>#include <unistd.h>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下明显的停顿后一次性刷出一屏幕的效果,然后过一会再刷出一屏幕)。
1.printf()函数的“延迟写”策略和多线程的碰撞
multiple_thread_use_printf.cpp
#include <iostream>#include <Windows.h>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 "<<endl; //Sleep(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"<<endl; printf("***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 <iostream>#include <Windows.h>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;}
- 多线程基础之七:多线程遇上printf的“延迟写”策略
- 当printf遇上多线程
- 多线程之延迟操作
- 多线程之策略模式
- iOS 多线程之延迟操作
- java多线程之延迟初始化
- 单例遇上多线程
- Java基础<七>_多线程
- Android之JAVASe基础篇-面向对象-多线程(七)
- 多线程之四大拒绝策略
- .NET 4.0多线程开发之 “对象的延迟创建与多线程安全访问”
- C++多线程编程之七
- Java多线程之七锁
- (七)java多线程之Condition
- 228,多线程之延迟加载实例
- 多线程之二,windows下的多线程-基础
- java基础之 多线程
- java基础之 多线程
- java客户端调用restful接口
- 6.6 接口与多态
- Undefined symbol xQueueCreateCountingSemaphore 问题解决
- Python的那些事:range() 与 xrange()
- codeforces 893F Subtree Minimum Query 线段树合并
- 多线程基础之七:多线程遇上printf的“延迟写”策略
- linux系统管理命令
- 【Python】Python操作MySQL数据库快速入门
- 常见进程通信方式
- 正余弦函数及其反函数导数推导
- KMP算法及其扩展应用
- 《弃子长安》第七章 真相迷离
- 第一天我的博客
- 某驱动的内核调试检测学习内核调试引擎加载机制