进程与线程的区别
进程是一个运行中的程序,站在操作系统的角度,进程就是一个运行中的长须的描述-PCB(进程控制块)。
线程是轻量级的进程。在传统的操作系统中,pcb就是进程,而tcb是线程控制块,每个线程有他自己的tcb。但是在Linux下,因为线程是通过进程的pcb描述实现的(task_struct结构体),因此Linux下的pcb实际上是一个线程组,并且因为这些线程共用一个虚拟地址空间,因此也把Linux下的线程成为轻量级进程,相较于传统pcb更加的轻量化。
进程是资源分配的基本单位,线程是CPU调度的基本单位。
进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。
而线程是共享进程中的数据的,使用相同的地址空间,因此CPU切换一个线程的花费远比进程要小很多,同时创建一个线程的开销也比进程要小很多。
线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要使用进程间通信方式(IPC)进行。
- 多线程的优点:
因为共用同一块虚拟地址空间,因此线程间通信更加灵活方便(可以使用全局变量、传参等方式)
线程间共用进程的大部分资源,因此线程的创建与销毁成本更低。
也因为线程间共用进程的大部分资源,所以线程的切换调度成本更低。 - 多线程缺点:
线程间缺乏访问控制,某些系统调用以及异常是针对整个进程产生效果的。 - 多进程优点:
进程间相互独立,因此稳定、健壮性更高,适用于对主程序安全稳定性要求更高的场景,例如shell、服务器。
线程
线程共享的资源
线程共用所属进程的同一地址空间,因此代码段、数据段(虚拟地址空间)都是共享的,定义一个函数,则在各线程中都可以调用,定义一个全局变量,在各线程中都可以访问,除此之外,各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN忽略、SIG_ DFL默认或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
线程独有的资源
线程共享进程的数据,但也有一部分自己独有的数据:
- 线程ID,即线程标识符
- 寄存器(存放上下文数据等)
- 栈
- errno
- 信号屏蔽字(阻塞信号集合)
- 调度优先级
线程属性pthread_attr_t结构体
typedef struct{
int detachstate; //线程的分离状态
int schedpolicy; //线程调度策略
struct sched_param schedparam; //线程的调度参数
int inheritsched; //线程的继承性
int scope; //线程的作用域
size_t guardsize; //线程栈末尾的警戒缓冲区大小
int stackaddr_set; //线程的栈设置
void* stackaddr; //线程栈的位置(最低地址)
size_t stacksize; //线程栈的大小
} pthread_attr_t;
属性值不能直接设置,须使用相关函数进行操作,初始化pthread_attr_init,这个函数必须在pthread_create函数之前调用。
之后须用pthread_attr_destroy函数来释放资源。
线程属性主要包括如下属性:作用域(scope)、栈尺寸(stack size)、栈地址(stack address)、优先级(priority)、分离的状态(detached state)、调度策略和参数(scheduling policy and parameters)。
默认属性为非绑定、非分离、缺省的堆栈、与父进程同样级别的优先级。
线程创建:
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
pthread_t:无符号长整型
*thread:线程地址空间,用户态县城描述信息/操作句柄。线程地址空间在进程虚拟地址空间中的首地址。
(pcb中的pid为轻量级进程ID,而tgid为进程ID,即线程组ID,值默认等于主线程的pid)
pthread_attr_t *attr:线程的属性参数,通常置空
void *(*start_routine) (void *), void *arg:线程入口函数,*arg为传递给线程的参数
线程终止:
- 线程见自己的入口函数运行完毕return退出,在主函数main函数中退出的是进程。
- void pthread_exit(void *retval); 退出调用线程
- int pthread_cancel(pthread_t thread); 取消一个指定线程,成功返回0,失败返回错误编号
线程等待:
默认情况下,线程退出后也不会完全释放资源,需要被其他线程等待,线程等待即指等待一个指定的线程退出,retval获取这个退出线程的返回值,并且回收资源。
#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);
线程分离:
一个线程被创建出来,默认在退出时不会释放所有资源,因为线程的默认情况下的joinable属性。处于joinable状态的线程,在退出时不会自动释放资源,需要被等待。
线程分离即将一个线程的属性从joinable设置为detach,表示分离一个线程,被分离处于detach的线程在退出后,会自动释放所有资源,故它不需要被等待。
#include <pthread.h>
int pthread_detach(pthread_t thread);
pthread_t pthread_self(void); // 获取调动线程的线程ID
线程安全
多个执行流(线程间)对同一个临界资源进行争抢访问,但是不会造成数据二义或逻辑混乱,这样才是安全的线程。
线程安全主要靠同步与互斥实现。
同步是通过条件判断实现对临界资源访问的时序和理性,不能访问则等待,能够访问则唤醒。
互斥是同一时间只能有一个一个执行流能够访问临界资源,实现数据的操作安全。
互斥
互斥的实现:互斥锁、信号量
互斥锁的互斥实现原理:互斥锁相当于一个只有0/1的计数器用于标记当前临界资源的访问状态,对于临界资源的访问之前都需要先加锁访问这个状态计数,如果可以进行访问则修改这个计数(计数操作是一个原子操作,通过使用一个交换指令完成寄存器与内存之间的数据交换)。
// 互斥锁的使用
// 黄牛抢票
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <pthread.h>
#define MAX_THR 4
int tickets = 200;
pthread_mutex_t mutex;
void *fun(void *arg)
{
while(1)
{
// pthread_mutex_lock 阻塞加锁
// pthread_mutex_trylock 非阻塞加锁
// pthread_mutex_timedlock 限制阻塞时长的阻塞加锁
pthread_mutex_lock(&mutex);
if (tickets > 0)
{
printf("%p get a ticket: %d\n", pthread_self(), tickets);
usleep(1000);
tickets--;
pthread_mutex_unlock(&mutex);
}
else
{
printf("there are no tickets\n");
// 加锁之后要在所有有可能退出线程的地方解锁
pthread_mutex_unlock(&mutex);
pthread_exit(NULL);
}
}
return NULL;
}
int main()
{
pthread_t tid[MAX_THR];
// pthread_mutex_init() 函数是以动态方式创建互斥锁的,参数attr指定了新建互斥锁的属性。如>果参数attr为空(NULL),则使用默认的互斥锁属性,默认属性为快速互斥锁 。互斥锁的属性在创建锁的时候指
定,在LinuxThreads实现中仅有一个锁类型属性,不同的锁类型在试图对一个已经被锁定的互斥锁加锁时表现>不同。
pthread_mutex_init(&mutex, NULL);
for (int i = 0; i < MAX_THR; i++)
{
// 第一个参数为指向线程标识符的指针
// 第二个参数用来设置线程属性
// 第三个参数是线程运行函数的起始地址
// 最后一个参数是运行函数的参数
int ret = pthread_create(&tid[i], NULL, fun, NULL);
if (ret != 0)
{
perror("pthread create error");
return -1;
}
}
for (int i = 0; i < MAX_THR; i++)
pthread_join(tid[i], NULL);
pthread_mutex_destroy(&mutex);
return 0;
}
同步
同步的实现:条件变量、信号量
条件变量提供了一个让线程等待与唤醒的功能,向外提供一个等待队列,以及等待与唤醒的功能接口。
条件变量并没有条件判断的功能,不具备判断什么时间线程该等待或唤醒,因此条件判断需要用户自身完成。
而条件变量通常需要与互斥锁搭配使用,因为用于判断是否等待的条件是一个临界资源。
pthread_cond_wait操作集合了解锁、休眠、加锁三个操作。
用户的条件判断需要使用while循环判断,因为被唤醒的多个进程可能都等待在锁上,解锁后有可能线程会在不具备访问条件的情况下加锁进行访问。
不同的角色要使用多个条件变量等待在不同的队列中。
// 条件变量的使用
// 生产者a 消费者b
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
int cond = 0;
pthread_cond_t cond_a;
pthread_cond_t cond_b;
pthread_mutex_t mutex;
void *funb(void *arg)
{
while (1)
{
pthread_mutex_lock(&mutex);
while (cond == 0)
pthread_cond_wait(&cond_b, &mutex);
printf("b -> cond-1\n");
cond--;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond_a); // 至少唤醒一个
// pthread_cond_broadcast 广播唤醒所有
}
return NULL;
}
void *funa(void *arg)
{
while (1)
{
pthread_mutex_lock(&mutex);
while (cond == 1)
pthread_cond_wait(&cond_a, &mutex);
printf("a -> cond+1\n");
cond++;
pthread_mutex_unlock(&mutex);
pthread_cond_signal(&cond_b);
}
return NULL;
}
#define MAX 5
int main()
{
pthread_t aTid, bTid;
int ret = 0;
pthread_cond_init(&cond_a, NULL);
pthread_cond_init(&cond_b, NULL);
pthread_mutex_init(&mutex, NULL);
for (int i = 0; i < MAX; i++)
{
ret = pthread_create(&bTid, NULL, funb, NULL);
if (ret != 0)
{
perror("thread create b error");
return -1;
}
}
ret = pthread_create(&aTid, NULL, funa, NULL);
if (ret != 0)
{
perror("thread create a error");
return -1;
}
pthread_join(aTid, NULL);
pthread_join(bTid, NULL);
pthread_cond_destroy(&cond_a);
pthread_cond_destroy(&cond_b);
pthread_mutex_destroy(&mutex);
return 0;
}
查看线程命令
查看当前运行的进程:ps aux
查看当前运行的轻量级进程:ps -aL
查看主线程和新线程的关系:pstree -p 主线程id
线程的查看以及利用gdb调试多线程