2019/7/28-C++线程和进程总结

本文详细介绍了C++中的线程和进程概念,包括线程的创建、属性、同步机制如互斥锁、条件变量、读写锁和信号量。还探讨了进程的创建、结束方式以及进程间通信的多种方法,如管道、共享内存、信号量、消息队列和套接字。此外,提到了Linux中的常见信号及其处理方式。
摘要由CSDN通过智能技术生成

线程

什么是线程
线程是处理器调度的基本单位。
线程的栈空间
线程的栈空间开辟在进程的堆区,线程和它所属的进程共享进程的用户空间,所以线程栈之间可以互访。线程栈的起始地址和大小放在pthread_attr_t中,栈大小用来初始化避免栈溢出的缓冲区大小。
线程的常用函数

  • pthread_create:创建一个线程,并设置线程标识符、线程的属性、线程运行函数的起始地址、线程函数的参数。创建成功返回0,失败返回-1;(如果线程调用的函数写在一个类中,应把这个函数声明为静态)
  • pthread_join:用来等待线程的结束,并获取线程的返回值。调用这个函数的线程在线程结束之前将处于阻塞状态。
  • pthread_exit:用于线程主动关闭,参数可以传入线程的返回值,join可以获取这个返回值。
  • pthread_self:用于获取当前线程的id;
  • pthread_detach:分离一个线程,输入参数为线程标识符。

线程的属性

  • 分离状态:如果线程终止的时候,线程处在分离态,主线程将不保留线程的终止状态。
  • 栈地址:有些系统不支持栈属性。当进程栈地址空间不够用的时候,指定新建线程使用malloc分配的空间作为自己的栈空间。
  • 栈大小
  • 栈保护区大小:在线程栈顶留出空间用来防止栈溢出。
  • 线程优先级
  • 是否继承父进程优先级
  • 争用范围:与系统中所有线程进行竞争/与进程中其他线程进行竞争
  • 线程并行级别:POSIX指定了三种线程调度策略:先入先出策略、循环策略、自定义策略。先入先出策略对每个优先级都是用不同的队列;

多线程同步

  • 互斥锁

互斥锁是一个特殊的变量,一般被设置成全局变量。自身拥有打开(unlock)和锁上(lock)两种状态。打开状态的互斥锁可以由某个线程获得,一旦获得,锁会转入锁上状态。其他想要获得互斥锁的线程,会被阻塞直到互斥锁被再次打开。

互斥锁并不限制两个线程对同一个变量的访问操作,需要程序员自己完善程序,在访问共享变量之前先去获得互斥锁。

锁的创建有动态创建和静态创建两种方式;

常用函数:
pthread_mutex_init()
pthread_mutex_lock()
pthread_mutex_trylock()  //在锁被占据的时候会返回EBUSY
pthread_mutex_unlock()
pthread_mutex_destory()

  • 条件变量

条件变量通过允许线程阻塞和等待另一个线程发送信号的方法弥补互斥锁的不足。一般用在当满足某一个条件之后,通知并唤醒另一个阻塞状态的线程执行接下来的处理任务。例如:生产者-消费者模型。当生产者每次添加一个元素到待处理的队列中时,生产者通过条件变量唤醒阻塞状态的消费者完成元素的后处理操作。

条件变量也有动态和静态创建两种方式;

常用函数:
pthread_cond_init()
pthread_cond_wait()
pthread_cond_timedwait()
pthread_cond_signal()    //按照入队顺序激发一个等待该条件的线程,最多发一个信号
pthread_cond_broadcast()  //有惊群现象
pthread_cond_destory()

  • 读写锁

也叫排它锁和共享锁。读写锁比起互斥锁有更高的适用性和并行性,可以有多个线程同时占用读模式的读写锁,但是只能有一个线程占用写模式的读写锁。

读写锁最适用于对数据结构的读操作次数多于写操作次数的场合。处理读者-写者问题的两种常见策略是 强读者同步强写者同步 。强读者同步中,总是给读者更高的优先权,只要写者没有进行写操作,读者就可以获得访问权限,比如:图书馆查阅系统。强写着同步中总是给写者更高的优先权,读者只能等待写者或者正在等待的写者结束以后才能执行,比如航班订票系统。

常用函数:
pthread_rwlock_init()
pthread_rwlock_rdlock()
pthread_rwlock_wrlock()
pthread_rwlock_unlock()
pthread_rwlock_tryrdlock()
pthread_rwlock_trywrlock()
pthread_rwlock_destory()

  • 信号量

