一、线程同步
线程之间协同工作,用于多线程访问临界资源,必须按照一定的先后顺序访问执行。线程同步的方式包括线程级的信号量、互斥锁以及条件变量
1、线程级的信号量
线程级的信号量有两种,一是二进制信号量,二是计数型信号量;当有多个临界资源时,采用计数型信号量。进程间同步的信号量是系统V信号量,线程间同步的信号量是POSIX,这两个信号量不可以交换使用。
头文件:#include<semaphore.h>
Sem_t sem;//类型
初始化Int sem_init(sem_t *sem,int shared,int val);//一个信号量
信号量sem一般被定义在线程共享的全局数据区,sem_init函数是将信号量sem的初始值设置为val,shared参数控制这个信号量是否可以在多个进程之间共享,但是Linux对此不支持。
P操作Int sem_wait(sem_t *sem);//会阻塞
对信号量sem进行-1操作,如果结果小于0,则此函数调用会阻塞,直到有其他线程执行V操作。
V操作Int sem_post(sem_t *sem);
对信号量进行+1操作;
删除操作Int sem_destory(sem_t *sem);
2、互斥锁即互斥量(针对一个临界资源进行控制)
可以通过使用pthread的互斥接口保护数据,确保同一时间只有一个线程访问数据。互斥量(mutex)从本质上说是一把锁,在访问共享资源前对互斥量进行加锁,在访问完成后释放互斥量上的锁。对互斥量进行加锁以后,任何其他试图再次对互斥量加锁的线程将会被阻塞直到当前线程释放该互斥锁。如果释放互斥锁时有多个线程阻塞,所有在该互斥锁上的阻塞线程都会变成可运行状态,第一个变为运行状态的线程可以对互斥量加锁,其他线程将会看到互斥锁依然被锁住,只能回去再次等待它重新变为可用。在这种方式下,每次只有一个线程可以向前执行。
锁的类型:pthread_mutex_t
锁的初始化:
int pthread_mutex_init(pthread_mutex_t *mutex,pthread_mutexattr_t *attr);
互斥锁mutex一般被定义在线程共享的全局数据区,此函数是初始化互斥锁mutex,attr为锁的属性;
加锁:
Int pthread_mutex_lock(pthread_mutex_t *mutex);//阻塞
Int pthread_mutex_trylock(pthread_mutex_t *mutex);//尝试加锁 不会阻塞
解锁:
Int pthread_mutex_unlock(pthread_mutex_t *mutex);
销毁:
Int pthread_mutex_destroy(pthread_mutex_t *mutex);
3、条件变量
为多个线程提供了一个汇合的场所;条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生。
二、线程安全-----可重入函数
相同的条件,同一份程序,多次执行结果不同,执行结果有二义性。
1、产生线程不安全地原因:
线程是并发执行的或者是并行的;
操作是非原子操作----在并行系统上;
操作的对象是同一个,线程间数据(.data,.bss)是共享的;
操作是非互斥的-----在并行系统上;
2、由于线程之间共享全局数据、静态数据。在编程过程中,必须对线程访问的这些共享资源做一些同步控制,但是有些系统调用或者库函数在实现时,用到了静态的数据,当在多线程环境中调用到这些函数时,就会出现不安全的现象。对于这些函数,在多线程环境中必须使用它们的安全版本,即就是可重入版本。
例如:字符串分割函数:char *strtok(char *sourdtr, const char *flag)的可重入版本为:char *strtok_r(char *sourstr,const char *flag,char **res);
三、线程中fork的使用及其锁的继承
线程中fork的使用:在多线程程序中,某一条线程调用fork生成子进程,在子进程中,只有调用fork函数的线程会被启动,其他线程不会启动(不会运行)。
子进程会继承父进程中的锁,以及其状态。所以,有可能子进程发生死锁。
解决方案:在fork之前调用pthread_atfork函数。
#include<pthread.h>
Int pthread_atfork(void(*prepare)(void), void(*parent)(void) , void(*child)(void));
void(*prepare)(void):fork之前调用,对所有锁加锁
void(*parent)(void):fork完成之后,父进程空间调用,解锁
void(*child)(void):fork完成之后,子进程空间调用,解锁
返回值:成功则返回0,否则返回错误编号;
Strace:跟踪查看进程执行期间的系统调用函数;
操作系统为每一个进程维护一个页表,因而进程间的线程共享4G的虚拟内存空间,共享同一个页表;(.data .bss)存放全局数据和静态数据
原子操作的底层实现:
一是用锁来实现;
二是操作之前将中断机制屏蔽,操作之后恢复中断;
原子性是保证操作不会被中断,互斥性保证多个线程可以进行操作;
从内存中获取其初值;
CPU执行++;
将结果存储到内存上;