一、线程安全——可重入函数
概念:一个函数被称为线程安全的(thread-safe),当且仅当被多个并发进程反复调用时,它会一直产生正确的结果。反之,如果一个函数不是线程安全的,我们就说它是线程不安全的(thread-unsafe)。
1、调用线程安全的函数,可重入函数;(有些系统调用或者库函数在实现时,用到了静态的数据,在多线程调用时,出现不安全现象)
2、同步(因为线程之间共享全局数据、静态数据)
以下四种函数是线程不安全的:
1、不保护共享变量的函数;
2、函数状态随着调用改变的函数;
3、返回指向静态变量指针的函数;
4、调用线程不安全函数的函数;
判断线程是否安全:
如果代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。或者说一个类或者程序所提供的接口对于线程来说是原子操作或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题。
线程安全问题都是由全局变量及静态变量引起的。
若每个线程中对全局变量、静态变量只有读操作,而无写操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步,否则的话就可能影响线程安全。一般来说,一个函数被称为线程安全的,当且仅当被多个并发线程反复调用时,它会一直产生正确的结果。
根据线程的同步与互斥,也就是当两个线程同时访问到同一个临界资源的时候,如果对临界资源的操作不是原子的就会产生冲突,使得结果并不如最终预期的那样。
写线程安全函数时应注意:
1, 减少对临界资源的依赖,尽量避免访问全局变量,静态变量或其它共享资源,如果必须要使用共享资源,所有使用到的地方必须要进行互斥锁 (Mutex) 保护;
2, 线程安全的函数所调用到的函数也应该是线程安全的,如果所调用的函数不是线程安全的,那么这些函数也必须被互斥锁 (Mutex) 保护;
一个函数成为可重入的函数,必须满足下列要求:
a) 不能使用静态或者全局的非常量数据
b) 不能够返回地址给静态或者全局的非常量数据
c) 函数使用的数据由调用者提供
d) 不能够依赖于单一资源的锁
e) 不能够调用非可重入的函数
线程安全:(相同的条件,同一份程序,多次执行结果不同,执行结果有二义性)
1、从内存中获取其初始值;
2、CPU执行++;
3、将结果存储到内存上。
安全原因:
1、线程时并发运行的;
2、操作是非原子操作 → 在并发系统上;
3、操作对象是同一个,线程之间数据(.data .bsss)共享
4、在并行系统上 → 操作必须是互斥的。
二、线程的实现
分配资源 → 进程
调度、执行 → 线程
1、创建线程 → 只会在进程资源中申请栈区资源,用于线程函数的执行。进程中的数据区域、堆区多线程都是使用同一份,其都是共享的。
pthread_create(pthread_t *thread,NULL,void *(*start routine)(void*),void*arg);
pthread_t *thread: &id
NULL:线程的属性(不改变);
void *(*start routine)(void*): 将要启动执行的函数;(调用函数)
void*arg: 传递给该函数的参数;NULL
创建时给函数线程传递的两种方式:
1)、值传递——最多传递 4 字节的数据,指针将传递的值直接强转为void*
arg:类型为void* 记录的传递的值
2)、地址传递——将要传递的值的地址转化为void*
arg:类型为void* 记录的是传递的地址
2、线程结束
结束线程 void pthread_exit(void * reval);
main函数结束,进程结束 exit(0);
线程依赖于进程,进程结束,所有线程随之结束。
3、等待线程结束,获取线程退出信息
int pthread_join(pthread_t id,void ** retval); → waitpid();
4、取消一个线程:只是发出取消请求,并不会阻塞。
int pthread_cancel(pthread_t id);
三、多线程环境下创建进程(多线程中执行fork的情况)
1、 由一个线程调用fork函数创建子进程,在子进程中,只有调用fork的线程会被启动,其他线程不会执行。
2、 多线程环境下对锁进行加锁,然后创建进程,则子进程中对锁加锁可能会出现死锁(子进程会复制父进程的锁)
3、 保证子进程复制父进程的锁是解锁状态;
int pthread_atfork(void(*parent)(void) , void(*parent)(void) , void(*child)(void));
void(*parent)(void):fork调用之后,真正复制进程之前执行的。(对父进程所有的锁加锁)
void(*parent)(void):fork复制进程之后,在父进程空间中执行,对所有的锁加锁;
void(*child)(void):fork复制进程之后,在子进程空间中执行,对所有的锁解锁。
子进程会继承父进程中的锁、及其状态。所以,子进程有可能发生死锁。
多线程中执行fork
父进程多个线程,子进程只启用一个线程
四、多线程调试 gdb
attach pid:在gdb中调试该命令添加要调试的进程(pid)
set follow-fork-mode parent/child :默认为parent代表跟踪父进程,可设置为child代表跟踪子进程。
infor threads :查看有哪些线程可调式
thread id:切换到指定线程
set scheduler-locking step/ off / on
step:在单步执行时,只有当前被调试线程执行;
off:代表不锁定任何线程;
on:只有被调试的线程可以执行,其他不执行;
ulimit -a:显示所有限制
ulimit -s:查看大小
ulimit -s +(大小):更改栈大小
ulimit -u:查看用户最大进程数
strace 跟踪系统调用
ltrace 跟踪程序中调用的库函数