信号量和互斥锁的区别:互斥锁只允许一个线程进入临界区,信号量允许多个线程进入临界区。

常用函数:
//初始化一个sem指向的信号对象,pshared用于设置信号量的类型,为0表示是当前进程的局部信号量,否则可以再多个进程间共享。value为sem的初始值。即为允许进入临界区的线程数量。
int sem_init(sem_t* sem, int pshared, unsigned int value);
//以原子操作的方式将信号量的值减一
sem_wait()
//以原子操作的方式将信号量的值加一
sem_post()
sem_destory()

线程间通信

对于线程间的通信可以分为两类:

  1. 共享内存
  2. 消息传递

C++11提供的线程间通信的方式主要有三个

  1. 互斥锁:mutex/lock_guard/unique_lock
  2. 条件变量:condition_variable
  3. 读写锁:share_lock
  4. 信号量c++11没有实现,但是可以自己使用互斥量和条件变量结合写一个功能类似的实现。

注意:
“线程内”可以多次使对同一互斥量(临界区、事件)进行多次上锁,这叫做“锁的重入”。
锁分为可重入锁和不可重入锁。
如果在一个线程中递归多次上具有非递归属性的锁,会导致死锁。

//实现三个线程按照先后顺序打印ABC
#include<iostream>
#include<mutex>
#include<thread>
#include<condition_variable>
#include<windows.h>

std::mutex _mutex;
std::condition_variable cv1,cv2,cv3;

void func1() {
 for (int i = 0; i < 10;i++) {
  std::unique_lock<std::mutex> lock(_mutex);
  cv1.wait(lock);
  printf("%d: A\n",std::this_thread::get_id());
  Sleep(200);
  cv2.notify_one();
 }
}
void func2() {
 for (int i = 0; i < 10; i++) {
  std::unique_lock<std::mutex> lock(_mutex);
  cv2.wait(lock);
  printf("%d: B\n", std::this_thread::get_id());
  Sleep(200);
  cv3.notify_one();
 }
}
void func3() {
 for (int i = 0; i < 10; i++) {
  std::unique_lock<std::mutex> lock(_mutex);
  cv3.wait(lock);
  printf("%d: C\n", std::this_thread::get_id());
  Sleep(200);
  cv1.notify_one();
 }
}
int main()
{
 std::thread thread1(func1);
 std::thread thread2(func2);
 std::thread thread3(func3);
 cv1.notify_one();
 thread1.join();
 thread2.join();
 thread3.join();
 system("pause");
 return 0;
} 

unique_lock和lock_guard的区别:

  1. lock_guard是RAII模板类的简单实现,功能简单。lock_guard在构造函数中加锁,在析构函数中解锁,在实际编程中可以根据自己的需求编写RAII类避免忘记释放资源。
  2. unique_lock是通用的互斥包装器,比lock_guard使用更加灵活,功能也更加强大。经常和条件变量一起使用,并且一般需要付出更多的性能和时间成本。

进程

什么是进程
进程是并发执行的程序在执行过程中分配和管理资源的基本单位。也是计算机中处于运行状态下的程序的实体。

系统通过进程控制块(PCB)感知进程的存在和对进程进行管理和调度。PCB处在进程核心堆栈的底部。

进程和线程的区别

  • 进程是并发执行的程序在执行过程中分配和管理资源的基本单位,线程是进程执行的一个单元,是进程内的调度实体。
  • 一个程序至少有一个进程,一个进程至少有一个线程。
  • 进程可以独立执行,线程不能独立执行。
  • 同一个进程下的所有线程共享进程的地址空间,但进程和进程之间是相互独立的。
  • 一个进程崩溃之后不会对其他进程产生影响,线程崩溃整个进程都会死掉。
  • 线程是处理器调度的基本单位,进程不是。
  • 线程的执行开销小,不利于资源的管理和保护,适合在多cpu的系统下运行。
  • 进程执行开销大,但是能够很好地进行资源管理和保护。
  • 两个都可以并发执行。

程序的生成主要分为四个阶段

  • 预编译:

预编译器把源文件和头文件作为输入,处理生成.i文件。预编译的规则如下:

  1. 展开所有的宏定义#include
  2. 处理#ifdef、#if、#else等预编译指令
  3. 删除所有的注释信息
  4. 添加行号和文件名标识
  5. 处理#include预编译指令
  6. 保留所有的#pragma编译器指令
  • 编译:

