c98标准中不支持线程创建,c11标准中才有线程创建支持。
目前windows和linux系统都自带创建进程和线程函数,进程process,线程thread。
1、进程与线程
不管是后台应用还是前台应用,我们一般喜欢说后台程序或者前台程序,即我们可以先理解为进程就是执行中的程序。任何程序一启动就是个(父)进程,同时自身也是一个主线程。从图1中可以看到,启动一个程序会分配很多资源。线程是在进程内创建的,一个进程中的所有线程在同一个地址空间中执行,共享程序的代码和数据结构,如图1中所示。即进程就是程序加上所有在该程序中执行的线程。
比如我们一楼有个图书柜,那这个图书柜的存在就相当于数据已经加载到内存中了,你在那边读计算机系统书,然后我过来拿起一本数据结构书读。对于这个整体,你和我是两个线程,图书柜+你+我是这个进程。如果你我读同一本书就比较麻烦了,就是多线程编程的问题了,两个线程读同一内存数据想想有啥办法不?
图1 程序内存痕迹
2、进程与线程的区别
现在操作系统都是能运行很多程序,一个程序起码有一个进程,一般开机后就会有几十个进程。即使回到20年前,单核的赛扬器,那时候的操作系统启动也是有几十个进程在运行,但是我们还是可以很多事情一起干,比如:一边开比特精灵下载,一边听音乐,还一边玩qq游戏。就是因为这些进程不是一直占有CPU,每个进程都会有个时间片。时间片一到就切换到其他进程在CPU上,时间短到我们觉察不出来。
操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位。线程,有时被称为轻量级进程(Lightweight Process,LWP),是操作系统调度(CPU调度)执行的最小单位。即进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位;线程是进程的一部分。
打个比方:进程是火车,那线程就是其中的车厢。那这个火车有10节车厢,就相当于这个进程有10个线程。如果在同一时间从上海到太原的列车有5趟,就是5俩火车同时开,那就是有5个进程。相比而言,加一趟列车难,加一节车厢容易。如果一节车厢坏了,处理不好会影响这辆火车;但是一趟并行的火车坏了,并不会影响其他趟火车。车厢之间可以相互走动,但各趟火车之间没法走动。
通过火车例子可以看出,同一个进程内的多个线程之间数据可以分享,各个进程之间的数据是隔离的,跨进程数据交互需要通过进程间通讯机制等外力才行;创建进程或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销,但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响;线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。
所以,在进程与线程之间,没有说哪个是最好的,只有跟据不同的应用场景择优使用,一般都是多进程多线程一起使用。
3、系统接口
创建子进程进程
windows:CreateProcess(),参数比较多,可以指定某个可执行程序,不一定是自身程序。
BOOL CreateProcess
(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes。
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
linux:
pid_t fork(void);
返回值大于0表示现在运行的是父进程,等于0表示运行的是子进程,-1表示创建进程失败。
启动进程
#include <unistd.h> int execl(const char *path, const char *arg, ...);
我们后台应用就有这个。假设有个businesscenter和business两个可执行程序,businesscenter相当于business的控制中心/统一调度程序,那么可以让businesscenter程序拉起business程序。具体做法是:在ini配置文件写上business所在的路径、可执行程序名,以及启动几个。如果写了5个,那么在businesscenter程序中就要fork5个子进程。在子进程中调用exec函数,如果失败了会返回,成功了就不会返回了,直到新程序运行结束。fork会将调用进程的所有内容原封不动的拷贝到新产生的子进程中,很耗费资源。所以,操作系统都有做了对应优化。使得fork结束后如果遇到exec函数并不立刻复制父进程的内容,而是到了真正实用的时候才复制。
线程
windows:
HANDLE CreateThread( LPSECURITY_ATTRIBUTES lpThreadAttributes,//SD:线程安全相关的属性,常置为NULL SIZE_T dwStackSize,//initialstacksize:新线程的初始化栈的大小,可设置为0 LPTHREAD_START_ROUTINE lpStartAddress,//threadfunction:被线程执行的回调函数,也称为线程函数 LPVOID lpParameter,//threadargument:传入线程函数的参数,不需传递参数时为NULL DWORD dwCreationFlags,//creationoption:控制线程创建的标志 LPDWORD lpThreadId//threadidentifier:传出参数,用于获得线程ID,如果为NULL则不返回线程ID )
linux:
#include<pthread.h> int pthread_create(pthread_t *tidp,const pthread_attr_t *attr, void *(*start_rtn)(void*),void *arg);
C11:
// thread example #include <iostream> // std::cout #include <thread> // std::thread void foo() { // do stuff... } void bar(int x) { // do stuff... } int main() { std::thread first (foo); // spawn new thread that calls foo() std::thread second (bar,0); // spawn new thread that calls bar(0) std::cout << "main, foo and bar now execute concurrently... "; // synchronize threads: first.join(); // pauses until first finishes second.join(); // pauses until second finishes std::cout << "foo and bar completed. "; return 0; }
4、代码编写难易度
我们写的代码都是顺序执行的,有些场景需要并行,那多线程的价值就存在了。像我们有个程序,一个进程里有两个线程,主线程主要用来是接收其他系统发来的tcp数据;子线程是从管道(文件)中读取数据,然后做业务逻辑处理。
多线程因为可以共享数据,所以多个线程对共享数据的那块内存进行写操作时候,需要加锁,对锁处理不好会引起死锁,忙等待,导致程序的僵死,这块比较难,所以觉得要比写多进程复杂。
参考资料:
1、https://www.zhihu.com/question/25532384
2、https://blog.csdn.net/ThinkWon/article/details/102021274
3、https://blog.csdn.net/daaikuaichuan/article/details/82951084
4、https://www.cnblogs.com/wanghetao/archive/2011/11/06/2237937.html