目录
线程
从操作系统层面来讲,一个进程是由一个或多个线程组成的。
什么是线程
线程就是一个进程中的执行流,也就是指令运行轨迹。
Linux没有线程这一具体实现,采用了轻量级进程LWP(light weight process)的方式。
也就是说,在Linux下,一个进程内的所有线程共用一个虚存,一个页表,但线程独有自己的PCB,线程也有自己的ID,称为TID。
查看的指令为:ps -LF <PID>
这只是Linux线程的基本概念,而Linux线程具体是如何实现的移步这篇文章
链接: Linux线程实现机制分析.
线程VS进程
- 进程是资源分配的基本单位
- 线程是调度的基本单位
- 线程共享进程数据,但也拥有自己的一部分数据:
- 线程ID
- 一组寄存器
- 栈
- errno
- 信号屏蔽字
- 调度优先级
- 进程的多个线程共享 同一地址空间,因此Text Segment、Data Segment都是共享的。各线程还共享以下进程资源和环境:
- 文件描述符表
- 每种信号的处理方式(SIG_ IGN、SIG_ DFL或者自定义的信号处理函数)
- 当前工作目录
- 用户id和组id
注意:
由于线程共享信号屏蔽字集,所以单个线程异常产生信号,或者给某一个线程发信号,会导致整个线程组(即进程)收到信号退出。
线程优缺点
优点
- 创建一个新线程的代价要比创建一个新进程小得多
- 与进程之间的切换相比,线程之间的切换需要操作系统做的工作要少很多
- 线程占用的资源要比进程少很多
- 能充分利用多处理器的可并行数量
- 在等待慢速I/O操作结束的同时,程序可执行其他的计算任务
- 计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现
- I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
缺点
- 性能损失
一个很少被外部事件阻塞的计算密集型线程往往无法与共它线程共享同一个处理器。如果计算密集型线程的数量比可用的处理器多,那么可能会有较大的性能损失,这里的性能损失指的是增加了额外的同步和调度开销,而可用的资源不变。 - 健壮性降低
编写多线程需要更全面更深入的考虑,在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的,换句话说线程之间是缺乏保护的。 - 缺乏访问控制
进程是访问控制的基本粒度,在一个线程中调用某些OS函数会对整个进程造成影响。 - 编程难度提高
编写与调试一个多线程程序比单线程程序困难得多
线程操作库函数集
编译环境
- 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的
- 要使用这些函数库,要通过引入头文<pthread.h>
- 链接这些线程函数库时要使用编译器命令的“-lpthread”选项
makefile:
ticket:ticket.c
gcc -o $@ $^ -lpthread
.PHONY:clean
clean:
rm -f ticket
创建线程
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);
查看自身线程ID
pthread_t pthread_self(void);
线程终止
如果需要只终止某个线程而不终止整个进程,可以有三种方法:
- 从线程函数return。这种方法对主线程不适用,从main函数return相当于调用exit。
- 线程可以调用pthread_ exit终止自己。
void pthread_exit(void *value_ptr);
参数:用来获取线程执行的退出码。不关心可以设置为NULL。
value_ptr:value_ptr不要指向一个局部变量。
- 一个线程可以调用pthread_ cancel终止同一进程中的另一个线程。
int pthread_cancel(pthread_t thread);
线程等待
已经退出的线程,其空间没有被释放,仍然在进程的地址空间内。
创建新的线程不会复用刚才退出线程的地址空间。
int pthread_join(pthread_t thread, void **value_ptr);
调用该函数的线程将挂起等待,直到id为thread的线程终止。thread线程以不同的方法终止,通过pthread_join得到的终止状态是不同的,总结如下:
- 如果thread线程通过return返回,value_ ptr所指向的单元里存放的是thread线程函数的返回值。
- 如果thread线程被别的线程调用pthread_ cancel异常终掉,value_ ptr所指向的单元里存放的是常数PTHREAD_ CANCELED。
- 如果thread线程是自己调用pthread_exit终止的,value_ptr所指向的单元存放的是传给pthread_exit的参数。
- 如果对thread线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。
线程分离
可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离:
int pthread_detach(pthread_t thread);
默认情况下,新创建的线程是joinable的,线程退出后,需要对其
进行pthread_join操作,否则无法释放资源,从而造成系统泄漏。
如果不关心线程的返回值,join是一种负担,这个时候,我们可以告诉系统,当线程退出时,自动释放线程资源。
joinable和分离是冲突的,一个线程不能既是joinable又是分离的。
可重入与线程安全
线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。
重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
可重入与线程安全联系:
函数是可重入的,那就是线程安全的
函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别:
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
代码示例
为了保证线程安全,又引入了互斥量(也就是锁);锁的引入,又引发了”死锁与饥饿“的问题。下篇文章总结互斥与同步,死锁与饥饿、和几个典型的多线程模型。
一个多线程抢票小程序
#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
int tickets=10000; //临界资源
pthread_mutex_t lock; //使线程看到同一把锁
void* rout(void* arg)
{
while(1)
{
usleep(1000); //由于进程创建和等待互斥锁的时间差异,有可能会出现一个线程抢走了所有的票,所以要模拟时间差。
pthread_mutex_lock(&lock); //上锁
if(tickets>0)
{
usleep(1000);
printf("%s_id:%lu ",(char*)arg,pthread_self());
printf("get tickets success:%d!\n",tickets);
tickets--;
pthread_mutex_unlock(&lock); //解锁
sleep(1);
}
else
{
printf("%s_id:%lu ",(char*)arg,pthread_self());
printf("have no tickets!\n");
pthread_mutex_unlock(&lock); //推出前解锁:锁要上在正确的位置,临界区越精确越好。
break;
}
}
//pthread_exit(retval); //三种退出方式,
//return 0;
pthread_cancel(pthread_self());
return NULL;
}
int main()
{
pthread_mutex_init(&lock,NULL);
pthread_t t[4];
int i=4;
char s[]="pthread";
for(i=0;i<4;i++)
{
pthread_create(t+i,NULL,rout,s);
}
pthread_join(t[0],NULL);
pthread_join(t[1],NULL);
pthread_join(t[2],NULL);
pthread_join(t[3],NULL);
pthread_mutex_destroy(&lock); //记得将互斥锁释放
return 0;
}