编译器将预编译完的文件,经过词法分析,语法分析,语义分析,优化,最终生成相应的.s汇编代码文件,共计五个阶段。

  • 汇编:

将汇编语言输入,产生扩展名为.o的目标文件

  • 链接:

链接器负责将程序的目标文件和所有附加的目标文件(包括静态链接库和动态链接库)连接起来,生成可执行程序。

程序转化为进程的三个步骤

  1. 内核把程序读入到内存中,为程序分配内存空间。
  2. 内核为程序分配进程标识符PID以及进程需要的其他资源。
  3. 内核保存进程PID和其他的状态信息,然后将进程加入到运行队列中等待执行,程序转化为进程后就可以被操作系统的调度程序调度执行了。

进程的创建有两种方式

  1. 由操作系统创建
  2. 由父进程通过fork()函数创建

fork()函数的里里外外

  1. 父进程调用fork()函数之后会创建一个新的进程。并在内核中为这个进程分配一个新的可用的进程标识符(PID),之后,为进程分配新的进程空间,并且将父进程的进程空间中的内容复制到子进程的进程空间中,包括堆栈段和数据段,并且和父进程共享代码段。子进程因为复制了父进程的堆栈段,所以两个函数都停留在了fork()函数中等待返回。fork()函数此时会返回两次,一次在父进程,一次在子进程,并且函数的返回值不同。
  2. 父进程的fork返回值,调用成功时返回子进程的PID,调用失败时返回小于0的值;子进程的fork返回0;
  3. getpid返回当前进程的pid,getppid返回父进程的id
  4. 在fork()完成之后子进程修改静态变量,全局变量均不会对父进程的同名变量产生影响。
  5. 写时复制:现在版本的linux内核一般不会再fork之后立刻复制父进程的进程空间,而是当子进程修改数据内容的时候复制操作才会发生。

进程的结束

进程结束分为正常结束和异常结束。

  1. 正常退出
  • main函数执行return
  • 调用exit()函数
  • 调用_exit()函数
  1. 异常退出
  • 调用abort函数
  • 进程收到某个信号使得程序终止

注意:return是函数执行完后的返回,return执行完后把控制权交给调用函数;exit()是函数,带有参数,执行完后将控制权交给系统。

exit()和_exit()的区别和联系

  1. 都是用于终止进程的函数;
  2. _exit()执行后立刻返回给内核,exit()要先执行一些清除操作,且exit()是在_exit()函数基础上的封装。
  3. exit()函数会检查文件的打开情况,把文件缓冲区的内容写回到文件(缓冲IO)。_exit()不会,如果缓冲区中有未写入的内容,直接调用_exit会导致数据丢失。

僵尸进程: 子进程先于父进程退出,并且父进程未调用wait或waitpid函数获取子进程的状态信息,子进程的进程描述符会一直存在于系统中,成为僵尸进程。
孤儿进程: 父进程先于子进程退出,子进程会被init进程收养成为孤儿进程。之后的状态收集由init进程完成。

守护进程:

linux操作系统中系统引导的过程中会开启很多服务,这些服务叫做守护进程。守护进程一般脱离终端的控制运行在系统后台,且生存期较长。

创建一个守护进程主要有五步:

  1. 父进程调用fork函数产生一个子进程后,让父进程退出,使得子进程成为孤儿进程。
    第一步执行完后,守护进程在形式上就脱离了终端的控制。
  2. 子进程调用setsid创建一个新的会话。由于子进程在创建过程中,进程空间的内容是由父进程拷贝而来的,所以进程的会话期,控制组,控制终端并没有发生改变,不是真正意义上的独立。setsid可以让进程完全独立出来,从而摆脱其他进程的控制。
  3. 改变当前目录为根目录。这步如果不执行,会使得进程所在的文件系统不能正常的解除挂载。
  4. 重新设置文件权限掩码。umask(0);增加问价的访问权限,增强守护进程的灵活性。
  5. 关闭文件描述符。子进程由fork继承来的文件描述符0,1,2不再使用(分别对应标准输入,标准输出,标准错误),应该被关闭。

