多进程和多线程的基本了解 (C/C++、Linux)

多进程

想了解多进程,首先就要知道进程的定义。

进程是操作系统中最基本、重要的概念。是多道程序系统出现后,为了刻画系统内部出现的动态情况,描述系统内部各道程序的活动规律引进的一个概念,所有多道程序设计操作系统都建立在进程的基础上。

进程的定义

狭义定义:进程是正在运行的程序的实例。
广义定义:进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动。它是操作系统动态执行的基本单元,在传统的操作系统中,进程既是基本的分配单元,也是基本的执行单元。

进程和线程概念、进程和线程的关系

进程简单来说,就是一个可以独立运行的程序单位。一个进程可以包含一个或多个线程,即线程的集合。 线程是操作系统运行调度的最小单位,线程是进程中的最小的一个单元。

多进程的定义

在操作系统中,你每运行一个程序,就相当于你运行了一个进程。多进程就是计算机同时执行了多个进程。例如:你的电脑同时打开了微信、网易云音乐、游戏…

1、fork()

fork() 函数被调用一次,但返回两次,可能会有三个不同的返回值。
1)在父进程中,fork返回新创建子进程的进程ID;
2)在子进程中,fork返回0;
3)如果出现错误,fork返回一个负值;
故我们可以通过判断返回值来区分父子进程。如果不做判断,fork() 后的代码父子进程会各执行一遍。

示例:测试fork()函数

#include<iostream>
#include<unistd.h>

using namespace std;

int main(){
    
    
	cout<<"主进程getpid() pid = "<<getpid()<<endl;
	auto pid = fork();
	if(pid == 0){
    
    
		// 子进程
		cout<<"子进程 pid = "<<getpid()<<endl;
	}
	else if(pid == -1){
    
    
		cout<<"创建子进程失败"<<endl;
	}
	else{
    
    
		// 父进程
		cout<<"子进程 pid = "<<pid<<endl;
	}

	return 0;
}

fork()系统调用会创建一个子进程,这个子进程是父进程的一个副本。这也就意味着,系统在创建子进程成功后,会将父进程的文本段、数据段、堆栈都复制一份给子进程,但子进程有一份自己独立的空间。 子进程对这些内存的修改并不会影响父进程空间的相应内存。 这时系统中出现两个基本完全相同的进程(父、子进程),这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的调度策略。 如果需要确保让父进程或子进程先执行。 则需要程序员在代码中通过进程间通信的机制来实现。

插入知识点
  • IPC进程间通信
    1、管道
    2、共享内存
    3、消息队列
    4、信号(开销小)
    5、信号量
    6、socke

  • 僵尸进程:子进程先结束,而父进程没有回收子进程,没有释放子进程占用的空间,此时子进程将成为一个僵尸进程。

  • 孤儿进程:父进程先结束,而子进程仍然存活,此时子进程成为孤儿进程,将由系统的init进程负责回收相关资源。

2、vfork()

vfork()也可以用来创建进程,它与fork()的用法相同,也用于创建一个新的进程。
但vfork()并不将父进程的地址空间完全复制到子进程中,因为子进程会立刻调用exec或者exit(),于是也就不会引用地址空间了。不过子进程再调用exec()或exit()之前,他将在父进程的空间中运行,但如果子进程想尝试修改数据域(数据段、堆、栈)都会带来未知的结果,因为他会影响了父进程空间的数据可能导致父进程的执行异常。 此外,vfork()会保证子进程先运行,在他调用了exec或exit()之后父进程才可能被调度运行。如果子进程以来父进程的进一步动作,则会导致死锁。

3、wait()和waitpid()

(1) wait()函数

进程一旦调用了wait(),就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);

参数

  • status:参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像这样:pid = wait(NULL);

返回值
如果成功,wait会返回被收集的子进程的进程ID;
如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD。

(2) waitpid()函数

从本质上讲,系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,从而为我们编程提供了另一种更灵活的方式。

#include <sys/wait.h>
#include <sys/types.h>
pid_t waitpid(pid_t pid, int *status, int options)

