Linux多线程操作pthread_t

目录

进程概念

线程概念

线程进程基本操作

一、创建线程

二、线程属性

三、线程终止

四、线程安全

五、其他操作


进程概念

进程是表示资源分配的基本单位,又是调度运行的基本单位。例如,用户运行自己的程序,系统就创建一个进程,并为它分配资源,包括各种表格、内存空间、磁盘空间、I/O设备等。然后,把该进程放人进程的就绪队列。进程调度程序选中它,为它分配CPU以及其它有关资源,该进程才真正运行。所以,进程是系统中的并发执行的单位。

在Mac、Windows NT等采用微内核结构的操作系统中,进程的功能发生了变化:它只是资源分配的基本单位,而不再是调度运行的单位。在微内核系统中,真正调度运行的基本单位是线程。因此,实现并发功能的单位是线程。

线程概念

线程是进程中执行运算的最小单位,亦即执行处理机调度的基本单位。如果把进程理解为在逻辑上操作系统所完成的任务,那么线程表示完成该任务的许多可能的子任务之一。线程可以在处理器上独立调度执行,这样,在多处理器环境下就允许几个线程各自在单独处理器上进行。操作系统提供线程就是为了方便而有效地实现这种并发性。

相对进程而言,线程是一个更加接近于执行体的概念,它可以与同进程中的其他线程共享数据,但拥有自己的栈空间,拥有独立的执行序列。在串行程序基础上引入线程和进程是为了提高程序的并发度,从而提高程序运行效率和响应时间。

引入线程的好处
(1)易于调度。
(2)提高并发性。通过线程可方便有效地实现并发性。进程可创建多个线程来执行同一程序的不同部分。
(3)开销少。创建线程比创建进程要快,所需开销很少。。
(4)利于充分发挥多处理器的功能。通过创建多线程进程(即一个进程可具有两个或更多个线程),每个线程在一个处理器上运行,从而实现应用程序的并发性,使每个处理器都得到充分运行。

  • 进程
    子进程具备自己独立的用户空间(内容全部复制父进程);
    父子进程不可相互访问对方资源;
  • 线程
    仅申请自己的栈空间,与同进程的其它线程共享内存空间;
    需要注意资源的同步和互斥访问问题

在Linux系统中,多线程的管理使用 pthread_t

线程进程基本操作

功能 进程 线程
创建 fork() pthread_create()
退出 exit pthread_exit()
等待 wait/waitpid() pthread_join()
取消 abort() pthread_cancel()
获取ID getpid() pthread_self()
调度策略 SCHED_OTHER、SCHED_FIFO、SCHED_RR SCHED_OTHER、SCHED_FIFO、SCHED_RR
通信机制 管道、消息队列、共享内存、信号、信号量 信号、信号量、互斥锁、读写锁、条件变量

一、创建线程 pthread_create

int   pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void *), void *arg)

第一个参数为指向线程标识符的指针,也就是线程对象的指针

第二个参数用来设置线程属性。

第三个参数是线程运行函数的地址,通俗理解线程要执行函数(线程做的事情的)指针。一般这个函数执行时间比较长(有while循环),做的事情比较多。如果单次动作(执行时间比较短),也就无需多线程执行了。

最后一个参数是线程要运行函数的参数。

线程的默认堆栈大小是1MB,就是说,系统每创建一个线程就要至少提供1MB的内存,那么,创建线程失败,极有可能就是内存不够用了。

pthread_create会导致内存泄露! pthread_create创建的线程结束后,系统并未回收其资源,从而导致了泄露。

为什么要分离线程?


    线程的分离状态决定一个线程以什么样的方式来终止自己。线程的默认属性是非分离状态,这种情况下,原有的线程等待创建的线程结束。只有当pthread_join()函数返回时,创建的线程才算终止,才能释放自己占用的系统资源。而分离线程不是这样子的,它没有被其他的线程所等待,自己运行结束了,线程也就终止了,马上释放系统资源。程序员应该根据自己的需要,选择适当的分离状态。


从上面的描述中可以得知如果调用pthread_create函数创建一个默认非分离状态的线程,如果不用pthread_join()函数,线程结束时并不算终止,所以仍然会占用系统资源。这里有如下几种方法解决这个问题:
如果新线程创建后,不用pthread_join()等待回收新线程,那么就会造成内存泄漏,但是当等待新线程时,主线程就会一直阻塞,影响主线程处理其他链接要求,这时候就需要一种办法让新线程退出后,自己释放所有资源,因此产生了线程分离。

