您的位置首页>企业动态>

Linux多线程与同步

导读大家好,我是极客范的本期栏目编辑小友,现在为大家讲解Linux多线程与同步问题。典型的UNIX系统支持创建多线程的过程。在Linux进程基础中,

大家好,我是极客范的本期栏目编辑小友,现在为大家讲解Linux多线程与同步问题。

典型的UNIX系统支持创建多线程的过程。在Linux进程基础中,提到Linux是按进程组织操作的,Linux中的线程也是基于进程的。虽然实现与其他UNIX系统不同,但Linux多线程在逻辑和使用上与真正的多线程并无不同。

多线程我们先来看看什么是多线程。在Linux中,从程序到进程,我们在内存中看到程序的表示。在这个程序的整个运行过程中,只有一个控制权。当一个函数被调用时,它获得控制权,成为acTIve函数,然后运行函数中的指令。同时,其他功能处于离开状态,不运行。如下图所示。

Linux从程序到进程。

我们可以看到正方形用箭头连接起来。每个功能就像是连接在一条线上,计算机像流水线一样执行每个功能中定义的操作。这样的程序称为单线程程序。

多线程是允许一个进程中有多个控制权,这样可以同时激活多个功能,让多个功能的操作同时运行。即使是单CPU的计算机,也能不断在不同线程的指令之间切换,导致多个线程同时运行。如下图所示,这是一个多线程进程:

Main()到func3()再到main()构成一个线程,func1()和func2()构成另外两个线程。操作系统通常有一些系统调用,让你在一个新的线程中运行一个函数。

回忆一下Linux中提到的栈从程序到进程的功能和用途。一个栈,只有最低的帧可以读写。因此,只有对应于框架的功能被激活并处于工作状态。为了实现多线程,我们必须绕过堆栈的限制。为此,当创建一个新线程时,我们为这个线程构建一个新的堆栈。每个堆栈对应一个线程。当一个栈完全弹出时,对应的线程完成任务并结束一天。因此,多线程进程在内存中有多个堆栈。多个堆叠由用于堆叠生长的特定空白区域隔开。每个线程都可以调用堆栈底部框架中的参数和变量,并与其他线程共享内存中的文本、堆和全局数据区域。对应于上面的例子,我们需要在进程空间中有3个栈。

(需要注意的是,对于多线程,由于同一个进程空间中有多个栈,填充任何空白区域都会导致栈溢出。)

并发多线程相当于一个会议系统。并发系统通常同时执行多个任务。如果多个任务可以共享资源,特别是同时写一个变量时,就需要解决同步问题。例如,我们有一个多线程火车售票系统,它使用全局变量I来存储剩余的投票。许多线程一直在卖票(i=i-1),直到剩余票数为0。因此,每个都需要执行以下操作:

while(1){if (i!=0)I=I-1 else { printf(' no more ticks ');exit();}}如果只有一个线程在执行上述程序(相当于一个窗口卖票),就没有问题。但是,如果多个线程执行上述程序(相当于在多个窗口售票),我们就会出现问题。我们可以看到,根本原因在于所有并发线程都可以读写I。

我们这里的if结构会给CPU两个指令,一个是判断是否还有剩余票(我!=0),一个是卖票(i=i -1)。一个线程会先判断是否有票(比如此时I为1),但两个指令之间有一个时间窗口,其他线程可能会在这个时间窗口内执行卖票操作(i=i -1),导致这个线程的卖票条件不再有效。但是由于线程已经执行了判断指令,不可能知道我已经变了,所以继续执行卖票指令,从而卖出不存在的票(我变成负数)。对于一个真正的售票系统来说,这将是一个严重的错误(售出了太多的票,火车满员)。

在并发的情况下,指令执行的顺序由内核决定。在同一个线程中,指令是按顺序执行的,但是很难说不同线程中的哪些指令会先执行。如果运行结果取决于不同线程的执行顺序,它将创建一个race condiTIon。在这种情况下,计算机的结果很难预测。我们应该尽力避免竞争条件的形成。解决竞争条件最常见的方法是由两个先前分离的指令组成一个不可分割的原子操作,而其他任务不能插入到原子操作中。

多线程同步对于多线程程序,同步意味着在一定时间内只允许一个线程访问某个资源。在此期间,不允许其他线程访问资源。我们可以通过互斥、条件变量和读写锁来同步资源。

1)互斥锁。

互斥锁是一个特殊的变量,它有两种状态:锁定和解锁。互斥锁通常被设置为全局变量。由打开的互斥体可以由线程获得。一旦获得,互斥锁就被锁定,只有线程有权在之后打开它。想要获取互斥体的其他线程将等待,直到互斥体再次打开。我们可以把mutex想象成只能容纳一个人的浴室。当有人进入浴室时,他们可以从里面锁上浴室。

上。其它人只能在互斥锁外面等待那个人出来,才能进去。在外面等候的人并没有排队,谁先看到洗手间空了,就可以首先冲进去。

