了解线程间同步方法之前,还是要弄清楚线程是什么。
线程是进程内部的一条执行路径或者序列,是CPU调度的基本单位。
进程是一个正在运行的程序,是资源分配的基本单位。
在操作系统中将线程的实现分为了三类
内核线程和普通的进程间的区别:内核线程没有独立的地址空间(实际上它的mm指针被设置为NULL),它们只在内核空间运行,从来不会切换到用户空间去。内核进程和普通进程一样,可以被调度,也可以被抢占。
Linux中的线程实现的就更独特了。**从内核角度来说,其实并没有线程这个概念,Linux把所有的线程都当做进程来实现。**内核并没有准备特别的调度算法或是定义特别的数据结构来表征线程。相反,线程仅仅被视为一个与其他进程共享某些资源的进程。每个线程都拥有唯一隶属于自己的PCB。所以在内核中,它看起来就像是一个普通的进程(只是该进程和其他一些进程共享某些资源,如地址空间)。
如果进程内部只有一条执行路径时,PID也就是线程的ID。
如图,进程thread的进程ID和线程ID是相同的。
如果进程内部有多条线程时,每个线程也会有自己的ID,就是LWP。
ps -ef可以查看当前所有进程的状态,但是查不到线程的PID。所以ps查看线程PID的参数是 -L
top -H 用来查看线程对资源的使用情况。
多线程使得进程内部的执行路径变成了多条,并且这多条执行路径在并发运行。那么程序的执行具有一定的不稳定性,每次执行的结果可能都会不同,因为程序交替执行的顺序和时机不同。其次,使得某些资源的访问出现了竞争,问题变得困难,需要资源进行同步。这会使得程序的可靠性和稳定性降低。
在进程中,多个线程共享该进程的资源,控制线程对资源的访问,使得彼此不会干涉,冲突。控制线程对临界资源的访问,保证同一时刻只有一个线程访问。
所以,线程的同步是非常重要的。线程的同步主要是为了更好的控制线程执行和访问代码临界区。常用到的同步方法有信号量和互斥锁,条件变量,读写锁。
内核中,线程同步用到的方法有自旋锁。
有两组接口函数都是用于信号量的,一组是被称为系统V信号量,常用于进程的同步。另外一组就是用于线程同步,也就是接下来的这组函数。
信号量被分为两种:
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);//创建信号量
int sem_wait(sem_t *sem);//原子减1,也就是P操作
int sem_post(sem_t *sem);//原子加1,也就是V操作
int sem_destroy(sem_t *sem);//清理信号量拥有的所有资源
#include
#include
#include
#include
#include
#include
#include
//先定义一个信号量
sem_t sem;
void *thread_fun(void *arg)
{
int i = 0;
for(;i<5;i++)
{
sem_wait(&sem);//原子减1
write(1,"B",1);
sleep(1);
sem_post(&sem);//原子加1
}
}
int main()
{
pthread_t id;
sem_init(&sem,0,1);
pthread_create(&id,NULL,thread_fun,NULL);
int i = 0;
for(;i<5;i++)
{
sem_wait(&sem);//原子减1
write(1,"A",1);
sleep(1);
sem_post(&sem);//原子加1
}
pthread_join(id,NULL);
sem_destroy(&sem);//destroy signal
exit(0);
}
#include
#include
#include
#include
#include
#include
#include
sem_t sem_a,sem_b,sem_c;
void *thread_fun2(void *arg)
{
int i = 0;
for(;i<5;++i)
{
sem_wait(&sem_c);
write(1,"C",1);
sleep(1);
sem_post(&sem_a);
}
}
void *thread_fun1(void *arg)
{
int i = 0;
for(;i<5;i++)
{
sem_wait(&sem_b);
write(1,"B",1);
sleep(1);
sem_post(&sem_c);
}
}
int main()
{
pthread_t id1;
pthread_t id2;
// pthread_t id3;
sem_init(&sem_a,0,1);
sem_init(&sem_b,0,0);
sem_init(&sem_c,0,0);
pthread_create(&id1,NULL,thread_fun1,NULL);
pthread_create(&id2,NULL,thread_fun2,NULL);
int i = 0;
for(;i<5;i++)
{
sem_wait(&sem_a);
write(1,"A",1);//begin
sleep(1);
sem_post(&sem_b);
}
pthread_join(id2,NULL);
sem_destroy(&sem_a);//destroy signal
sem_destroy(&sem_b);
sem_destroy(&sem_c);
exit(0);
}
多线程程序中用来同步访问的方法还有一种就是使用互斥锁(互斥量),使得每次只能有一个线程访问它。为了控制对关键代码的访问,必须在进入这段代码之前锁住一个互斥量,然后在完成操作之后进行解锁。
互斥锁的取值只能为1,相当于信号量中的二值信号量,只能用0和1来表示。
一组函数接口如下:
#include
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex);
与其他函数一样,成功时返回0,失败时将返回错误代码,但这些函数并不设置errno,所以必须对函数的返回代码进行检查。
互斥锁声明的对象的类型为 pthread_mutex_t。
代码实现:
#include
#include
#include
#include
#include
#include
#include
thread_mutex_t mutex;
void *thread_fun(void *arg)
{
int i = 0;
for(;i<5;i++)
{
pthread_mutex_lock(&mutex);
write(1,"B",1);
int n = rand() % 3;
sleep(n);
write(1,"B",1);
pthread_mutex_unlock(&mutex);
n = rand() % 3;
sleep(n);
}
}
int main()
{
pthread_t id;
pthread_mutex_init(&mutex,NULL);
pthread_create(&id,NULL,thread_fun,NULL);
int i = 0;
for(;i<5;i++)
{
pthread_mutex_lock(&mutex);
write(1,"A",1);//begin
int n = rand() % 3;
sleep(n);
write(1,"A",1);//end
pthread_mutex_unlock(&mutex);
n = rand() % 3;
sleep(n);
}
pthread_join(id,NULL);
pthread_mutex_destroy(&mutex);
exit(0);
}
在现实生活中,临界区很多情况下不只在一个函数或者代码中。比如,如果我们要将当前数据结构中的数据一一移出,然后进行格式转换和数据解析,最后再把它加入到另一个数据结构中。整个执行过程必须是原子的,在数据被更新完毕前,不能有其它代码来读取这些数据。这样的话,原子操作就无法满足需求了。所以需要使用到一种更为复杂的同步方法——锁来提供保护。
**Linux内核中最常见的锁就是自旋锁(spin lock)。自旋锁最多只能被一个可执行线程持有,所以同一时刻只能有一个线程位于临界区。**如果一个执行线程想要去获得一个被争用的自旋锁,那么该线程就会一直进行忙循环——旋转——等待锁重新可用。如果锁未被争用,那么请求锁的线程就可以立刻得到它,继续执行。在任何时刻,自旋锁都可以防止多于一个的执行线程同时进入临界区。同一个锁可以用在多个位置。
一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋,这种行为是自旋锁的要点。
Linux内核实现的自旋锁是不可递归的。如果你试图得到一个你正持有的锁,你必须自旋,等待自己释放。但要是你处于自旋忙等待中,就永远没有机会释放锁,就被锁死了。
自旋锁可以使用在中断处理程序中。
自旋锁使用注意事项
由于自旋锁最主要特性是不断反复循环测试自己是否可以获得锁:
Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已经被占用的信号量时,信号量会将其推进一个等待队列,然后让其睡眠。这时处理器就可以去处理别的事了。当有持有信号量的进程将信号量释放后,等待队列中的任务将被唤醒,并获得该信号量。
关于使用信号量的结论: