目录
前言
线程VS进程
POSIX线程库的使用
线程创建
线程等待
线程分离
线程状态
可结合态线程实例
分离态线程实例
线程退出
线程的同步与互斥
同步互斥的概念
互斥锁(互斥)
互斥锁的使用步骤
总结说明
信号量
信号量的使用步骤
条件变量(协同)
条件变量的使用步骤
总结说明
多线程的问题
解决方案(引入线程池概念)
前言
1、线程与进程的区别要能在面试时侃侃而谈至少五分钟。
二者的区别详细可以参考以下博客,这是我觉得写得非常好的一篇博客:
线程与进程,你真得理解了吗
2、本文除了介绍线程与进程的区别,还包括了C语言中POSIX线程库的使用以及对线程的同步与互斥的复习,最后还引入了线程池的概念,之后将从C语言的多线程过度到C++线程池的实现。
线程VS进程
进程是资源分配的最小单位,线程是任务调度的最小单位。
进程和线程区别的本质
每个进程拥有独立的地址空间,多个线程共享同一块地址空间
区别1:进程的并发方式是比较消耗资源的,因为是独立的地址空间;而线程是共享空间,所以并发的开销比较小。
区别2:通信机制的区别,因为进程使用的是独立地址空间,所以需要提供专门的通信方式。所以我们在多进程编程时,考虑更多的是进程间的通信。
线程是共享地址空间,它们的通信使用的是全局变量(也就是所谓的数据段)。优点是通信方式简单,缺点是上锁解锁、获取释放信号量要保证线程的同步,也就是说虽然通信方式简单,但是安全性低,需要保证线程的同步
区别3:对于进程和线程来讲,进程更加安全,因为进程使用独立的地址空间,也就是一个进程的消亡不会影响到另一个进程。而线程是共享地址空间的,因此一个线程消亡了,可能会影响到其它的线程工作。
因此在实际的开发应用中,对核心的业务开发更倾向于使用进程(安全性、稳定性)。而线程的开销小,因此开销多在交互式、有响应优先级以及需要资源共享的程序中使用。
从线程和进程的运行效率上来区分
如果有一万个进程同时运行和一万个线程同时运行,假设资源都足够的情况下,进程运行比线程更快。
线程操作的API并不是操作系统提供的,进程相关的API属于系统调用,而线程相关的API属于POSIX线程库(使用POSIX线程库的好处是支持跨平台)
POSIX线程库的使用
线程创建
参数1:线程id
参数2:线程属性
参数3:线程处理函数
参数4:线程处理函数参数
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
线程等待
int pthread_join(pthread_t thread, void **retval);
该函数用于回收线程资源,如果该线程没有结束,就一直阻塞直到线程结束,相对于进程中的wait方法
为什么要实现线程等待,因为我们要回收创建的线程资源
线程分离
int pthread_detach(pthread_t thread);
功能:如果次线程的资源不希望别人调用pthread_join函数来回收,而是希望自己在结束时,自动回收资源的话,可以调用该函数,且不会导致程序阻塞(分离次线程,让次线程在结束时自动回收资源)
返回值:成功返回0,失败返回错误号
线程状态
- 可结合态joinable
- 这种状态下的线程是能够被其它进程回收其资源或杀死的(默认创建的线程都是可结合态,可被pthread_join回收)
- 分离态detached
- 这种状态下的线程是不能够被其他线程回收或杀死的;它的存储资源在它终止时由系统自动释放。这种线程也叫做异步线程。不会导致主线程阻塞,因此我们写代码一般使用分离态。
面试题:如何避免多线程退出导致的内存泄露?
1.每个可结合线程需要显示地调用pthread_join回收
2.将其变成分离态地线程。线程分离函数——pthread_detach
可结合态线程实例
#include <stdio.h>
#include <pthread.h>
void *mythread1(void *arg)
{
for(int i=0;i<3;i++)
{
printf("hello world\n");
sleep(1);
}
}
int main(int argc, char const *argv[])
{
pthread_t id;
pthread_create(&id,NULL,mythread1,NULL);
pthread_join(id,NULL);//wait
return 0;
}
如果不回收线程,就会导致线程结束成为僵尸线程。在实际开发中,比如服务器开发,如果大量的线程没有被回收,就会导致占用大量的内存资源,导致系统运行效率下降。为了避免忘记手动回收资源,我们可以将线程设置为分离态,让系统自动回收。
#include <stdio.h>
#include <pthread.h>
void *mythread1(void *arg)
{
for(int i=0;i<3;i++)
{
printf("hello world\n");
sleep(1);
}
}
int main(int argc, char const *argv[])
{
pthread_t id;
pthread_create(&id,NULL,mythread1,NULL);
//pthread_join(id,NULL);//wait
while(1)
{
}
return 0;
}
pthread_join导致主线程阻塞
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void *mythread1(void *arg)
{
for(int i=0;i<3;i++)
{
printf("hello world\n");
sleep(1);
}
}
int main(int argc, char const *argv[])
{
pthread_t id;
pthread_create(&id,NULL,mythread1,NULL);
pthread_join(id,NULL);//wait
//pthread_detach(id);
while(1)
{
printf("main!\n");
sleep(2);
}
return 0;
}
分离态线程实例
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void *mythread1(void *arg)
{
for(int i=0;i<3;i++)
{
printf("hello world\n");
sleep(1);
}
}
int main(int argc, char const *argv[])
{
pthread_t id;
pthread_create(&id,NULL,mythread1,NULL);
//pthread_join(id,NULL);//wait
pthread_detach(id);
while(1)
{
printf("main!\n");
sleep(2);
}
return 0;
}
线程退出
被动退出
int pthread_cancel(pthread_t thread);
主动退出
- void pthread_exit(void *retval)
- return返回
例:线程运行1s后退出
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
void *mythread1(void *arg)
{
for(int i=0;i<3;i++)
{
printf("hello world\n");
sleep(1);
}
}
int main(int argc, char const *argv[])
{
pthread_t id;
pthread_create(&id,NULL,mythread1,NULL);
//pthread_join(id,NULL);//wait
pthread_detach(id);
while(1)
{
printf("main!\n");
sleep(1);
pthread_cancel(id);
}
return 0;
}
注册线程退出处理函数,这两个函数需要成对出现使用
void pthread_cleanup_push(void(*routine)(void *),void *arg);
void pthread_cleanup_pop(int execute);
线程的同步与互斥
因为线程是共享数据段的,所以对数据段的保护一定要做到。不做线程同步的话使,就会导致共享资源和临界资源出现问题。
使用互斥锁、线程信号量或条件变量可以实现线程同步。
同步互斥的概念
互斥:同一时间,只能一个任务(进程或线程)执行,谁先运行不确定。
同步:同一时间,只能一个任务(进程或线程)执行,有顺序的运行。
同步 是特殊的 互斥。
互斥锁(互斥)
用于线程的互斥。
互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即加锁(
lock )和解锁( unlock )
互斥锁的操作流程如下:
1)在访问共享资源临界区域前,对互斥锁进行加锁。
2)在访问完成后释放互斥锁上的锁。 (解锁)
3)对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁 被释放。
互斥锁的使用步骤
- 定义一个互斥锁(变量)
pthread_mutex_t mutex;
- 初始化互斥锁
功能:初始化定义的互斥锁
什么时初始化,解锁设置互斥锁所需要的值
返回值:总是返回0,所以这个函数不需要进行任何出错处理
参数
- mutex:互斥锁,需要我们自己定义
比如:pthread_mutex_t mutex;
pthread_mutex_t是一个结构体类型,所以mutex实际上是一个结构体变量
- attr:互斥锁的属性
设置NULL表示使用默认属性,除非我们想要实现一些互斥锁的特殊功能,否则默认属性
#include <pthread.h>
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
const pthread_mutexattr_t *restrict attr);
编译时初始化锁位解锁状态
pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER
- 加锁解锁
pthread_mutex_lock(&mutex);(阻塞加锁)访问临界区加锁操作
pthread_mutex_trylock(&mutex)(非阻塞加锁);
与lock类似,不同的是在锁已经被占据时返回EBUSY而不是挂起等待
pthread_mutex_unlock(&mutex);访问临界区解锁操作
一般都使用pthread_mutex_trylock,使用非阻塞可以提高开发效率
- 进程退出时销毁互斥锁
#include <pthread.h>
pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:销毁互斥锁
所谓销毁,说白了就是删除互斥锁相关的数据,释放互斥锁数据所占用的各种内存资源
返回值:成功返回0,失败返回非零错误号
总结说明
在使用互斥锁的时候一定要锁信息度比较小的,也就是锁的代码段越少,锁解决问题效果越好。比如说红绿灯都是锁一个路口,没有锁一条路的。
信号量
信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被 用来控制对公共资源的访问。
当信号量值大于 0 时,则可以访问,否则将阻塞。
信号量数据类型为:sem_t
- 信号量用于互斥:
不管多少个任务互斥 只需要一个信号量。先P 任务 再 V
- 信号量用于同步:
有多少个任务 就需要多少个信号量。最先执行的任务对应的信号量为1,其他信号量全 部为0。
每任务先P自己 任务 V下一个任务的信号量。
信号量的使用步骤
- 定义信号量集合
sem_t *sem;
- 初始化集合中的每个信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value)
功能:
创建一个信号量并初始化它的值。一个无名信号量在被使用前必 须先初始化。
参数:
sem:信号量的地址
pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。
value:信号量的初始值
返回值: 成功返回0,失败返回-1
- p、v操作
PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。
功能:
创建一个信号量并初始化它的值。一个无名信号量在被使用前必 须先初始化。
参数:
sem:信号量的地址
pshared:等于 0,信号量在线程间共享(常用);不等于0,信号量在进程间共享。
value:信号量的初始值
返回值: 成功返回0,失败返回-1
- p、v操作
PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。
信号量减一 P操作
int sem_wait(sem_t *sem);
功能: 将信号量减一,如果信号量的值为0 则阻塞,大于0可以减一
参数:信号量的地址
返回值:成功返回0 失败返回-1
尝试对信号量减一
int sem_trywait(sem_t *sem);
功能: 尝试将信号量减一,如果信号量的值为0 不阻塞,立即返回 ,大于0可以减一。
参数:信号量的地址
返回值:成功返回0 失败返回-1
信号量加一 V操作
int sem_post(sem_t *sem);
功能:将信号量加一
参数:信号量的地址
返回值:成功返回0 失败返回-1
- 进程结束时,删除线程信号量集合
销毁信号量
int sem_destroy(sem_t *sem);
功能: 销毁信号量
参数: 信号量的地址
返回值:成功返回0 失败返回-1
条件变量(协同)
协同指的是当条件满足的时候就通知线程去执行,条件不满足就不通知。
条件变量的使用步骤
条件变量的使用步骤
- 定义一个条件变量(全局变量 )由于条件变量需要互斥锁的配合,所以还需要定义一个线程互斥锁
pthread_cond_t
- 初始化条件变量
int pthread_cond_init(pthread_cont_t *restrict cond,const pthread_condattr_t *restrict attr);
功能
初始化条件变量。与互斥锁的初始化类似
pthread_cond_t cond; //定义条件变量
pthread_cond_init(&cond,NULL); //第二个参数位NULL,表示不设置条件变量的属性
也可以直接初始化
pthread_cond_t cond = PTHREAD_COND_INITALIZER; //与互斥锁的初始化的原理是一样的
返回值:成功返回0,失败返回非0错误号
参数:
- cond:条件变量
- attr:条件变量属性
- 使用条件变量
- 等待条件变量函数
#include <pthread.h>
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
功能:
检测条件变量cond,如果cond没有被设置,表示条件还不满足,别人还没有对cond进行设置,此时pthread_cond_wait会休眠(阻塞),直到别的线程设置cond表示条件准备好后,才会被唤醒。
返回值:成功返回0,失败返回非0错误号
参数:
- cond:条件变量
- mutex:和条件变量配合使用的互斥锁
-
- pthread_cond_wait的兄弟函数
int pthread_cond_timewait(pthread_cond_t *restrict cond,\
pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);
多了第三个参数,用于设置阻塞时间,如果条件不满足时休眠(阻塞),但不会一直休眠,当时间超时后,如果cond还没有被设置,函数不再休眠
-
- 设置条件变量的函数
#include <pthread.h>
int pthread_cond_signal(pthread_cond_t *cond);
功能:
当线程将某个数据准备好时,就可以调用该函数去设置cond,表示条件准备好了,pthread_cond_wait检测到cond被设置后就不再休眠(被唤醒),线程继续运行,使用别的线程准备好的数据来做事。
当调用pthread_cond_wait函数等待条件满足的线程只有一个时,就是用pthread_cond_signal来唤醒,如果说好多个线程都调用pthread_cond_wait在等待时,使用
int pthread_cond_broadcast(pthread_cond_t *cond);
它可以将所有调用pthread_cond_wait而休眠的线程都唤醒
- 删除条件变量
#include <pthread.h>
int pthread_cond_destroy(pthread_cond_t *cond);
总结说明
调用pthread_cond_signal区设置条件变量,相当于是给pthread_cond_wait发送了一个线程间专用的信号,通知调用pthread_cond_wait的线程,某某条件满足了,不要再睡了,赶紧做事吧。
多线程的问题
我们在实现并发服务器模型的时候,使用多线程解决问题最大的问题就是线程的数量是受限的。因为线程是存在于进程中的,线程占用的是进程的栈空间。
问题概况如下:
- 进程所支持的线程数量问题(受限)
- 线程的创建和销毁是有一定开销的(如果频繁地创建线程,会严重占用CPU资源)
解决方案(引入线程池概念)
使用池化技术(线程池)来解决多线程的问题:在线程池中开辟一定量的线程,如果某人需要使用线程,就从池子中来拿,如果池子为空就等待线程空余。