上面的问题很容易使用互斥锁的问题解决,每个线程的程序可以改为:

while (1) { mutex_lock(mu);       if (i != 0) i = i - 1; else { printf("no more tickets"); exit(); } mutex_unlock(mu);     }

第一个执行mutex_lock()的线程会先获得mu。其它想要获得mu的线程必须等待,直到第一个线程执行到mutex_unlock()释放mu,才可以获得mu,并继续执行线程。所以线程在mutex_lock()和mutex_unlock()之间的操作时,不会被其它线程影响,就构成了一个原子操作。

需要注意的时候,如果存在某个线程依然使用原先的程序 (即不尝试获得mu,而直接修改i),互斥锁不能阻止该程序修改i,互斥锁就失去了保护资源的意义。所以,互斥锁机制需要程序员自己来写出完善的程序来实现互斥锁的功能。我们下面讲的其它机制也是如此。

 

2) 条件变量

条件变量是另一种常用的变量。它也常常被保存为全局变量,并和互斥锁合作。

 

假设这样一个状况: 有100个工人,每人负责装修一个房间。当有10个房间装修完成的时候,老板就通知相应的十个工人一起去喝啤酒。

我们如何实现呢?老板让工人在装修好房间之后,去检查已经装修好的房间数。但多线程条件下,会有竞争条件的危险。也就是说,其他工人有可能会在该工人装修好房子和检查之间完成工作。采用下面方式解决:

mutex_lock(mu)num = num + 1; if (num <= 10) { cond_wait(mu, cond);     printf("drink beer");}else if (num = 11) { cond_broadcast(mu, cond);        }mutex_unlock(mu);

上面使用了条件变量。条件变量除了要和互斥锁配合之外,还需要和另一个全局变量配合(这里的num, 也就是装修好的房间数)。这个全局变量用来构成各个条件。

 

具体思路如下。我们让工人在装修好房间(num = num + 1)之后,去检查已经装修好的房间数( num < 10 )。由于mu被锁上,所以不会有其他工人在此期间装修房间(改变num的值)。如果该工人是前十个完成的人,那么我们就调用cond_wait()函数。cond_wait()做两件事情,一个是释放mu,从而让别的工人可以建房。另一个是等待,直到cond的通知。这样的话,符合条件的线程就开始等待。

当有通知(第十个房间已经修建好)到达的时候,condwait()会再次锁上mu。线程的恢复运行,执行下一句prinft("drink beer") (喝啤酒!)。从这里开始,直到mutex_unlock(),就构成了另一个互斥锁结构。

那么,前面十个调用cond_wait()的线程如何得到的通知呢?我们注意到elif if,即修建好第11个房间的人,负责调用cond_broadcast()。这个函数会给所有调用cond_wait()的线程放送通知,以便让那些线程恢复运行。

 

条件变量特别适用于多个线程等待某个条件的发生。如果不使用条件变量,那么每个线程就需要不断尝试获得互斥锁并检查条件是否发生,这样大大浪费了系统的资源。

 

3) 读写锁

读写锁与互斥锁非常相似。r、RW lock有三种状态: 共享读取锁(shared-read), 互斥写入锁(exclusive-write lock), 打开(unlock)。后两种状态与之前的互斥锁两种状态完全相同。

一个unlock的RW lock可以被某个线程获取R锁或者W锁。

如果被一个线程获得R锁,RW lock可以被其它线程继续获得R锁,而不必等待该线程释放R锁。但是,如果此时有其它线程想要获得W锁,它必须等到所有持有共享读取锁的线程释放掉各自的R锁。

如果一个锁被一个线程获得W锁,那么其它线程,无论是想要获取R锁还是W锁,都必须等待该线程释放W锁。

这样,多个线程就可以同时读取共享资源。而具有危险性的写入操作则得到了互斥锁的保护。

 

我们需要同步并发系统,这为程序员编程带来了难度。但是多线程系统可以很好的解决许多IO瓶颈的问题。比如我们监听网络端口。如果我们只有一个线程,那么我们必须监听,接收请求,处理,回复,再监听。如果我们使用多线程系统,则可以让多个线程监听。当我们的某个线程进行处理的时候,我们还可以有其他的线程继续监听,这样,就大大提高了系统的利用率。在数据越来越大,服务器读写操作越来越多的今天,这具有相当的意义。多线程还可以更有效地利用多CPU的环境。

(就像做饭一样,不断切换去处理不同的菜。)

 

本文中的程序采用伪C的写法。不同的语言有不同的函数名(比如mutex_lock)。这里关注的是逻辑上的概念,而不是具体的实现和语言规范。

 

总结

multiple threads, multiple stacks

race condition

mutex, condition variable, RW lock

郑重声明:本文版权归原作者所有,转载文章仅为传播更多信息之目的,如作者信息标记有误,请第一时间联系我们修改或删除,多谢。