参数

  • status:用法与wait()中相同。
  • pid:从参数的名字pid和类型pid_t中就可以看出,这里需要的是一个进程ID。但当pid取不同的值时,在这里有不同的意义。

① pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去(阻塞在waitpid()函数这里);
② pid=-1时,等待任何一个子进程退出,没有任何限制,此时waitpid和wait的作用一模一样;   
③ pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬;
④ pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值;

  • options:提供了一些额外的选项来控制waitpid,目前在Linux中只支持:WNOHANG和WUNTRACED两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用;比如ret=waitpid(-1,NULL,WNOHANG | WUNTRACED); 如果我们不想使用它们,也可以把options设为0,如ret=waitpid(-1,NULL,0);
    WNOHANG参数:调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。
    WUNTRACED参数:由于涉及到一些跟踪调试方面的知识,加之极少用到,可以自行查阅相关材料。

返回值
waitpid的返回值比wait稍微复杂一些,一共有三种情况:
① 当正常返回的时候,waitpid返回收集到的子进程的进程ID;
② 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;      
③ 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;当pid所指示的子进程不存在,或此进程存在,但不是调用进程的子进程,waitpid就会出错返回,这时errno被设置为ECHILD;

多线程

线程的定义

线程:是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

多线程的定义

多线程:是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。在一个程序中,这些独立运行的程序片段叫作“线程”,利用它编程的概念就叫作“多线程处理” 。

1、thread()

thread()函数用于创建一个线程。

构造、析构函数

函数 类别 作用
thread() noexcept 默认构造函数 创建一个线程,什么也不做
template <class Fn, class… Args> explicit thread(Fn&& fn, Args&&… args) 初始化构造函数 创建一个线程,以args为参数执行fn函数
thread(thread&& x) noexcept 移动构造函数 构造一个与x相同的对象,会破坏x对象
~thread() 析构函数 析构对象

常用成员函数

函数 作用
void join() 等待线程结束并清理资源(会阻塞)
bool joinable() 返回线程是否可以执行join函数
void detach() 将线程与调用其的线程分离,彼此独立执行(此函数必须在线程创建时立即调用,且调用此函数会使其不能被join)
std::thread::id get_id() 获取线程id

示例:测试thread()

#include<iostream>
#include<thread>
using namespace std;

void helloworld(){
    
    
	cout<<"hello world"<<endl;
}
void f1(string str){
    
    
	cout<<str<<endl;
}
void main(){
    
    
	thread t(helloworld);
	t.join();
	cout<<"t1结束"<<endl;

	thread t2([]{
    
    
		cout<<"hello world thread"<<endl;
	});
	t2.join();
	cout<<"t2结束"<<endl;

	thread t3(f1,"transfer parameter");
	t3.join();
	cout<<"t3结束"<<endl;
	return 0;
}

2、mutex互斥锁

多个线程访问同一资源时,为了保证数据的一致性,最简单的方式就是使用 mutex

相关方法
C++11中提供了std::mutex互斥量,共包含四种类型:

  • std::mutex:最基本的mutex类。
  • std::recursive_mutex:递归mutex类,能多次锁定而不死锁。
  • std::time_mutex:定时mutex类,可以锁定一定的时间。
  • std::recursive_timed_mutex:定时递归mutex类。

另外,还提供了两种锁类型:

  • std::lock_guard:方便线程对互斥量上锁。
  • std::unique_lock:方便线程对互斥量上锁,但提供了更好的上锁和解锁控制。

以及相关的函数:

  • std::try_lock:尝试同时对多个互斥量上锁。
  • std::lock:可以同时对多个互斥量上锁。
  • std::call_once:如果多个线程需要同时调用某个函数,call_once可以保证多个线程对该函数只调用一次。

本文基于整理,原文参考链接:

  1. https://blog.csdn.net/weixin_53361650/article/details/114635696
  2. https://blog.csdn.net/aLingYun/article/details/95934465
  3. https://blog.csdn.net/bandaoyu/article/details/109541053
  4. https://blog.csdn.net/yueguangmuyu/article/details/118580089
  5. https://blog.csdn.net/sinat_31608641/article/details/107733436

猜你喜欢

转载自blog.csdn.net/ProSWhite/article/details/129424906
今日推荐