1.使用pthread_join()函数回收相关内存区域。

int pthread_detach(pthread_t thread);将已经运行中的线程设定为分离状态;
pthread_t tid;
void* state;

pthread_create(&tid, NULL, test, NULL);
pthread_join(tid, &state);

pthread_join使一个线程等待另一个线程结束。代码中如果没有pthread_join 主线程会很快结束从而使整个进程结束,从而使创建的线程没有机会开始执行就结束了。加入pthread_join后,主线程会一直等待直到等待的线程结束自己才结束,使创建的线程有机会执行。pthread_join()函数以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。

2.可以调用 pthread_detach() 函数分离线程。

pthread_t tid;
pthread_create(&tid, NULL, test, NULL);
pthread_detach(tid);

当然,也可以在 thread function 中调用。

void* test(void* arg)
{
    .....
    pthread_detach(pthread_self());
    return NULL;
}

3.使用线程属性 pthread_attr_setdetachstate

pthread_attr_t attr;
pthread_t tid;

pthread_attr_init(&attr);
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);

pthread_create(&tid, &attr, test, NULL);

sleep(3);//等待线程结束

pthread_attr_destroy(&attr);

二、线程属性

pthread_create()中的attr参数是一个结构指针,结构中的元素分别对应着新线程的运行属性,主要包括以下几项:

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;
  • __detachstate, 表示新线程是否与进程中其他线程脱离同步,缺省为 PTHREAD_CREATE_JOINABLE状态。如果置位,则新线程不能用pthread_join()来同步,且在退出时自行释放所占用的资源。这个属性也可以在线程创建并运行以后用pthread_detach()来设置,而一旦设置为 PTHREAD_CREATE_DETACH状态(不论是创建时设置还是运行时设置)则不能再恢复到 PTHREAD_CREATE_JOINABLE状态。
int pthread_attr_setdetachstate (pthread_attr_t *__attr, int __detachstate)设定线程的分离状态

int pthread_attr_getdetachstate (const pthread_attr_t *__attr,int *__detachstate)获取线程参数中的分离状态;
  • __schedpolicy,表示新线程的调度策略,主要包括 SCHED_OTHER(正常、非实时)、SCHED_RR(实时、轮转法)和 SCHED_FIFO(实时、先入先出)三种,缺省为SCHED_OTHER,后两种调度策略仅对超级用户有效。运行时可以用过 pthread_setschedparam()来改变。
int pthread_attr_setschedpolicy (pthread_attr_t *__attr, int __policy),设定线程的调度策略;
  • __schedparam,一个struct sched_param结构,目前仅有一个sched_priority整型变量表示线程的运行优先级。这个参数仅当调度策略为实时(即SCHED_RR 或SCHED_FIFO)时才有效,并可以在运行时通过pthread_setschedparam()函数来改变,缺省为0。
int pthread_attr_getschedparam (const pthread_attr_t *__restrict __attr, struct sched_param *__restrict __param)
//获取参数中的线程优先级;

int pthread_attr_setschedparam (pthread_attr_t *__restrict __attr, const struct sched_param *__restrict __param)
//设定线程的优先级;
  • __inheritsched, 有两种值可供选择:PTHREAD_EXPLICIT_SCHED和PTHREAD_INHERIT_SCHED,前者表示新线程使用显式指定调度策略和 调度参数(即attr中的值),而后者表示继承调用者线程的值。缺省为PTHREAD_EXPLICIT_SCHED
int pthread_attr_setinheritsched (pthread_attr_t *__attr, int __inherit)
//设定线程调度策略的继承属性,该函数必须在root权限下调用;
  • __scope, 表示线程间竞争CPU的范围,也就是说线程优先级的有效范围。POSIX的标准中定义了两个值: PTHREAD_SCOPE_SYSTEM和PTHREAD_SCOPE_PROCESS,前者表示与系统中所有线程一起竞争CPU时间,后者表示仅与同 进程中的线程竞争CPU。目前LinuxThreads仅实现了PTHREAD_SCOPE_SYSTEM一值。
int pthread_attr_setscope(pthread_attr_t *attr, int contentionscope); 
//设定线程优先级的可竞争范围:

