【摘要】线程的出现让我们的计算机运行效率得到了显著地提升,虽然我们现在还很少接触到多线程的程序,但是在以后的实际工作中,多线程绝对是我们的一大得力助手,它可以显著提升你的工作效率,并且使得你的代码执行质量更有保障。在本文中,我整理了关于线程的各种知识,希望会对你们有所帮助
- 通过为每种事物分配单独的处理线程,可以简化处理异步事件的代码。
线程的概念
什么是线程
在一个程序里的一个执行路线叫做线程。更准确的定义是:线程是一个进程内部的控制序列
- 一切进程至少都有一个执行线程
进程和线程
- 进程是资源竞争的基本单位
- 线程是程序执行的最小单位
- 线程共享进程数据,但也拥有自己的一部分数据:>线程ID >一组寄存器 > 栈 >
- errno > 信号屏蔽字 > 调度优先级
一进程的多个线程共享
- 同一地址空间,因此Text Segment,Data Segment都是共享的,如果定义一个函数,在各线程中都可以调用,如果定义一个全局变量,在各线程中都可以访问到,各线程还共享一下进城资源和环境
- 文件描述符表
- 每种信号的处理方式
- 当前工作目录
- 用户ID和组ID
线程的优点
- 创建一个新线程的代价要比创建一个进程一个新进程要小得多
- 与进程间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠,线程可以同时等待不同的I/O操作
线程的缺点
- 性能损失
如果计算密集型线程的数量比可用的处理器多,那么可能有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变 - 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,同时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,线程之间可能是缺乏保护的。
缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响
编程难度增加(编写和调试一个多线程程序比单线程程序困难得多)
POSIX线程库
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以pthread_打头的
- 要使用这些函数库,要通过引入头文件
- 链接这些线程函数库时要使用编译器命令的 -lpthread选项
创建线程
int pthread_create(pthread_t *thread,
const pthread_attr_t *attr,
void *(*start_routine(void*),
void *arg);
thread:返回线程ID
attr: 设置线程的属性,attr表示为默认属性
start_routine:是个函数地址,线程启动后要执行的函数
arg:传给线程启动函数的参数
返回值:成功返回0,失败返回错误码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<pthread.h>
void* rout(void *arg){
int i;
for( ; ;){
printf("I am thread 1\n");
sleep(1);
}
}
int main()
{
pthread_t tid;
int ret;
if((ret = pthread_create(&tid,NULL,rout,NULL)) != 0){
fprintf(stderr,"pthread_create:%s\n",strerror(ret));
exit(EXIT_FAILURE);
}
int i;
for( ; ;){
printf("I an main thread\n");
sleep(1);
}
}
查看线程ID ps-eLf
线程ID及进程地址空间布局
- pthread_create函数会产生一个线程ID,存放第一个参数指向的地址中。该线程ID和前面所说的线程ID不是一回事
- 前面讲的线程ID属于进程调度的范畴。因为线程是轻量级进程,是操作系统调度器的最小单位,所以需要一个数值来唯一表示该线程
- pthread_create 函数产生并标记在第一个参数指向的地址中的线程ID中,属于NPTL线程库的范畴。线程库的后续操作,就是根据该线程ID来操作线程的。
- 获得线程自身的ID
pthread_t pthread_self(void);
对于Linux目前实现的NPTL实现而言,pthread_t类型的线程ID,本质上就是一个进程地址空间上的一个地址
线程终止
如果需要只终止谋个线程而不终止整个进程,可以有三种方法
- 1.从线程函数return,这种方法对主线程不适用,从main函数return相当于调用exit
- 2.线程可以调用 pthread_exit终止自己
- 3.一个线程可以调用pthread_cancel终止同一个进程中的另一个线程
pthread_exit函数
void pthread_exit(void *value_ptr);
参数 value_ptr: value_ptr不要指向一个局部变量
返回值:无返回值,跟进程一样,线程结束时无法返回到调用者(自身)
注意:pthread_exit 或者 return返回的指针所指向的内存单元必须是全局的或者是用malloc 分配的,不能再线程函数的栈上分配,因为当其他线程得到这个返回指针时线程函数已经退出了。
取消一个执行中的线程
pthread_cancel
int pthread_cancel(pthread_t thread);
参数 thread:线程ID
返回值:成功返回0,失败返回错误码
线程等待与分离
为什么需要线程等待
1.已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。避免产生僵尸进程2.创建新的线程不会复用刚才退出线程的地址空间
功能:等待线程结束
int pthread_join(pthread_t thread,void **value_ptr);
参数
thread:线程ID
value_ptr:它指向一个指针,后者指向线程的返回值
调用该函数的线程将挂起等待,直到id为thread 的线程为止,thread 线程以不同的方法终止,通过thread_join得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ptr所指向单元里存放的是thread线程函数的返回值
- 如果theread 线程被别的线程调用 pthread_cancel 异常终止掉,value ptr所指向的单元里存放的是常数PTHREAD_CANCELED
- 如果thread线程是自己调用pthreadexit终止的,valueptr所指向的单元存放的是传给thread_exit的参数
- 如果对thread线程的终止状态不感兴趣,可以传NULL 给value_ptr参数
//线程间替换演示代码
#include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<pthread.h>
5
6 void *thread1(void *arg) //线程1
7 {
8 printf("thread 1 returning ...\n");
9 int *p = (int *)malloc(sizeof(int));
10 *p =1;
11 return (void*) p;
12 }
13
14 void *thread2(void *arg) //线程2
15 {
16 printf("thread 2 exiting...\n");
17 int *p = (int *)malloc(sizeof(int));
*p = 2;
19 pthread_exit((void*)p);
20 }
21
22 void *thread3(void *arg) //线程3
23 {
24 while(1){
25 printf("thread 3 is running...\n");
26 sleep(1);
27 }
28 return NULL;
29 }
30
31 int main()
32 {
33 pthread_t tid;
34 void *ret;
pthread_create(&tid,NULL,thread1,NULL); //创建线程
36 pthread_join(tid, &ret); //等待回收线程
37 printf("thread return ,thread id %x,return code:%d\n",tid,*(int*)ret );
38 free(ret); //由于ret是动态开辟的,所以需要手动释放
39
40 pthread_create(&tid,NULL,thread2,NULL);//创建线程2
41 pthread_join(tid, &ret); //等待回收线程
42 printf("thread return ,thread id %x,return code:%d\n",tid,*(int*)ret );
43 free(ret);
44
45 pthread_create(&tid,NULL,thread3,NULL);//创建线程3
46 sleep(3);
47 pthread_cancel(tid); //让一个线程中断
48 pthread_join(tid, &ret); /回收一个线程状态
49 if(ret == PTHREAD_CANCELED)
printf("thread return ,thread id %x,return code");
51 else
52 printf("thread return ,thread id %x,return code:NULL\n",tid);
53 }
54
- 线程自动地可以访问相同的存储地址,空间和文件描述符
- 有些问题分解可以提高整个程序的吞吐量。在只有一个控制线程的情况下,一个单线程进程要完成多个任务,只需要把这些任务串行化。但有多个控制线程时,相互独立的的任务处理就可以交叉进行,两个任务的处理过程互不干扰时,才可以交叉运行。
- 处理器的数量并不会影响程序结构,所以不管处理器的个数多少,程序都可以通过使用线程得以简化
多线程程序在单处理器上运行还是可以改善响应时间和吞吐量
- 执行环境所必须的信息:标示线程的线程ID,一组寄存器值,栈,调度优先级和策略,信号屏蔽字,errno变量,以及线程私有数据
一个进程的所有信息对该进程的所有线程都是共享的(包括可执行程序的代码,程序的全局内存和堆内存,栈以及文件描述符
11.3 线程标识
#include<pthread.h>
int pthread_equal(pthread_t tid1,pthread_t tid2)
pthread_t pthread_self(void) ; //返回调用线程的线程ID
主线程不允许每个线程任意处理从队列顶端取出的作业,而是由主线程控制作业的分配
线程局部存储 线程范围内的全局变量
线程属性
pthread_attr_t attr;
//初始化线程属性
int pthread_attr_init(pthread_attr_t *attr);
//销毁线程属性
int pthread_attr_destroy(pthread_attr_t *attr);
分离属性 : Detach state
pthread_attrsetdetachstate(pthread_attr_t *attr,int ds);
抢夺资源范围:
是否继承调度策略
调度策略
调度优先级
Guard size (线程栈中间留有的安全区域,防止破坏线程)
Stack address (栈地址)希望空间由我们自己去指定地址
Stack size (栈大小)
资源共享,并行运行,导致时间片切换时有可能造成数据修改
互斥
互斥量
1.定义互斥量 pthread_mutex_t mutex;
2.初始化 pthread_mutex_init(&mutex,NULL);
3.上锁:pthread_mutex_lock(&mutex);//如果是1,置0,返回
//如果是0,阻塞
restrict 只能用在指针函数形参 C99
当a从内存拿到寄存器,然后b也准备从内存拿到寄存器,这时候时间片切换,那么前面值已经付过了,然后只给后面赋值
4.解锁:pthread_mutex_unlock(&mutex) //置1,返回
5.销毁:pthread_mutex_destroy(&mutex)//
条件量加互斥锁案例
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>
pthread_cond_t cond;
pthread_mutex_t mutex;
void *r1(void* arg)
{
while(1) {
pthread_cond_wait(&cond,&mutex);
printf("wait fanhui\n");
}
}
void *r2(void *arg)
{
while(1) {
pthread_cond_signal(&cond);
sleep(1);
}
}
int main()
{
pthread_cond_init(&cond,NULL);
pthread_mutex_init mutex(&mutex,NULL);
pthread_t t1,t2;
pthread_create(&t1,NULL,r1,NULL);
pthread_create(&t2,NULL,r2,NULL);
pthread_join(t1,NULL);
pthread_join(t2,NULL);
pthread_cond_destroy(&cond);
pthread_mutex_destroy(&mutex);
}
}
自旋锁
1.定义自旋锁 pthread_spinlock_t spin;
2.初始化自旋锁 pthread_spin_init(pthread_spinlock_t *s,int s);
3.上锁 int pthread_spin_lock(pthread_spinlock_t *lock);
4.解锁 int pthread_spin_unlock(pthread_spinlock_t *lock);
5.销毁 int pthread_spin_destroy(pthread_spinlock_t *lock);
自旋锁和互斥锁的区别:互斥锁是当阻塞在pthread_mutex_lock时,放弃cpu,好让别人使用CPU。自旋锁阻塞在spin_lock时,不会释放CPU,不断地问cpu可以使用了吗
用途不一样,自旋锁可以在短暂停歇之后,立马需要cpu资源
读写锁
- 读读共享
- 读写排他
- 写写排他
- 写优先级高
1.pthread_rwlock_t lock;
初始化 pthread_rwlock_init(&lock,NULL);
3.读锁 pthread_rwlock_rdlock(&lock);
写锁 pthread_rwlock_wrlock(&lock);
4.解锁 pthread_rwlock_unlock(&lock);
5.pthread_rwlock_destroy(&lock);
同步
条件变量
- 1.定义条件变量 pthread_cond_t cond;
- pthread_mutex_t mutex
- 2.初始化条件变量 pthread_cond_init(&cond,&mutex);
3.等待条件 pthread_cond_wait(&cond ,&mutex)//如果没有在锁环境下使用,形同虚设的互斥量
// 如果在锁环境下,将mutex解锁
// wait 返回时,将mutex锁置成原来的状态4.使条件满足 pthread_cond_signal(&cond);
- 5.销毁条件变量pthread_cond_destory(&cond);
生产者消费者模型
消费者:
while(1){
pthread_mutex_lock(&mutex);
while(没有产品) //while是因为wait有可能会被信号打断而返回
pthread_cond_wait(&cond,&mutex);
//...
pthread_mutex_unlock(&mutex);
}
消费者:
while(1){
ptheread_mutex_lock(&mutex);
//生产
pthread_cond_signal(&cond); //如果消费者线程没有等待在
//wait上,通知会丢掉哦,不过没关系,,因为已经生产了产品,消费者不会进入while
pthread_mutex_unlock(&mutex);
}