多线程同步原因:
多线程相当于一个并发(concunrrency)系统。并发系统一般同时执行多个任务。如果多个任务可以共享资源,特别是同时写入某个变量的时候,就需要解决同步的问题。比如说,我们有一个多线程火车售票系统,用全局变量i存储剩余的票数。多个线程不断地卖票(i = i - 1),直到剩余票数为0。所以每个都需要执行如下操作:
while (1) {
*infinite loop*/
if (i !=0)}
i = i -1
else { printf("no more tickets");
exit();
}
如果只有一个线程执行上面的程序的时候(相当于一个窗口售票),则没有问题。但如果多个线程都执行上面的程序(相当于多个窗口售票), 我们就会出现问题。我们会看到,其根本原因在于同时发生的各个线程都可以对i读取和写入。
我们这里的if结构会给CPU两个指令, 一个是判断是否有剩余的票(i != 0), 一个是卖票 (i = i -1)。某个线程会先判断是否有票(比如说此时i为1),但两个指令之间存在一个时间窗口,其它线程可能在此时间窗口内执行卖票操作(i = i -1),导致该线程卖票的条件不再成立。但该线程由于已经执行过了判断指令,所以无从知道i发生了变化,所以继续执行卖票指令,以至于卖出不存在的票 (i成为负数)。对于一个真实的售票系统来说,这将成为一个严重的错误 (售出了过多的票,火车爆满)。
在并发情况下,指令执行的先后顺序由内核决定。同一个线程内部,指令按照先后顺序执行,但不同线程之间的指令很难说清除哪一个会先执行。如果运行的结果依赖于不同线程执行的先后的话,那么就会造成竞争条件(race condition),在这样的状况下,计算机的结果很难预知。我们应该尽量避免竞争条件的形成。最常见的解决竞争条件的方法是将原先分离的两个指令构成不可分隔的一个原子操作(atomic operation),而其它任务不能插入到原子操作中。
多线程同步方法
对于多线程程序来说,同步(synchronization)是指在一定的时间内只允许某一个线程访问某个资源。而在此时间内,不允许其它的线程访问该资源。我们可以通过互斥锁(mutex),条件变量(condition variable)和读写锁(reader-writer lock)来同步资源。
1) 互斥锁
互斥锁是一个特殊的变量,它有锁上(lock)和打开(unlock)两个状态。互斥锁一般被设置成全局变量。打开的互斥锁可以由某个线程获得。一旦获得,这个互斥锁会锁上,此后只有该线程有权打开。其它想要获得互斥锁的线程,会等待直到互斥锁再次打开的时候。我们可以将互斥锁想像成为一个只能容纳一个人的洗手间,当某个人进入洗手间的时候,可以从里面将洗手间锁上。其它人只能在互斥锁外面等待那个人出来,才能进去。在外面等候的人并没有排队,谁先看到洗手间空了,就可以首先冲进去。
上面的问题很容易使用互斥锁的问题解决,每个线程的程序可以改为:
/*mu is a global mutex*/ while (1) { /*infinite loop*/ mutex_lock(mu); /*aquire mutex and lock it, if cannot, wait until mutex is unblocked*/ if (i != 0) i = i - 1; else { printf("no more tickets"); exit(); } mutex_unlock(mu); /*release mutex, make it unblocked*/ }第一个执行mutex_lock()的线程会先获得mu。其它想要获得mu的线程必须等待,直到第一个线程执行到mutex_unlock()释放mu,才可以获得mu,并继续执行线程。所以线程在mutex_lock()和mutex_unlock()之间的操作时,不会被其它线程影响,就构成了一个原子操作。
需要注意的时候,如果存在某个线程依然使用原先的程序 (即不尝试获得mu,而直接修改i),互斥锁不能阻止该程序修改i,互斥锁就失去了保护资源的意义。所以,互斥锁机制需要程序员自己来写出完善的程序来实现互斥锁的功能。我们下面讲的其它机制也是如此。
2) 条件变量
条件变量是另一种常用的变量。它也常常被保存为全局变量,并和互斥锁合作。假设这样一个状况: 有100个工人,每人负责装修一个房间。当有10个房间装修完成的时候,老板就通知相应的十个工人一起去喝啤酒。
我们如何实现呢?老板让工人在装修好房间之后,去检查已经装修好的房间数。但多线程条件下,会有竞争条件的危险。也就是说,其他工人有可能会在该工人装修好房子和检查之间完成工作。采用下面方式解决:
/*mu: global mutex, cond: global codition variable, num: global int*/ mutex_lock(mu) num = num + 1; /*worker build the room*/ if (num <= 10) { /*worker is within the first 10 to finish*/ cond_wait(mu, cond); /*wait*/ printf("drink beer"); } else if (num = 11) { /*workder is the 11th to finish*/ cond_broadcast(mu, cond); /*inform the other 9 to wake up*/ } 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的环境。
(就像做饭一样,不断切换去处理不同的菜。)
(4)、互斥量的应用
使用互斥量并不困难,核心步骤如下:
1-CreateMutex创建一个互斥量:
HANDLE CreateMutex(
LPSECURITY_ATTRIBUTES lpMutexAttributes,
BOOL bInitialOwner,
LPCTSTR lpName
);
第一个参数同样是安全结构,默认是NULL不能继承句柄;第二个参数为FALSE时创建Mutex时 不指定所有权,若为TRUE则指定为当前的创建线程ID为所有者,其他线程访问需要先 ReleaseMutex;第三个参数用于设置Mutex名,后续我们会说明,为NULL时表示是匿名互斥量。
2-WaitForSingleObject():请求一个互斥量的访问权;
3-ReleaseMutex():释放一个互斥量的访问权;
#include <iostream>
#include <cstdlib>
#include <windows.h>
using namespace std;
DWORD WINAPI Fun1Proc(LPVOID lpParameter);
DWORD WINAPI Fun2Proc(LPVOID lpParameter);
int tickets = 100;
HANDLE hMutex;
int main()
{
hMutex = CreateMutex(NULL, FALSE, NULL);
HANDLE hThread_1 = CreateThread(NULL, 0, Fun1Proc, NULL, 0, NULL);
HANDLE hThread_2 = CreateThread(NULL, 0, Fun2Proc, NULL, 0, NULL);
CloseHandle(hThread_1);
CloseHandle(hThread_2);
system("pause");
return 0;
}
DWORD WINAPI Fun1Proc(LPVOID lpParameter)
{
while (true)
{
WaitForSingleObject(hMutex, INFINITE);
if (tickets > 0)
{
Sleep(10);
cout << "Thread 1 sell ticket : "<<tickets--<<endl;
}
else
break;
ReleaseMutex(hMutex);
}
return 0;
}
DWORD WINAPI Fun2Proc(LPVOID lpParameter)
{
while (true)
{
WaitForSingleObject(hMutex, INFINITE);
if (tickets > 0)
{
Sleep(10);
cout << "Thread 2 sell ticket : "<<tickets--<<endl;
}
else
break;
ReleaseMutex(hMutex);
}
return 0;
}(5) 互斥变量
互斥变量,让每个线程都按顺序地访问变量。这样就需要使用EnterCriticalSection和LeaveCriticalSection函数,是多线程中用来确保同一时刻只有一个线程操作被保护的数据的操作函数,
CRITICAL_SECTION _
cs
;
InitializeCriticalSection(&cs);
//初始化临界区
EnterCriticalSection(&cs);
//进入临界区
//操作数据
MyMoney*=10;
//所有访问MyMoney变量的程序都需要这样写Enter.. Leave...
LeaveCriticalSection(&cs);
//离开临界区
DeleteCriticalSection(&cs);
//删除临界区
实际上是通过限制有且只有一个函数进入CriticalSection变量来实现代码段同步的。简单地说,对于同一个 CRITICAL_SECTION,当一个线程执行了EnterCriticalSection而没有执行LeaveCriticalSection的时 候,其它任何一个线程都无法完全执行EnterCriticalSection而不得不处于等待状态。再次强调一次,没有任何资源被“锁定”,CRITICAL_SECTION这个东东不是针对于资源的,而是针对于不同线程间的代码段的!我们能够用它来进 行所谓资源的“锁定”,其实是因为我们在任何访问共享资源的地方都加入了EnterCriticalSection和 LeaveCriticalSection语句,使得同一时间只能够有一个线程的代码段访问到该共享资源而已(其它想访问该资源的代码段不得不等待)。
线程同步
最新推荐文章于 2024-03-11 16:32:38 发布
线程同步