pthread_attr_t结构中还有一些值,但不使用pthread_create()来设置。

int pthread_attr_init (pthread_attr_t *__attr), 初始化pthread创建参数;

为了设置这些属性,POSIX定义了一系列属性设置函数,包括pthread_attr_init()、pthread_attr_destroy()和与各个属性相关的pthread_attr_get---/pthread_attr_set---函数。

三、线程终止

  • 线程退出
void pthread_exit(void *value_ptr); //在线程执行的函数中调用此接口


例子:
void *thread_run(void* arg)
{

    while(1)
    {
        printf("new thread,thread is :%u,pid:%d\n",pthread_self(),getpid());
        sleep(1);
        pthread_exit(NULL);
    }
}
  • 线程取消
int pthread_cancel(pthread_t thread);
参数
thread:线程ID
返回值:成功返回0;失败返回错误码

例子:
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,thread_run,NULL);
    while(1)
    {   
        printf("main thread,thread is :%u,pid:%d\n",pthread_self(),getpid())    ;
        sleep(3);
        pthread_cancel(pthread_self());//杀死自己,pthread_self()是获取主线程id
    }

    return 0;
}
  int pthread_setcancelstate(int state, int *oldstate)
  • state: 欲设置的线程状态,PTHREAD_CANCEL_ENABLE和PTHREAD_CANCEL_DISABLE。默认值是PTHREAD_CANCEL_ENABLE,可以被取消。
  • oldstate: 存储原来的线程状态
  int pthread_setcanceltype(int type, int *oldtype)
  • type: 欲设置的类型,PTHREAD_CANCEL_DEFERRED:在取消点取消和PTHREAD_CANCEL_ASYNCHRONOUS:可随时执行新的或未决的请求。
  • oldtype: 存储原来的类型

四、线程安全

多个线程同时操作临界资源而不会出现数据二义性,实现线程安全:

  • 互斥: 临界资源同一时间唯一访问,是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性,操作的独占性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
  • 同步: 临界资源的合理访问,是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。显然,同步是一种更为复杂的互斥,而互斥是一种特殊的同步。

多线程的同步与互斥的区别

假如把整条道路看成是一个【进程】的话,那么马路中间白色虚线分隔开来的各个车道就是进程中的各个【线程】了
①这些线程(车道)共享了进程(道路)的公共资源(土地资源)。
②这些线程(车道)必须依赖于进程(道路),也就是说,线程不能脱离于进程而存在(就像离开了道路,车道也就没有意义了)。
③这些线程(车道)之间可以并发执行(各个车道你走你的,我走我的),也可以互相同步(某些车道在交通灯亮时禁止继续前行或转弯,必须等待其它车道的车辆通行完毕)。
④这些线程(车道)之间依靠代码逻辑(交通灯)来控制运行,一旦代码逻辑控制有误(死锁,多个线程同时竞争唯一资源),那么线程将陷入混乱,无序之中。
⑤这些线程(车道)之间谁先运行是未知的,只有在线程刚好被分配到CPU时间片(交通灯变化)的那一刻才能知道。

1.互斥

互斥如何实现:
互斥锁: 一个1/0的计数器
1标识完成加锁,加锁就是计数-1;
操作完毕后要解锁, 解锁就是计数+1
0表示不可以加锁, 不能加锁则等待

互斥锁的操作步骤:

1.定义互斥锁变量 pthread_mutex_t

pthread_mutex_t lock;

2.初始化互斥锁变量 pthread_mutex_init

int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);

3.加锁 pthread_mutex_lock

int pthread_mutex_trylock(pthread_mutex_t* mutex);//非阻塞式加锁
int pthread_mutex_lock(pthread_mutex_t* mutex)//阻塞式加锁

如果一个线程既想获得锁,又不想挂起等待,可以调用pthread_mutex_trylock,如果Mutex已经被 另一个线程获得,这个函数会失败返回EBUSY,而不会使线程挂起等待。

4.解锁 pthread_mutex_unlock

int pthread_mutex_unlock(pthread_mutex_t* mutex)//解锁

5.销毁互斥锁 pthread_mutex_destory

int pthread_mutex_destroy(pthread_mutex_t* mutex);

