文章目录
- 前言
- 一、线程和进程
- 1. 进程(Process)
- 2. 线程(Thread)
- 3. 二者的比较
- 二、多线程和多进程
- 三. 代码编写
- 1. 相关函数
- pthread_create( )函数
- pthread_exit( )函数
- pthread_join( )函数
- 2. 线程同步
- 3. 互斥量
- 4. 条件变量
- 5. 实验代码
前言
在前面的学习中,我们提到了ROTS操作系统的特点,即可以多线程操作命令,这样的好处是可以同时操作好几个目标,而不是因为上一个目标未结束使得需要的操作陷入阻塞状态。
一、线程和进程
这是一个常用的术语,通常情况下线程和进程是指操作系统中用于实现并发执行的两个基本单位,它们各自具有不同的特点和适用场景。
1. 进程(Process)
定义:进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位。它包含了一个程序的当前执行状态,包括程序计数器、内存指针以及多个寄存器的当前值等。
资源占用:每个进程都拥有独立的内存空间和系统资源,如代码、数据、堆栈等。
独立性:进程之间是相互独立的,一个进程的崩溃不会影响到其他进程。
通信与同步:进程间的通信(IPC)通常通过管道、消息队列、共享内存等方式实现,而同步则需要使用信号量、互斥锁等机制。
开销:由于进程拥有独立的资源,因此创建、销毁和切换进程的开销相对较大。
2. 线程(Thread)
定义:线程是进程内的一条执行路径或执行流,是系统调度的基本单位。线程共享进程的资源,包括代码、数据、打开的文件、信号处理器和进程ID等。
资源占用:线程之间共享进程的内存空间和系统资源,但每个线程拥有自己独立的程序计数器、寄存器和堆栈。
通信与同步:线程间的通信和同步相对简单,可以通过共享内存直接访问,但也需要使用适当的同步机制来避免数据竞争和不一致。
开销:由于线程共享进程的资源,因此创建、销毁和切换线程的开销相对较小。
3. 二者的比较
独立性:进程是完全独立的,而线程则依赖于进程。
资源占用:进程占用独立的资源,而线程共享进程的资源。
开销:进程的创建、销毁和切换开销较大,而线程的开销较小。
并发性:由于线程的开销较小,因此多线程可以实现更高的并发性。
二、多线程和多进程
多进程:进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,也是操作系统结构的基础。当需要运行多个独立的程序或需要完全隔离的资源时,使用多进程是合适的。例如,在服务器上运行多个独立的服务。其特点为:
- 独立性:每个进程都有自己的独立地址空间,包括代码、数据、栈等,彼此之间相互隔离,不会相互干扰或影响。
- 并发性:操作系统可以同时运行多个进程,每个进程独占一定的系统资源,通过切换和调度来实现并发执行。
- 优势:稳定性高(一个进程的崩溃不会影响其他进程的运行)、数据隔离(进程间数据独立,简化了数据同步的问题)。
- 劣势:资源消耗大 (每个进程都需要独立的内存空间和系统资源,因此资源消耗相对较大)、切换开销大(进程上下文切换的开销相对较大,可能影响系统的整体性能)。
多线程:多线程是指从软件或硬件上实现多个线程并发执行的技术。具有多线程能力的计算机能够在同一时间执行多于一个线程,进而提升整体处理性能。当需要在一个程序中并发执行多个任务,且这些任务需要共享某些资源时,使用多线程是合适的。例如,GUI程序中的事件处理、网络编程中的并发连接处理等。其特点大体如下:
- 并发执行:多个线程可以同时执行,不必等待其他线程完成。
- 共享资源:多个线程可以共享同一份资源,例如内存、文件等。
- 独立性:每个线程都有自己的执行上下文和栈空间,彼此之间相互独立。
- 优势:资源消耗少(线程间共享进程资源,所以资源消耗相对较少)、切换快(线程的上下文切换相对较快,有助于提高CPU的利用率)
- 劣势:数据同步困难(由于线程间共享数据,数据同步可能会变得复杂和困难)。
三. 代码编写
1. 相关函数
pthread_create( )函数
//功能:创建一个子线程
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
- 参数
- thread:传出参数,线程创建成功后,子线程的线程ID被写到该变量中。
- attr : 设置线程的属性,一般使用默认值,NULL
- start_routine : 函数指针,这个函数是子线程需要处理的逻辑代码
- arg : 给第三个参数使用,传参 - 返回值
- 成功:0
- 失败:返回错误号。这个错误号和之前errno不太一样。
- 获取错误号的信息: char * strerror(int errnum);
pthread_exit( )函数
//终止一个线程,在哪个线程中调用,就表示终止哪个线程
#include <pthread.h>
void pthread_exit(void *retval);。
- 参数(retval):需要传递一个指针,作为一个返回值,可以在pthread_join( )中获取到。
需要注意的是,pthread_exit并不等同于普通的C库函数exit。exit会终止整个进程,而pthread_exit只终止调用它的那个线程。其他线程会继续执行,直到它们也各自调用pthread_exit,或者主线程返回,这时整个进程才会结束。
pthread_join( )函数
//阻塞调用它的线程,直到指定的thread线程终止
int pthread_join(pthread_t thread, void **retval);
- 参数:
- thread:需要回收的子线程的ID
- retval: 接收子线程退出时的返回值 - 返回值: 成功 – 0
当thread线程调用pthread_exit并返回时,pthread_join会解除阻塞,并可以通过retval参数获取thread线程的退出状态。如果retval是NULL,那么就不会获取线程的退出状态。
2. 线程同步
线程同步是指在多个线程之间协调共享资源的访问,以保证数据的一致性和正确性。基本的线程同步原理是通过协调线程之间的访问顺序,确保共享资源的正确访问。当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作,其他线程才能对该内存地址进行操作。线程同步可以避免竞态条件、死锁、饥饿等问题。实现线程同步有多种方式,如临界区、互斥量、信号量和事件等。
- 临界区(Critical Section):通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。
- 互斥量(Mutex):为协调共同对一个共享资源的单独访问而设计,可以指定资源被独占的方式使用。
- 信号量(Semaphore):为控制一个具有有限数量用户资源而设计,允许多个线程在同一时刻访问同一资源,但需要限制在同一时刻访问此资源的最大线程数目。
- 事件(Event):用来通知线程有一些事件已发生,从而启动后继任务的开始。
这些同步方式可以根据具体的应用场景和需求来选择使用,以确保线程之间能够正确地共享和访问资源,提高程序的稳定性和效率,本次实验我们使用互斥量。
3. 互斥量
互斥量(Mutex,全称互斥锁或互斥对象)是一种用于线程同步的编程工具,它允许一个线程独占某个共享资源,以防止其他线程同时访问。互斥量通常用于保护对共享数据的访问,以避免数据竞争和不一致的问题。通常情况下,我们可以将他理解为一把“锁”,当进入目标线程时,这把锁扣起来,在线程内的任务结束时再把锁打开,其大致流程如下所示。
互斥锁和信号量不同的是,它具有互斥锁所有权、递归访问等特性,常用于实现对临界资源的独占式处理, 在任意时刻互斥锁的状态只有两种,开锁或闭锁。当互斥锁被线程持有时,该互斥锁处于闭锁状态,线程获得互斥锁的所有权。当该线程释放互斥锁时, 该互斥锁处于开锁状态,线程失去该互斥锁的所有权。也就是说,同时只有一个线程能获取互斥锁,特别地,持有该互斥锁的线程能够再次获得这个锁而不被阻塞, 这就是互斥锁的递归访问,这个特性与一般的信号量有很大的不同, 在信号量中,由于会不存在可用的信号量,线程递归获取信号量时会发生阻塞,最终形成死锁。
想要避免死锁,最好遵循以下的规则:
- 对共享资源操作前一定要获得锁。
- 完成操作以后一定要释放锁。
- 尽量短时间地占用锁。
如果有多个锁, 如获得顺序是ABC连环扣, 释放顺序也应该是ABC。
4. 条件变量
既然已经掌握了Mutex,我们便再引入它的一个“好伙伴”----条件变量(Condition Variables)。条件量是计算机编程中用于处理并发编程的一种同步机制。在多线程或多进程环境中,条件量通常与互斥锁(mutex)一起使用,以允许线程或进程在满足特定条件之前等待,并在条件满足时被唤醒。它的主要作用是让线程能够在某个条件不成立时进入阻塞状态,等待其他线程改变条件并通知它。一旦条件成立,被阻塞的线程会被唤醒并继续执行。这种机制可以避免线程轮询检查条件是否成立,从而节省CPU资源并提高程序的性能。其大致流程即相关函数如下:
5. 实验代码
//这是一个多线程案例,其主要目标是通过其中的一个线程进行发送,再经由另一个线程接
收达到回环的目标
#include<pthread.h>
#include<stdio.h>
#include<unistd.h>
#include<string.h>
//初始化
#include<semaphore.h>
static char re_buff[256];
//创建互斥量并初始化,防止持续写入
static pthread_mutex_t tmutex = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t tcond = PTHREAD_COND_INITIALIZER;
static void *my_thread_func(void *data){
while(1){
//上锁
pthread_mutex_lock(&tmutex);
//等待条件
pthread_cond_wait(&tcond,&tmutex);
printf("rec: %s\n",re_buff);
//解锁
pthread_mutex_unlock(&tmutex);
}
return NULL;
}
int main(int argc, char **argv){
pthread_t tid;
int res;
//防止主线程长期霸占互斥量
char buff[256];
//创建接收线程(若成功返回0)
res = pthread_create(&tid,NULL,my_thread_func,NULL);
//验证是否创建成功
if(res != 0){
printf("create error!\n");
return -1;
}
//主线程读取标准输入发给接收线程
while(1){
fgets(buff,256,stdin);
//上锁
pthread_mutex_lock(&tmutex);
memcpy(re_buff,buff,256);
//通知接收线程
pthread_cond_signal(&tcond);
//解锁
pthread_mutex_unlock(&tmutex);
}
return 0;
}
注:在gcc -o 编译时需要加上扩展库(-lpthread),例如:
gcc -o thread.o thread.c -lpthread