Linux线程介绍
线程与进程
典型的UNIX/Linux进程可以看成只有一个控制线程:一个进程在同一时刻只做一件事情。有了多个控制线程后,在程序设计时可以把进程设计成在同一时刻做不止一件事,每个线程各自处理独立的任务。
进程是程序执行时的一个实例,是担当分配系统资源(CPU时间、内存等)的基本单位。在面向线程设计的系统中,进程本身不是基本运行单位,而是线程的容器。程序本身只是指令、数据及其组织形式的描述,进程才是程序(那些指令和数据)的真正运行实例。
线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。线程包含了表示进程内执行环境必须的信息,其中包括进程中表示线程的线程ID、一组寄存器值、栈、调度优先级和策略、信号屏蔽字、errno常量以及线程私有数据。进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本、程序的全局内存和堆内存、栈以及文件描述符。在Unix和类Unix操作系统中线程也被称为轻量级进程(lightweight processes),但轻量级进程更多指的是内核线程(kernel thread),而把用户线程(user thread)称为线程。
进程——资源分配的最小单位,线程——程序执行的最小单位"
进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程。
使用线程的理由
1 线程占用的资源比进程少,只虚复制PCB即可
2.创建时代价较小
3.线程间的切换(调度)需要操作系统做的工作少很多
4.线程间共享数据更容易
5.在等待慢速 I/O操作结束的同时,程序可执行其他的计算任务。
6.计算密集型应用,为了能在多处理器系统上运行,将计算分解到多个线程中实现。
7.I/O密集型应用,为了提高性能,将I/O操作重叠。线程可以同时等待不同的I/O操作。
注:关于I/O密集型和计算密集型可参考这篇文章:CPU-bound(计算密集型) 和I/O bound(I/O密集型
在操作系统内部,它不管什么进程线程的,它只以PCB为准,只有在用户态里才有线程的概念。一般实现线程会用到一个POSIX线程库,在这里可以通过调用POSIX库里的函数来实现有关线程的各种操作。不过内核中也有一种内核级线程。
两个基本类型:
用户级线程:管理过程全部由用户程序完成,操作系统内核心只对进程进行管理。如POSIX线程库。
系统级线程(核心级线程):由操作系统内核进行管理。操作系统内核给应用程序提供相应的系统调用和应用程序接口API,以使用户程序可以创建、执行、撤消线程。
POSIX线程库
由系统库支持。线程的创建和撤销以及线程状态的变化都由库函数控制并在目态(user态)完成,与线程相关的控制结构TCB保存在目态并由系统维护。由于线程对操作不可见(操作系统可见的必然保存在kernel态由系统维护),系统调度仍以进程为单位(同一进程内线程相互竞争),核心栈的个数与进程个数相对性。
与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以“pthread_”打头的要使用这些库函数,就要引入头文件
gcc在链接这些线程函数库时要使用编译器命令的“-lpthread”选项(pthread是共享库文件)
Linux上线程API
多线程开发在 Linux 平台上已经有成熟的 pthread 库支持。其涉及的多线程开发的最基本概念主要包含三点:线程,互斥锁,条件。其中,线程操作又分线程的创建,退出,等待 3 种。互斥锁则包括 4 种操作,分别是创建,销毁,加锁和解锁。条件操作有 5 种操作:创建,销毁,触发,广播和等待。其他的一些线程扩展概念,如信号灯等,都可以通过上面的三个基本元素的基本操作封装出来。详细请见下表:
线程运行过程中必须带上:gcc f10.c -lpthread
线程API
具体参考:线程的创建、线程等待、线程终止、线程分离
注意:线程ID、创建时传递的参数得注意转换 、空类型的指针一般都要强转过去
,因为输出ptthread_t 、void * 类型,大多数参数都要转
线程ID:prhread_t ,读取时转为unsigned long %ld
参数: int ret=1100; 创建时 (void * )&ret 转换为空类型指针,读取时在转回来(int *)&ret
设置线程退出的返回值,必须加static,join和exit搭配时,注意转换
1. 线程创建
#include <pthread.h>
原型:
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
void *(*start_routine) (void *), void *arg);
调用样例:ID、NULL、函数名、参数
参数:
id: 输出时应该强制转换unsigned long %ld
thread: 线程id地址,将线程id存入,线程标识符,线程栈的起始地址,输出型参数
attr: 线程属性,NULL,8种选项
函数指针:新线程执行该函数指针指向的代码,线程回调函数 void* func (void *)
arg: 传递给函数指针指向的函数的参数,线程回调函数的参数一个地址、不同类型的地址得转换void *
返回值:成功返回0,失败返回错误码,如:
EAGAIN : 描述: 超出了系统限制,如创建的线程太多。
EINVAL : 描述: tattr 的值无效。
当pthread_create成功返回时,由tidp指向的内存单元被设置为新创建线程的线程ID。像进程id一样,由内核决定,再存放进去。attr参数用于定制各种不同的线程属性,暂可以把它设置为NULL,以创建默认属性的线程。
新创建的线程从start_rtn函数的地址开始运行,该函数只有一个无类型指针参数arg。如果需要向start_rtn函数传递的参数不止一个,那么需要把这些参数放到一个结构中,然后把这个结构的地址作为arg参数传入。
2. 线程退出
单个线程可以通过以下三种方式退出,在不终止整个进程的情况下停止它的控制流:
1)线程只是从启动例程中返回,返回值是线程的退出码。
2)线程可以被同一进程中的其他线程取消。
3)线程调用pthread_exit:
#include <pthread.h>
int pthread_exit(void *rval_ptr);//rval_ptr:退出返回值 (void *)(&(int ret))强转
// rval_ptr是一个无类型指针,与传给启动例程的单个参数类似。进程中的其他线程可以通过调用pthread_join函数访问这个指针
在与pthread_join配合传递返回值的时候,返回值的定义一定要用static,防止函数调用重置数据,从而pthread_join读取到随机数。应该是强制转换的时候,会改变地址,加static可以不变。
rval_ptr 可以为空,线程退出时,传入参数。
3. 线程等待
#include <pthread.h>
int pthread_join(pthread_t thread, void **rval_ptr);
//pthread_t thread, //要等待的线程ID
//void **retval //用于接收线程退出的退出码
// 返回:若成功返回0,否则返回错误编号
功能:以阻塞的方式回收新线程,可以得到线程的退出码,并回收其资源
如果不使用pthread_join回收线程,有可能造成和僵尸进程一样的问题,造成内存泄漏;调用这个函数的线程将一直阻塞,直到指定的线程(pthread_t thread)调用pthread_exit、从启动例程中返回或者被取消。( void **rval_ptr)为进程id(pthread_t thread)返回或退出的返回码
指定线程exit退出后,静态返回码static int c;的地址被存放在另一个位置,这个位置被join读取。所以这里接收要用二级指针,而存放用的是一级指针
如果例程只是从它的启动例程返回i,rval_ptr将包含返回码。如果线程被取消,由rval_ptr指定的内存单元就置为PTHREAD_CANCELED。
可以通过调用pthread_join自动把线程置于分离状态,这样资源就可以恢复。如果线程已经处于分离状态,pthread_join调用就会失败,返回EINVAL。
如果对线程的返回值不感兴趣,可以把rval_ptr置为NULL。在这种情况下,调用pthread_join函数将等待指定的线程终止,但并不获得线程的终止状态。
4. 线程脱离
一个线程或者是可汇合(joinable,默认值),或者是脱离的(detached)。当一个可汇合的线程终止时,它的线程ID和退出状态将留存到另一个线程对它调用pthread_join。脱离的线程却像守护进程,当它们终止时,所有相关的资源都被释放,我们不能等待它们终止。如果一个线程需要知道另一线程什么时候终止,那就最好保持第二个线程的可汇合状态。
pthread_detach函数把指定的线程转变为脱离状态。
#include <pthread.h>
int pthread_detach(pthread_t thread);
// 返回:若成功返回0,否则返回错误编号
本函数通常由想让自己脱离的线程使用,就如以下语句:
pthread_detach(pthread_self());
5. 线程ID获取及比较
#include <pthread.h>
pthread_t pthread_self(void);
// 返回:调用线程的ID
对于线程ID比较,为了可移植操作,我们不能简单地把线程ID当作整数来处理,因为不同系统对线程ID的定义可能不一样。我们应该要用下边的函数:
对于多线程程序来说,我们往往需要对这些多线程进行同步。同步(synchronization)是指在一定的时间内只允许某一个线程访问某个资源。而在此时间内,不允许其它的线程访问该资源。我们可以通过互斥锁(mutex),条件变量(condition variable)和读写锁(reader-writer lock)来同步资源。在这里,我们暂不介绍读写锁。
示例
示例1:线程的创建、等待
include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *func(void * age){
printf("This is next 线程 ID:%ld\n",(unsigned long)pthread_self());//转化
printf("传入的参数%d\n",*((int*)age)); //转换
}
int main()
{
pthread_t main_xpid; //读取要转换
int date=100; //运用要转换
pthread_create(&main_xpid,NULL,func,(void*)&date);
//函数名+地址的转换,主要传地址
//pthread_create(&main_xpid,NULL,func,NULL);
printf("这是主线程的id:%ld\n",(unsigned long)pthread_self());
// sleep(2);
pthread_join(main_xpid,NULL);//等待次线程退出,也可接收返回码
示例二 :线程的退出,等待
等待退出和之前的效果一样,空类型的返回值可以为任意值,字符串也行
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
void *func(void * age){
static int back=10;//设置的返回值
printf("This is next 线程 ID:%ld\n",(unsigned long)pthread_self());
printf("传入的参数%d\n",*((int*)age));
pthread_exit((void*)&back);//强转为void *类型指针
//字符串的话,(void*)back
}
int main()
{
pthread_t ci_xpid;
int date=100;
int *back; //设置为一级指针,方便强转
pthread_create(&ci_xpid,NULL,func,(void*)&date);
printf("这是次线程的id:%ld\n",(unsigned long)ci_xpid);
printf("这是主线程的id:%ld\n",(unsigned long)pthread_self());
pthread_join(ci_xpid,(void **)&back);//取地址后为int**类型,强转void**类型
printf("返回值%d\n",*back);//字符串的话back
return 0;
}
//exit退出后,static int c;的地址被存放在另一个位置,这个位置被join的&p读取。
所以指针p就等于&c,即p指向了c,*p==c