死锁::对所资源的竞争以及进程/线程加锁的推进顺序不当,因为对一些无法加锁的锁进行加锁而导致程序卡死

死锁产生的四个必要条件:

  1. 互斥条件(我能操作别人不能操作)
  2. 不可剥夺操作(我的锁,别人不能解)
  3. 请求与保持条件(拿着碗里的,看着锅里的)
  4. 环路等待条件

避免死锁:破坏必要条件

死锁处理:死锁检测算法 ,银行家算法

2.同步

条件变量:描述某些资源就绪与否的状态,为了实现同步而引入。同步是以互斥为前提的。少数情况可实现无锁同步。

1.定义条件变量 pthread_cond_t

pthread_cond_t condition;

2.初始化条件变量 pthread_cond_init

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
参数: pthread_cond_attr是用来设置pthread_cond_t的属性,当传入的值是NULL的时候表示使用默认的属性。

3.等待 / 唤醒 pthread_cond_wait / pthread_cond_signal

int pthread_cond_broadcast(pthread_cond_t* cond);//唤醒条件变量等待的所有线程
int pthread_cond_signal(pthread_cond_t* cond)//唤醒条件变量上等待的一个线程
int pthread_cond_wait(pthread_cond_t* cond)//解锁互斥量并等待条件变量触发
int pthread_cond_timewait(pthread_cond_t* cond,int abstime)//pthread_cond_wait,但可设定等待超时 

4.销毁条件变量 pthread_cond_destroy

int pthread_cond_destroy(pthread_cond_t* cond);

条件变量为什么要搭配互斥锁使用?
因为条件变量本身只提供等待与唤醒的功能,具体要什么时候等待需要用户来进行判断.这个条件的判断,通常涉及临界资源的操作(其他线程要通过修改条件,来促使条件满足), 而这个临界资源的操作应该受到保护.因此要搭配互斥锁一起使用。

3.互斥和同步的联合使用

pthread_mutex_lock(&mutex)
while or if(线程执行的条件是否成立)
    pthread_cond_wait(&cond,&mutex);
    线程执行
pthread_mutex_unlock(&mutex);

五、其他操作

进程id:

这里所说的进程ID指我们通过fork创建子进程,子进程和父进程在内核中独立运行,并且一个进程对应一个进程描述符(PCB),PCB中包含了进程的ID,通过getpid返回当前进程ID

线程id:

  • 内核态线程id:linux内核中,并不存在线程这一说,而是通过复制了进程的PCB作为标识自己(线程),作为进程的一个执行分支;既然有进程描述符(PCB)标识,自然就有一个标识符(ID)来标识着我是你(进程)的哪一个分支,这个标识符(ID)就是内核中的线程ID,通过syscall获得
#include 

pid_t tid;
tid = syscall(SYS_gettid); //在线程执行的函数中调用此接口
  • 用户态线程id:对线程的操控是由用户自己来完成,那么对此线程操控,用户知道你是哪一个线程,故此又有了用户态的线程ID;这里我们通过pthread_self()函数获得。
 #include 
pthread_t pthread_self(void); //在线程执行的函数中调用此接口
返回值:成功返回0,失败返回错误码
注:这里的ID是一个地址,而不是向上面两个ID是一个整数

对于单线程的进程,内核中tid==pid,对于多线程进程,他们有相同的pid,不同的tid。tid用于描述内核真实的pid和tid信息。

pthread_self返回的是posix定义的线程ID,man手册明确说明了和内核线程tid不同。它只是用来区分某个进程中不同的线程,当一个线程退出后,新创建的线程可以复用原来的id。

描述线程的id,为什么需要两个不同的ID呢?

答:这是因为线程库实际上由两部分组成:内核的线程支持+用户态的库支持(glibc),Linux在早期内核不支持线程的时候glibc就在库中(用户态)以纤程(就是用户态线程)的方式支持多线程了,POSIX thread只要求了用户编程的调用接口对内核接口没有要求。linux上的线程实现就是在内核支持的基础上以POSIX thread的方式对外封装了接口,所以才会有两个ID的问题。

gettid 获取的是内核中真实线程ID,  对于多线程进程来说,每个tid实际是不一样的。

而pthread_self获取的是相对于进程的线程控制块的首地址, 只是用来描述统一进程中的不同线程

Linux多线程操作pthread_t_第1张图片

你可能感兴趣的:(Linux)