Linux应用 线程
一、线程的概念
进程是系统分配资源的基本单位,线程是系统调度的基本单位。
线程是进程实际的执行单位,一个进程可以创建很多个进程,多个进程并发运行,每个线程要执行不同的任务。
线程的创建
我们执行一个程序,就意味着创建了一个进程,每个进程都有一个主线程,就是我们一开始创建的那个,main函数就是主线程的入口函数。
其他的线程都是由主线程创建的,主线程通常会在最后执行完成,执行各种清理工作。
线程的特点
线程是程序的最基本运行单位,可以理解为进程仅仅是一个容器,包含了线程执行需要的各种数据和变量。
同一个进程中的多个线程共享进程中的全部系统资源、如虚拟地址空间,文件描述符和信号处理等。但同一进程中的多个线程有各自的调用栈(call stack,我们称为线程栈),自己的寄存器环境(registercontext)、自己的线程本地存储(thread-local storage)。
1、线程不单独存在,线程包含在进程中
2、线程是参与调度的基本单位
3、线程可以并发执行,实现宏观上的并行
4、同一个进程中的各个线程共享进程所拥有的资源。
多线程的优缺点
进程的切换开销很大,多个进程实现并行的效率更低,所以使用切换开销小的线程进行并发。
进程间的通讯较为麻烦,每个进程都有自己的地址空间。多个线程因为共享进程的资源,所以切换的成本比较小
创建一个线程比创建一个进程更简单
多线程咋多核处理器上优势更大
二、线程ID
同进程一样,每一个线程都有一个线程ID对这个线程进行标识。线程在整个进程是唯一的(不是对于系统是唯一的,而是对于进程是唯一的)
一些关于进程的函数
#include <pthread.h>
//umsigned long int 就是 phread_t
phread_t pthread_self(void) //获取自己的线程号
int pthread_equal(pthread_t t1,pthread_t t2) //比较两个线程号是否相等
三、创建一个线程
相关函数
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);
//thread :接受线程ID的数据指针
//attr: 要创建的线程的线程属性
//start_routine:要创建的线程执行的函数
//arg :要传入线程的参数 (要保证线程执行生命周期内存在)
在创建线程以后,要把线程视为一个不透明的数据。
四、终止一个线程
线程的终止方式
1、在线程执行函数中return
2、线程函数调用pthread_exit()函数
3、调用pthread_cancel()终止线程。
相关函数
#include <pthread.h>
void pthread_exit(void* retval)
//retval:指明退出函数的返回值,在另一个线程中通过pthread_join()进行接受,参数不应该在线程栈中,因为线程结束以后线程栈会被回收
如果主线程使用pthread_exit()终止线程的话,主线程也会结束,但不影响其他线程的运行,等所有线程都结束以后,进程才会结束。
五、回收一个进程
在进程中可以使用wait()等待子进程的结束,在线程中可以使用pthread_join()阻塞等待线程的结束,同时可以获取线程的结束状态
#include <pthread.h>
int pthread_join(pthread_t thread,void** retval)
//等待指定线程的结束,同时获取线程的结束状态
如果线程已经终止,会直接返回,不阻塞,如果不使用pthread_join()回收进程资源,那么线程会变成僵尸线程,浪费系统资源,(在进程结束以后会被系统回收资源)
对于回收线程,没有规定线程的回收关系,也就是说,我创建的线程可以不由我回收,可以交给其他线程回收。
但是pthread_join()回收线程资源是阻塞的。
六、取消一个线程
取消线程通常发生在一个线程向另一个线程发送请求,要求这个线程进行取消,但是这种取消并不是一接受到请求就取消的,被请求的线程如果没有屏蔽取消信号,它会在一些取消点进行线程的取消。
#include <pthread.h>
int pthread_cancel(pthread_t thread)
//向指定线程发送取消请求
进程对应请求取消的状态是不同的,有对应的函数进行设置
#include <pthread.h>
int pthread_setcancelstate(int state, int *oldstate); //设置取消状态
int pthread_setcanceltype(int type, int *oldtype); //设置取消类型
取消状态
PTHREAD_CANCEL_ENABLE: 线程可以取消,这是新创建的线程取消性状态的默认值,所以新建线程以及主线程默认都是可以取消的。
PTHREAD_CANCEL_DISABLE: 线程不可被取消,如果此类线程接收到取消请求,则会将请求挂起,直至线程的取消性状态变为 PTHREAD_CANCEL_ENABLE。
取消类型
PTHREAD_CANCEL_DEFERRED: 取消请求到来时,线程还是继续运行,取消请求被挂起,直到线程到达某个取消点为止,这是所有新建线程包括主线程默认的取消性类型。
**PTHREAD_CANCEL_ASYNCHRONOUS:**可能会在任何时间点(也许是立即取消,但不一定)取消线程,这种取消性类型应用场景很少,不再介绍!
取消点
在执行一些函数的时候进行取消
必取消点
头文件 | 函数 |
---|---|
<stdio.h> | fgets , getline , gets , printf , scanf , fopen , fclose , freopen , open , close , read , write , lseek , fcntl …(所有会阻塞的 stdio/系统 I/O) |
<unistd.h> | read , write , close , fsync , fdatasync , sleep , usleep , nanosleep , pause , fcntl (F_SETLKW), ioctl (部分 cmd), pread , pwrite , readv , writev |
<pthread.h> | pthread_join , pthread_cond_wait , pthread_cond_timedwait , pthread_barrier_wait |
<semaphore.h> | sem_wait , sem_timedwait |
<mqueue.h> | mq_receive , mq_timedreceive , mq_send , mq_timedsend |
<poll.h> | poll , ppoll |
<sys/select.h> | select , pselect |
<sys/epoll.h> | epoll_wait , epoll_pwait |
<sys/socket.h> | accept , accept4 , connect , recv , recvfrom , recvmsg , send , sendto , sendmsg |
<sys/mman.h> | msync (MS_SYNC) |
<signal.h> | sigwait , sigtimedwait , sigwaitinfo |
<fcntl.h> | open , openat |
<aio.h> | aio_suspend |
可选取消点
函数族 |
---|
stat , lstat , fstat , fstatat |
closedir , readdir , readdir_r |
gethostbyname , gethostbyaddr |
mmap , munmap , mprotect , madvise |
flock , lockf , fcntl (F_SETLK 非阻塞) |
自己设置取消点
#include <pthread.h>
void pthread_testcancel(void);
其实就是在执行到这里的时候检查一下是否有取消请求,如果有取消请求,且可以取消,那就取消这个进程。
七、分离线程
分离线程就是在一些无所谓线程执行情况的时候,让系统创建一个线程去执行一些任务,然后执行结束对线程资源进行回收,那就用到了分离线程
#include <pthread.h>
int pthread_detach(pthread_t thread);
//将指定线程进行分离,一般用在新线程执行函数中
八、线程清理处理函数
和进程一样,在线程终止退出以后,也可以执行我们规定的一些函数,也就是线程清理函数。
一个线程可以创建多个线程清理函数,它们就像栈一样存放,也就后创建的先执行,
#include <pthread.h>
void pthread_cleanup_push(void (*routine)(void *), void *arg); //加入一个清理函数
void pthread_cleanup_pop(int execute); //弹出一个清理函数 execute为1时表示执行后弹出。
清理函数的执行时机:
1、调用pthread_exit()退出
2、线程相应取消请求
3、非0参数调用pthread_cleanup_pop
!!!值得注意的是 pthread_cleanup_push和pthread_cleanup_pop要成对出现,否则无法通过编译。
九、线程属性
创建线程的时候说过,可以传入线程属性,也就是一个pthread_arr_t数据结构,这个结构需要指定的函数去初始化和销毁
#include <pthread.h>
int pthread_attr_init(pthread_attr_t *attr);
int pthread_attr_destroy(pthread_attr_t *attr);
对于这个结构中的具体属性,也有指定的函数去修改,
//线程栈属性
int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(const pthread_attr_t *attr, void **stackaddr, size_t *stacksize);int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);
int pthread_attr_getstacksize(const pthread_attr_t *attr, size_t *stacksize);
int pthread_attr_setstackaddr(pthread_attr_t *attr, void *stackaddr);
int pthread_attr_getstackaddr(const pthread_attr_t *attr, void **stackaddr);
//分离状态属性
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
int pthread_attr_getdetachstate(const pthread_attr_t *attr, int *detachstate);
//detachstate:PTHREAD_CREATE_DETACHED表示一开始就分离 PTHREAD_CREATE_JOINABLE表示不分离
十、线程安全
线程栈
进程中创建的每个线程都有自己的栈地址空间,将其称为线程栈,线程栈中的数据是每个线程独属的。
因为线程栈的存在,引申出来一个可重入函数,可重入函数表示在线程的执行过程中,不会因为多个线程同时访问一个数据导致数据出错,那就是可重入函数,也就是可以进行函数嵌套。
重入指的是同一个函数被不同执行流调用,前一个执行流还没有执行完该函数、另一个执行流又开始调用该函数了,其实就是同一个函数被多个执行流并发/并行调用,在宏观角度上理解指的就是被多个执行流同时调用。
发生重入的情况有以下几种:
1、在一个含有信号处理的程序当中,主程序正执行函数 func(),此时进程接收到信号,主程序被打断,跳转到信号处理函数中执行,信号处理函数中也调用了 func()。
2、在多线程环境下,多个线程并发调用同一个函数。
可重入函数分为绝对的可重入函数和带条件的可重入函数。
绝对的可重入函数:
该函数内操作的变量均是函数内部定义的自动变量(局部变量),每次调用函数,都会在栈内存空间为局部变量分配内存,当函数调用结束返回时、再由系统回收这些变量占用的栈内存,所以局部变量生命周期只限于函数执行期间。除此之外,该函数的参数和返回值均是值类型、而并非是引用类型(就是指针)
带条件的可重入函数:
带条件的可重入函数通常需要满足一定的条件时才是可重入函数。
线程安全函数
一个函数被多个线程(其实也是多个执行流,但是不包括由信号处理函数所产生的执行流)同时调用时,它总会一直产生正确的结果,把这样的函数称为线程安全函数。线程安全函数包括可重入函数,可重入函数是线程安全函数的一个真子集,也就是说可重入函数一定是线程安全函数,但线程安全函数不一定是可重入函数.
一次性初始化
对于线程的一些函数中,有些只需要执行一次初始化,可以使用以下函数实现一次性初始化
#include <pthread.h>pthread_once_t once_control = PTHREAD_ONCE_INIT;int pthread_once(pthread_once_t *once_control, void (*init_routine)(void));
//pthrea_once_t:一个指针,需要我们初始化,也就是上面哪一行。后面的就是一次性函数代码//示例:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
static pthread_once_t once = PTHREAD_ONCE_INIT;
static void initialize_once(void)
{printf("initialize_once 被执行: 线程 ID<%lu>\n", pthread_self());
}
static void func(void)
{pthread_once(&once, initialize_once);//执行一次性初始化函数printf("函数 func 执行完毕.\n");
}
static void *thread_start(void *arg)
{printf("线程%d 被创建: 线程 ID<%lu>\n", *((int *)arg), pthread_self());func(); //调用函数 funcpthread_exit(NULL); //线程终止
}
static int nums[5] = {0, 1, 2, 3, 4};
int main(void)
{pthread_t tid[5];
int j;/* 创建 5 个线程 */for (j = 0; j < 5; j++)pthread_create(&tid[j], NULL, thread_start, &nums[j]);/* 等待线程结束 */for (j = 0; j < 5; j++)pthread_join(tid[j], NULL);//回收线程exit(0);
}
线程特有数据
线程特有数据也称为线程私有数据,简单点说,就是为每个调用线程分别维护一份变量的副本(copy),**每个线程通过特有数据键(key)访问时,这个特有数据键都会获取到本线程绑定的变量副本。**这样就可以避免变量成为多个线程间的共享数据。
为每一个调用线程(调用某函数的线程,该函数就是我们要通过线程特有数据将其实现为线程安全的函数)分配属于该线程的私有数据区,为每个调用线程分别维护一份变量的副本。
#include <pthread.h>
int pthread_key_create(pthread_key_t *key, void (*destructor)(void*));
//用于释放与特有数据键关联的线程私有数据区占用的内存空间,
//destructor表示当使用线程特有数据的线程终止时,destructor()函数会被自动调用。int pthread_setspecific(pthread_key_t key, const void *value);
//key :pthread_key_t 类型变量,参数 key 应赋值为调用 pthread_key_create()函数时创建的特有数据键,也就是 pthread_key_create()函数的参数 key 所指向的 pthread_key_t 变量。
//value :参数 value 是一个 void 类型的指针,指向由调用者分配的一块内存,作为线程的私有数据缓冲区,当线程终止时,会自动调用参数 key 指定的特有数据键对应的解构函数来释放这一块动态申请的内存空间void *pthread_getspecific(pthread_key_t key);
//通过特有键获取指定数据nt pthread_key_delete(pthread_key_t key);
//删除特有键
//使用示例
#define _GNU_SOURCE
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <pthread.h>
#define MAX_ERROR_LEN 256
static pthread_once_t once = PTHREAD_ONCE_INIT;
static pthread_key_t strerror_key;
static void destructor(void *buf)
{free(buf); //释放内存
}
static void create_key(void)
{
/* 创建一个键(key),并且绑定键的解构函数 */if (pthread_key_create(&strerror_key, destructor))pthread_exit(NULL);
}
/******************************
* 对 strerror 函数重写
* 使其变成为一个线程安全函数
******************************/
static char *strerror(int errnum)
{char *buf;/* 创建一个键(只执行一次 create_key) */if (pthread_once(&once, create_key))pthread_exit(NULL);/* 获取 */buf = pthread_getspecific(strerror_key);if (NULL == buf) { //首次调用 my_strerror 函数,则需给调用线程分配线程私有数据buf = malloc(MAX_ERROR_LEN);//分配内存if (NULL == buf)pthread_exit(NULL);/* 保存缓冲区地址,与键、线程关联起来 */if (pthread_setspecific(strerror_key, buf))pthread_exit(NULL);}if (errnum < 0 || errnum >= _sys_nerr || NULL == _sys_errlist[errnum])snprintf(buf, MAX_ERROR_LEN, "Unknown error %d", errnum);else {strncpy(buf, _sys_errlist[errnum], MAX_ERROR_LEN - 1);buf[MAX_ERROR_LEN - 1] = '\0';//终止字符}return buf;
}
线程局部存储
通常情况下,程序中定义的全局变量是进程中所有线程共享的,所有线程都可以访问这些全局变量;而线程局部存储在定义全局或静态变量时,使用**__thread 修饰符修饰变量**,此时,每个线程都会拥有一份对该变量的拷贝。线程局部存储中的变量将一直存在,直至线程终止,届时会自动释放这一存储。
static __thread char buf[512];
⚫ 如果变量声明中使用了关键字 static 或 extern,那么关键字__thread 必须紧随其后。
⚫ 与一般的全局或静态变量声明一样,线程局部变量在申明时可设置一个初始值。
⚫ 可以使用 C 语言取值操作符(&)来获取线程局部变量的地址
十一、线程与信号
信号与线程存在冲突
1、信号的系统默认行为是属于进程层面。8.3 小节介绍到,每一个信号都有其对应的系统默认动作,当进程中的任一线程收到任何一个未经处理(忽略或捕获)的信号时,会执行该信号的默认操作,信号的默认操作通常是停止或终止进程。
2、信号处理函数属于进程层面。进程中的所有线程共享程序中所注册的信号处理函数
3、产生了硬件异常相关信号,譬如 SIGBUS、SIGFPE、SIGILL 和 SIGSEGV 信号;这些硬件异常信号在某个线程执行指令的过程中产生,也就是说这些硬件异常信号是由某个线程所引起;那么在这种情况下,系统会将信号发送给该线程。
4、当线程试图对已断开的管道进行写操作时所产生的 SIGPIPE 信号;
5、由函数 pthread_kill()或 pthread_sigqueue()所发出的信号;这些函数允许线程向同一进程下的其它线程发送一个指定的信号。
6、当一个多线程进程接收到一个信号时,且该信号绑定了信号处理函数时,内核会任选一个线程来接收这个信号,意味着由该线程接收信号并调用信号处理函数对其进行处理,并不是每个线程都会接收到该信号并调用信号处理函数;
7、信号掩码其实是属于线程层面的,也就是说信号掩码是针对每个线程而言。那么在多线程环境下,各个线程可以调用 pthread_sigmask()函数来设置它们各自的信号掩码,譬如设置线程可以接收哪些信号、不接收哪些信号,各线程可独立阻止或放行各种信号
线程的信号掩码
对于一个单线程程序来说,使用 sigprocmask()函数设置进程的信号掩码,在多线程环境下,使用pthread_sigmask()函数来设置各个线程的信号掩码,其函数原型如下所示
int pthread_sigmask(int how, const sigset_t *set, sigset_t *oldset);
向线程发送信号
调用 kill()或 sigqueue()所发送的信号都是针对整个进程来说的,它属于进程层面,具体该目标进程中的哪一个线程会去处理信号,由内核进行选择。事实上,在多线程程序中,可以通过 pthread_kill()向同一进程中的某个指定线程发送信号,
int pthread_kill(pthread_t thread, int sig);int pthread_sigqueue(pthread_t thread, int sig, const union sigval value);//支持排队
异步信号安全问题
线程安全函数不能在信号处理函数中被调用,否则就不能保证它一定是安全的。所以就出现了异步信号安全函数。
异步信号安全函数指的是可以在信号处理函数中可以被安全调用的线程安全函数,所以它比线程安全函数的要求更为严格!可重入函数满足这个要求,所以可重入函数一定是异步信号安全函数。而线程安全函数则不一定是异步信号安全函数了。
主要发生的原因:
1、信号是异步的,信号可能会在任何时间点中断主程序的运行,跳转到信号处理函数处执行,从而形成一个新的执行流(信号处理函数执行流)。
2、信号处理函数执行流与线程执行流存在一些区别,信号处理函数所产生的执行流是由执行信号处理函数的线程所触发的,它俩是在同一个线程中,属于同一个线程执行流。
对于一个安全的信号处理函数而言
1、首先确保信号处理函数本身的代码是可重入的,且只能调用异步信号安全函数;
2、当主程序执行不安全函数或是去操作信号处理函数也可能会更新的全局数据结构时,要阻塞信号的传递。