进程间通信

  1. 管道
    管道本质上也是个文件。分为有名管道和无名管道。无名管道常用于父子进程或者兄弟进程等具有亲缘关系的进程间的通信,以文件的形式存在内存中。管道是个半双工的进程间通信方式,数据以字节流的形式从管道的一端传递到另一端,需要输入方和输出方事先约定好数据格式。通常需要关闭一个读端和一个写端来实现,如果想要实现双端通信必须创建两个管道。并且管道本身有消息的单次写入最大值得长度限制。
    有名管道提供了一个路径名与之关联,以FIFO的文件形式存在于文件系统中。使得即使没有亲缘关系的进程,只要能够访问这个路径就可以通过FIFO进行通信。无名管道的消息传递通过内核实现,有名管道通过文件系统。
    流管道可以双向通信。
  2. 共享内存
    允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个进程之间传递数据的一种非常有效的方式。不同进程之间共享的内存通常被安排在同一段物理内存中,进程可以将同一段共享内存连接到自己的地址空间,所有的进程都可以访问共享内存中的地址。
    但共享内存并未提供同步机制。
  3. 信号量
    信号量分为POSIX信号量和SYSTEMV信号量。信号量是一个计数器,可以用来控制多个线程对共享资源的访问,它不是用于交换大批数据,而用于多线程之间的同步。他常作为一种锁机制。因此,主要作为进程间以及同一个进程内不同线程之间的同步手段。
  4. 消息队列
    消息队列是消息的链表,存放在内核中并由消息队列标识符标识,消息队列克服了信号传递信息少,管道只能承载无格式字节流以及缓冲区大小受限等特点。
    消息队列用于同一台机器上的进程间通信,和管道很相似,他是在系统内核中保存消息的队列,在系统内核中以消息链表的形式出现。使用消息队列进行通信的进程可以是不想关的进程,每个数据都有一个最大长度限制。
    相比命名管道:1.消息队列可以独立于发送和接受进程存在。2.可以避免命名管道存在的同步和阻塞问题。3.接收程序可以根据消息类型有选择的接收程序,命名管道只能默认的接收。
    消息队列是一种正逐渐被淘汰的通信方式,完全可以用流管道或者套接口的方式来取代。
  5. 套接字
    TCP/UDP/UNIX域
  6. 信号:
    信号是一种比较复杂的通信方式,用于通知接收进程某个时间已经发生。
    信号产生的条件:按键、硬件异常、进程调用kill函数将信号发送给另一个进程、用户调用kill命令将信号发送给其他进程,传递的消息比较少,主要是通知消息。

linux常见信号

  1. SIGKILL:用户调用kill -9产生的信号,会直接终止进程的运行。这个信号不能被忽略或者捕获,是杀死进程的终极武器。
  2. SIGINT:ctrl+c发送这个信号,用于终止进程。
  3. SIGSTOP:停止进程的运行,和SIGKILL一样不能被应用程序处理。程序还未结束,只是暂停运行。
  4. SIGCHILD:高性能服务器需要关注的信号,如果主线程不关注fork之后产生的进程的退出状态,为了防止产生将至进程,可以忽略SIGCHILD信号将回收资源的任务交给init进程完成。子进程不会再产生僵尸进程。
  5. SIGPIPE:在网络编程中,向一个关闭了读端的套接字写数据,首先会收到一个reset报文,如果服务器再次调用write,就会产生SIGPIPE信号。系统的默认处理方式是关闭这个进程,但对于高可用服务器来说,需要手动处理这个信号。
  6. SIGSEGV:试图访问一个未分配给自己的内存,或者向没有写权限的内存中写入数据。主要有三个情况:野指针,栈溢出,非法文件访问。

linux进程有三种方式处理收到的信号:

  1. 忽略,不对信号做任何处理,其中SIGKILL和SIGSTOP不能被忽略。
  2. 捕获,定义信号处理函数,当信号发生时,执行相应的处理函数。
  3. 执行默认操作。linux对每个信号都规定了默认操作。

linux内存映射mmap原理分析

  1. 内存映射是将用户空间的一端内存区域映射到内核空间。映射成功后,用户空间对内存的改动会直接反映到内核空间,对于内核空间到用户空间的大数据传输,效率会非常高。
  2. 他本身提供了另一种不同于一般的对文件访问的方式,普通文件被映射到进程地址空间后,可以想操作内存一样对文件进行访问,而不需要read,write函数,mmap不会分配空间,只是将文件映射到地址空间(会占掉虚拟内存),可以用memcpy等操作写文件,文件不会被立刻更新到磁盘,可以调用msync()来显式的同步。在mmap的过程中也不会产生数据拷贝,只有对内存进行访问的时候,系统会产生缺页中断,然后才会根据mmap的映射关系将数据读入物理内存。因此,效率上要比read和write要高,因为read和write要先将数据同步到内核缓冲区,然后再拷贝到用户态或者存入磁盘,这个过程中实际发生了两次数据拷贝。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值