内容
简介: 随着开发人员将一些普及的 Windows® 应用程序迁移到 Linux™ 平台,企业中正在进行的向开放源码迁移的浪潮有可能引发极大的移植问题。这个由三部分组成的系列文章提供了一个映射指南,并附有一些例子,以简化从 Windows 到 Linux 的转移。本文是系列文章的第 2 部分,将介绍两种同步对象类型:信号量和事件。
当前,很多全球商务和服务都正在走向开源 —— 业界的所有主要参与者都在争取实现此目标。这一趋势催生了一个重要的迁移模式:为不同平台(Windows、OS2、Solaris 等)维持的许多现有产品都将被移植到开放源码的 Linux 平台。
很多应用程序在设计时并未考虑到需要将它们移植到 Linux。这有可能使移植成为一件痛苦的事情,但并非绝对如此。本系列文章的目的是,帮助您将涉及到 IPC 和线程原语的复杂应用程序从 Windows 迁移到 Linux。我们与您分享迁移这些关键应用程序的经验,其中包括要求线程同步的多线程应用程序以及要求进程间同步的多进程应用程序。
简言之,可以将此系列文章看作是一个映射文档 —— 它提供了与线程、进程和进程间通信元素(互斥、信号量等等)相关的各种 Windows 调用到 Linux 调用的映射。我们将那些映射分为三部分:
- 在 第 1 部分 中,我们已经对进程和线程进行了讨论。
- 本文将介绍信号量和事件。
- 第 3 部分将介绍互斥、临界区和等待函数。
在本文中,我们将从同步技术入手,继续从 Windows 到 Linux 的映射指导。
在 Windows 上,同步是使用等待函数中的同步对象来实现的。同步对象可以有两种状态:有信号(signaled)状态和无信号(non-signaled)状态。当在一个等待函数中使用同步对象时,等待函数就会阻塞调用线程,直到同步对象的状态被设置为有信号为止。
下面是在 Windows 上可以使用的一些同步对象:
- 事件(Event)
- 信号量(Semaphore)
- 互斥(Mutexe)
- 临界区(Critical section)
在 Linux 中,可以使用不同的同步原语。Windows 与 Linux 的不同之处在于每个原语都有自己的等待函数(所谓等待函数就是用来修改同步原语状态的函数);在 Windows 中,有一些通用的等待函数来实现相同的目的。以下是 Linux 上可以使用的一些同步原语:
- 信号量(Semaphore)
- 条件变量(Conditional variable)
- 互斥(Mutexe)
通过使用上面列出的这些原语,各种库都可以用于 Linux 之上,以提供同步机制。
Windows | Linux —— 线程 | Linux —— 进程 |
互斥 | 互斥 - pthread 库 | System V 信号量 |
临界区 | 互斥 - pthread 库 | 不适用,因为临界区只用于同一进程的不同线程之间 |
信号量 | 具有互斥的条件变量 - pthreads POSIX 信号量 | System V 信号量 |
事件 | 具有互斥的条件变量 - pthreads | System V 信号量 |
Windows 信号量是一些计数器变量,允许有限个线程/进程访问共享资源。Linux POSIX 信号量也是一些计数器变量,可以用来在 Linux 上实现 Windows 上的信号量功能。
在对进程进行映射时,我们需要考虑以下问题:
- 信号量的类型: Windows 提供了有名(named)信号量和无名(unnamed)信号量。有名信号量可以在进程之间进行同步。在 Linux 上,在相同进程的不同线程之间,则只使用 POSIX 信号量。在进程之间,可以使用 System V 信号量。
- 等待函数中的超时: 当在一个等待函数中使用时,可以为 Windows 信号量对象指定超时值。在 Linux 中,并没有提供这种功能,只能通过应用程序逻辑处理超时的问题。
Windows | Linux 线程 | Linux 进程 | 类别 |
CreateSemaphore | sem_init | semget semctl | 与上下文相关 |
OpenSemaphore | 不适用 | semget | 与上下文相关 |
WaitForSingleObject | sem_wait sem_trywait | semop | 与上下文相关 |
ReleaseSemaphore | sem_post | semop | 与上下文相关 |
CloseHandle | sem_destroy | semctl | 与上下文相关 |
在 Windows 中,可以使用 CreateSemaphore()
创建或打开一个有名或无名的信号量。
HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, LONG lInitialCount, LONG lMaximumCount, LPCTSTR lpName ); |
在这段代码中:
-
lpSemaphoreAttributes
是一个指向安全性属性的指针。如果这个指针为空,那么这个信号量就不能被继承。 -
lInitialCount
是该信号量的初始值。 -
lMaximumCount
是该信号量的最大值,该值必须大于 0。 -
lpName
是信号量的名称。如果该值为 NULL,那么这个信号量就只能在相同进程的不同线程之间共享。否则,就可以在不同的进程之间进行共享。
这个函数创建信号量,并返回这个信号量的句柄。它还将初始值设置为调用中指定的值。这样就可以允许有限个线程来访问某个共享资源。
在 Linux 中,可以使用 sem_init()
来创建一个无名的 POSIX 信号量,这个调用可以在相同进程的线程之间使用。它还会对信号量计数器进行初始化:int sem_init(sem_t *sem, int pshared, unsigned int value)
。在这段代码中:
-
value
(信号量计数器)是这个信号量的初始值。 -
pshared
可以忽略,因为在目前的实现中,POSIX 信号量还不能在进程之间进行共享。
这里要注意的是,最大值基于 demaphore.h 头文件中定义的 SEM_VALUE_MAX。
在 Linux 中,semget()
用于创建 System V 信号量,它可以在不同集成的线程之间使用。可以用它来实现与 Windows 中有名信号量相同的功能。这个函数返回一个信号量集标识符,它与一个参数的键值关联在一起。当创建一个新信号量集时,对于与 semid_ds
数据结构关联在一起的信号量,semget()
要负责将它们进行初始化,方法如下:
-
sem_perm.cuid
和sem_perm.uid
被设置为调用进程的有效用户 ID。 -
sem_perm.cgid
和sem_perm.gid
被设置为调用进程的有效组 ID。 -
sem_perm.mode
的低 9 位被设置为semflg
的低 9 位。 -
sem_nsems
被设置为nsems
的值。 -
sem_otime
被设置为 0。 -
sem_ctime
被设置为当前时间。
用来创建 System V 信号量使用的代码是:int semget(key_t key, int nsems, int semflg)
。下面是对这段代码的一些解释:
-
key
是一个惟一的标识符,不同的进程使用它来标识这个信号量集。我们可以使用ftok()
生成一个惟一的键值。IPC_PRIVATE
是一个特殊的key_t
值;当使用IPC_PRIVATE
作为key
时,这个系统调用就会只使用semflg
的低 9 位,但却忽略其他内容,从而新创建一个信号量集(在成功时)。 -
nsems
是这个信号量集中信号量的数量。 -
semflg
是这个新信号量集的权限。要新创建一个信号量集,您可以将使用IPC_CREAT
来设置位操作或访问权限。如果具有该 key 值的信号量集已经存在,那么IPC_CREAT
/IPC_EXCL
标记就会失败。
注意,在 System V 信号量中,key
被用来惟一标识信号量;在 Windows 中,信号量是使用一个名称来标识的。
为了对信号量集数据结构进行初始化,可以使用 IPC_SET
命令来调用 semctl()
系统调用。将 arg.buf 所指向的 semid_ds 数据结构的某些成员的值写入信号量集数据结构中,同时更新这个结构的 sem_ctime member 的值。用户提供的这个 arg.buf 所指向的 semid_ds 结构如下所示:
-
sem_perm.uid
-
sem_perm.gid
-
sem_perm.mode
(只有最低 9 位有效)
调用进程的有效用户 ID 应该是超级用户,或者至少应该与这个信号量集的创建者或所有者匹配: int semctl(int semid, int semnum, int cmd = IPC_SET, ...)
。在这段代码中:
-
semid
是信号量集的标识符。 -
semnum
是信号量子集偏移量(从 0 到nsems
-1,其中 n 是这个信号量集中子集的个数)。这个命令会被忽略。 -
cmd
是命令;它使用IPC_SET
来设置信号量的值。 -
args
是这个信号量集数据结构中要通过IPC_SET
来更新的值(在这个例子中会有解释)。
最大计数器的值是根据在头文件中定义的 SEMVMX
来决定的。
在 Windows 中,我们使用 OpenSemaphore()
来打开某个指定信号量。只有在两个进程之间共享信号量时,才需要使用信号量。在成功打开信号量之后,这个函数就会返回这个信号量的句柄,这样就可以在后续的调用中使用它了。
HANDLE OpenSemaphore( DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName ) |
在这段代码中:
-
dwDesiredAccess
是针对该信号量对象所请求的访问权。 -
bInheritHandle
是用来控制这个信号量句柄是否可继承的标记。如果该值为 TRUE,那么这个句柄可以被继承。 -
lpName
是这个信号量的名称。
在 Linux 中,可以调用相同的 semget()
来打开某个信号量,不过此时 semflg
的值为 0:int semget(key,nsems,0)
。在这段代码中:
-
key
应该指向想要打开的信号量集的 key 值。 - 为了打开一个已经存在的信号量,可以将
nsems
和标记设置为 0。semflg
值是在返回信号量集标识符之前对访问权限进行验证时设置的。
在 Windows 中,等待函数提供了获取同步对象的机制。可以使用的等待函数有多种类型;在这一节中,我们只考虑WaitForSingleObject()
(其他类型将会分别进行讨论)。这个函数使用一个信号量对象的句柄作为参数,并会一直等待下去,直到其状态变为有信号状态或超时为止。
DWORD WaitForSingleObject( HANDLE hHandle, DWORD dwMilliseconds );
在这段代码中:
-
hHandle
是指向互斥句柄的指针。 -
dwMilliseconds
是超时时间,以毫秒为单位。如果该值是INFINITE
,那么它阻塞调用线程/进程的时间就是不确定的。
在 Linux 中,sem_wait()
用来获取对信号量的访问。这个函数会挂起调用线程,直到这个信号量有一个非空计数为止。然后,它可以原子地减少这个信号量计数器的值:int sem_wait(sem_t * sem)
。
在 POSIX 信号量中并没有超时操作。这可以通过在一个循环中执行一个非阻塞的 sem_trywait()
实现,该函数会对超时值进行计算:int sem_trywait(sem_t * sem)
。
在使用 System V 信号量时,如果通过使用 IPC_SET
命令的 semctl()
调用设置初始的值,那么必须要使用 semop()
来获取信号量。semop()
执行操作集中指定的操作,并阻塞调用线程/进程,直到信号量值为 0 或更大为止:int semop(int semid, struct sembuf *sops, unsigned nsops)
。
函数 semop()
原子地执行在 sops
中所包含的操作 —— 也就是说,只有在这些操作可以同时成功执行时,这些操作才会被同时执行。sops
所指向的数组中的每个 nsops
元素都使用 struct sembuf
指定了一个要对信号量执行的操作,这个结构包括以下成员:
-
unsigned short sem_num;
(信号量个数) -
short sem_op;
(信号量操作) -
short sem_flg;
(操作标记)
要获取信号量,可以通过将 sem_op
设置为 -1 来调用 semop()
;在使用完信号量之后,可以通过将 sem_op
设置为 1 来调用 semop()
释放信号量。通过将 sem_op
设置为 -1 来调用 semop()
,信号量计数器将会减小 1,如果该值小于 0(信号量的值是不能小于 0 的),那么这个信号量就不能再减小,而是会让调用线程/进程阻塞,直到其状态变为有信号状态为止。
sem_flg
中可以识别的标记是 IPC_NOWAIT
和 SEM_UNDO
。如果某一个操作被设置了 SEM_UNDO
标记,那么在进程结束时,该操作将被取消。如果 sem_op
被设置为 0,那么 semop()
就会等待 semval
变成 0。这是一个“等待为 0” 的操作,可以用它来获取信号量。
记住,超时操作在 System V 信号量中并不适用。这可以在一个循环中使用非阻塞的 semop()
(通过将 sem_flg
设置为 IPC_NOWAIT
)实现,这会计算超时的值。
在 Windows 中,ReleaseSemaphore()
用来释放信号量。
BOOL ReleaseSemaphore( HANDLE hSemaphore, LONG lReleaseCount, LPLONG lpPreviousCount ); |
在这段代码中:
-
hSemaphore
是一个指向信号量句柄的指针。 -
lReleaseCount
是信号量计数器,可以通过指定的数量来增加计数。 -
lpPreviousCount
是指向上一个信号量计数器返回时的变量的指针。如果并没有请求上一个信号量计数器的值,那么这个参数可以是 NULL。
这个函数会将信号量计数器的值增加在 lReleaseCount
中指定的值上,然后将这个信号量的状态设置为有信号状态。
在 Linux 中,我们使用 sem_post()
来释放信号量。这会唤醒对这个信号量进行阻塞的所有线程。信号量的计数器同时被增加 1。要为这个信号量的计数器添加指定的值(就像是 Windows 上一样),可以使用一个互斥变量多次调用以下函数:int sem_post(sem_t * sem)
。
对于 System V 信号量来说,只能使用 semop()
来释放信号量:int semop(int semid, struct sembuf *sops, unsigned nsops)
。
函数 semop()
原子地执行 sops
中包含的一组操作(只在所有操作都可以同时成功执行时,才会将所有的操作同时一次执行完)。sops
所指向的数组中的每个 nsops
元素都使用一个 struct sembuf
结构指定了一个要对这个信号量执行的操作,该结构包含以下元素:
-
unsigned short sem_num;
(信号量个数) -
short sem_op;
(信号量操作) -
short sem_flg;
(操作标记)
要释放信号量,可以通过将 sem_op
设置为 1 来调用 semop()
。通过将 semop()
设置为 1 来调用 semop()
,这个信号量的计数器会增加 1,同时用信号通知这个信号量。
在 Windows 中,我们使用 CloseHandle()
来关闭或销毁信号量对象。
BOOL CloseHandle( HANDLE hObject ); |
hObject
是指向这个同步对象句柄的指针。
在 Linux 中,sem_destroy()
负责销毁信号量对象,并释放它所持有的资源: int sem_destroy(sem_t *sem)
。对于 System V 信号量来说,只能使用 semctl()
函数的 IPC_RMID
命令来关闭信号量集:int semctl(int semid, int semnum, int cmd = IPC_RMID, ...)
。
这个命令将立即删除信号量集及其数据结构,并唤醒所有正在等待的进程(如果发生错误,则返回,并将 errno
设置为 EIDRM
)。调用进程的有效用户 ID 必须是超级用户,或者可以与该信号量集的创建者或所有者匹配的用户。参数 semnum
会被忽略。
下面是信号量的几个例子。
清单 1. Windows 无名信号量的代码
HANDLE hSemaphore; LONG lCountMax = 10; LONG lPrevCount; DWORD dwRetCode; // Create a semaphore with initial and max. counts of 10. hSemaphore = CreateSemaphore( NULL, // no security attributes 0, // initial count lCountMax, // maximum count NULL); // unnamed semaphore // Try to enter the semaphore gate. dwRetCode = WaitForSingleObject( hSemaphore, // handle to semaphore 2000L); // zero-second time-out interval switch (dwRetCode) { // The semaphore object was signaled. case WAIT_OBJECT_0: // Semaphore is signaled // go ahead and continue the work goto success: break; case WAIT_TIMEOUT: // Handle the time out case break; } Success: // Job done, release the semaphore ReleaseSemaphore( hSemaphore, // handle to semaphore 1, // increase count by one NULL) // not interested in previous count // Close the semaphore handle CloseHandle(hSemaphore); |
清单 2. Linux 使用 POSIX 信号量的等效代码
// Main thread #define TIMEOUT 200 /* 2 secs */ // Thread 1 sem_t sem ; // Global Variable int retCode ; // Initialize event semaphore retCode = sem_init( sem, // handle to the event semaphore 0, // not shared 0); // initially set to non signaled state while (timeout < TIMEOUT ) { delay.tv_sec = 0; delay.tv_nsec = 1000000; /* 1 milli sec */ // Wait for the event be signaled retCode = sem_trywait( &sem); // event semaphore handle // non blocking call if (!retCode) { /* Event is signaled */ break; } else { /* check whether somebody else has the mutex */ if (retCode == EPERM ) { /* sleep for delay time */ nanosleep(&delay, NULL); timeout++ ; } else{ /* error */ } } } // Completed the job, // now destroy the event semaphore retCode = sem_destroy( &sem); // Event semaphore handle // Thread 2 // Condition met // now signal the event semaphore sem_post( &sem); // Event semaphore Handle |
清单 3. Linux 使用 System V 信号量的等效代码
// Process 1 #define TIMEOUT 200 //Definition of variables key_t key; int semid; int Ret; int timeout = 0; struct sembuf operation[1] ; union semun { int val; struct semid_ds *buf; USHORT *array; } semctl_arg,ignored_argument; key = ftok(); // Generate a unique key, U can also supply a value instead semid = semget(key, // a unique identifier to identify semaphore set 1, // number of semaphore in the semaphore set 0666 | IPC_CREAT // permissions (rwxrwxrwx) on the new // semaphore set and creation flag ); //Set Initial value for the resource semctl_arg.val = 0; //Setting semval to 0 semctl(semid, 0, SETVAL, semctl_arg); //Wait for Zero while(timeout < TIMEOUT) { delay.tv_sec = 0; delay.tv_nsec = 1000000; /* 1 milli sec */ //Call Wait for Zero with IPC_NOWAIT option,so it will be non blocking operation[0].sem_op = -1; // Wait until the semaphore count becomes 0 operation[0].sem_num = 0; operation[0].sem_flg = IPC_NOWAIT; ret = semop(semid, operation,1); if(ret < 0) { /* check whether somebody else has the mutex */ if (retCode == EPERM ) { /* sleep for delay time */ nanosleep(&delay, NULL); timeout++ ; } else { printf("ERROR while wait "); break; } } else { /*semaphore got triggered */ break; } } //Close semaphore iRc = semctl(semid, 1, IPC_RMID , ignored_argument); } // Process 2 key_t key = KEY; // Process 2 should know key value in order to open the // existing semaphore set struct sembuf operation[1] ; //Open semaphore semid = semget(key, 1, 0); operation[0].sem_op = 1; // Release the resource so Wait in process 1 will // be triggered operation[0].sem_num = 0; operation[0].sem_flg = SEM_UNDO; //Release semaphore semop(semid, operation,0); } |
在 Windows 中,事件对象是那些需要使用 SetEvent()
函数显式地将其状态设置为有信号状态的同步对象。事件对象来源有两种类型:
- 在 手工重置事件(manual reset event) 中,对象的状态会一直维持为有信号状态,直到使用
ResetEvent()
函数显式地重新设置它为止。 - 在 自动重置事件(auto reset event) 中,对象的状态会一直维持为有信号状态,直到单个正在等待的线程被释放为止。当正在等待的线程被释放时,其状态就被设置为无信号的状态。
事件对象有两种状态,有信号(signaled)状态 和 无信号(non-signaled)状态。对事件对象调用的等待函数会阻塞调用线程,直到其状态被设置为有信号状态为止。
在进行平台的迁移时,需要考虑以下问题:
- Windows 提供了 有名(named) 和 无名(un-named) 的事件对象。有名事件对象用来在进程之间进行同步,而在 Linux 中, pthreads 和 POSIX 都提供了线程间的同步功能。为了在 Linux 实现与 Windows 中有名事件对象相同的功能,可以使用 System V 信号量或信号。
- Windows 提供了两种类型的事件对象 —— 手工重置对象和自动重置对象。Linux 只提供了自动重置事件的特性。
- 在 Windows 中,事件对象的初始状态被设置为有信号状态。在 Linux 中,pthreads 并没有提供初始状态,而 POSIX 信号量则提供了一个初始状态。
- Windows 事件对象是异步的。在 Linux 中,POSIX 信号量和 System V 信号量也都是异步的,不过 pthreads 条件变量不是异步的。
- 当在一个等待函数中使用事件对象时,可以指定 Windows 的事件对象的超时时间值。在 Linux 中,只有 pthreads 在等待函数中提供了超时的特性。
还有几点非常重要,需要说明一下:
- 尽管 POSIX 信号量是计数器信号量,但是当这个计数器被设置为 1 时,它们可以提供与 Windows 事件对象相似的功能。它们并不能在等待函数中提供超时时间。如果在进行移植时,超时并不是一个影响因素,那么建议您使用 POSIX 信号量。
- 当与互斥一起使用时,pthreads 条件变量可以在线程之间提供基于事件的同步机制,不过这是同步的。根据应用程序的逻辑,这可以将此作为移植过程中在 Linux 上实现这种功能的一个选择。
Windows | Linux 线程 | Linux 进程 | 类别 |
CreateEvent OpenEvent | pthread_cond_init sem_init | semget semctl | 与上下文相关 |
SetEvent | pthread_cond_signal sem_post | semop | 与上下文相关 |
ResetEvent | N/A | N/A | 与上下文相关 |
WaitForSingleObject | pthread_cond_wait pthread_cond_timedwait sem_wait sem_trywait | semop | 与上下文相关 |
CloseHandle | pthread_cond_destroy sem_destroy | semctl | 与上下文相关 |
在 Windows 中,我们使用 CreateEvent()
来创建事件对象。
HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, BOOL bManualReset, BOOL bInitialState, LPCTSTR lpName ) |
在这段代码中:
-
lpEventAttributes
是一个指针,它指向一个决定这个句柄是否能够被继承的属性。如果这个指针为 NULL,那么这个对象就不能被初始化。 -
bManualReset
是一个标记,如果该值为 TRUE,就会创建一个手工重置的事件,应该显式地调用ResetEvent()
,将事件对象的状态设置为无信号状态。 -
bInitialState
是这个事件对象的初始状态。如果该值为 true,那么这个事件对象的初始状态就被设置为有信号状态。 -
lpName
是指向这个事件对象名的指针。对于无名的事件对象来说,该值是 NULL。
这个函数创建一个手工重置或自动重置的事件对象,同时还要设置改对象的初始状态。这个函数返回事件对象的句柄,这样就可以在后续的调用中使用这个事件对象了。
OpenEvent()
用来打开一个现有的有名事件对象。这个函数返回该事件对象的句柄。
HANDLE OpenEvent( DWORD dwDesiredAccess, BOOL bInheritHandle, LPCTSTR lpName ) |
在这段代码中:
-
dwDesiredAccess
是针对这个事件对象所请求的访问权。 -
bInheritHandle
是用来控制这个事件对象句柄是否可继承的标记。如果该值为 TRUE,那么这个句柄就可以被继承;否则就不能被继承。 -
lpName
是一个指向事件对象名的指针。
在 Linux 中,可以调用 sem_init()
来创建一个 POSIX 信号量:int sem_init(sem_t *sem, int pshared, unsigned int value)
(其中 value
(即信号量计数值)被设置为这个信号量的初始状态)。
Linux pthreads 使用 pthread_cond_init()
来创建一个条件变量:int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr)
。
可以使用 PTHREAD_COND_INITIALIZER
常量静态地对 pthread_cond_t
类型的条件变量进行初始化,也可以使用pthread_condattr_init()
对其进行初始化,这个函数会对与这个条件变量关联在一起的属性进行初始化。可以调用pthread_condattr_destroy()
用来销毁属性:
int pthread_condattr_init(pthread_condattr_t *attr) int pthread_condattr_destroy(pthread_condattr_t *attr) |
在 Windows 中,等待函数提供了获取同步对象的机制。我们可以使用不同类型的等待函数(此处我们只考虑WaitForSingleObject()
)。这个函数会使用一个互斥对象的句柄,并一直等待,直到它变为有信号状态或超时为止。
DWORD WaitForSingleObject( HANDLE hHandle, DWORD dwMilliseconds ); |
在这段代码中:
-
hHandle
是指向互斥句柄的指针。 -
dwMilliseconds
是超时时间的值,单位是毫秒。如果该值为INFINITE
,那么它阻塞调用线程/进程的时间就是不确定的。
Linux POSIX 信号量使用 sem_wait()
来挂起调用线程,直到信号量的计数器变成非零的值为止。然后它会自动减小信号量计数器的值:int sem_wait(sem_t * sem)
。
在 POSIX 信号量中并没有提供超时操作。这可以通过在一个循环中执行非阻塞的 sem_trywait()
来实现,该函数会对超时时间进行计数:int sem_trywait(sem_t * sem)
.
Linux pthreads 使用 pthread_cond_wait()
来阻塞调用线程,其时间是不确定的:int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex)
。在另外一方面,如果调用线程需要被阻塞一段确定的时间,那么就可以使用 pthread_cond_timedwait()
来阻塞这个线程。如果在这段指定的时间内条件变量并没有出现,那么 pthread_cond_timedwait()
就会返回一个错误:int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,const struct timespec *abstime)
。在这里,abstime
参数指定了一个绝对时间(具体来说,就是从 1970 年 1 月 1 日 0 时 0 分 0 秒到现在所经过的时间。)
函数 SetEvent()
用来将事件对象的状态设置为有信号状态。对一个已经设置为有信号状态的事件对象再次执行该函数是无效的。
BOOL SetEvent( HANDLE hEvent ) |
Linux POSIX 信号量使用 sem_post()
来发出一个事件信号量。这会唤醒在该信号量上阻塞的所有线程:int sem_post(sem_t * sem)
。
调用 pthread_cond_signal()
被用在 LinuxThreads 中,以唤醒在某个条件变量上等待的一个线程,而 pthread_cond_broadcast()
用来唤醒在某个条件变量上等待的所有线程。
int pthread_cond_signal(pthread_cond_t *cond) int pthread_cond_broadcast(pthread_cond_t *cond) |
注意,条件函数并不是异步信号安全的,因此不能在信号处理函数中调用。具体地说,在信号处理函数中调用pthread_cond_signal()
或 pthread_cond_broadcast()
可能会导致调用线程的死锁。
在 Windows 中,ResetEvent()
用来将事件对象的状态重新设置为无信号状态。
BOOL ResetEvent( HANDLE hEvent ); |
在 Linux 中,条件变量和 POSIX 信号量都是自动重置类型的。
在 Windows 中,CloseHandle()
用来关闭或销毁事件对象。
BOOL CloseHandle( HANDLE hObject ); |
在这段代码中,hObject
是指向同步对象句柄的指针。
在 Linux 中, sem_destroy()/ pthread_cond_destroy()
用来销毁信号量对象或条件变量,并释放它们所持有的资源:
int sem_destroy(sem_t *sem) int pthread_cond_destroy(pthread_cond_t *cond) |
在 Linux 中,进程之间有名事件对象所实现的功能可以使用 System V 信号量实现。System V 信号量是计数器变量,因此可以实现 Windows 中事件对象的功能,信号量的计数器的初始值可以使用 semctl()
设置为 0。
要将某个事件的状态修改为有信号状态,可以使用 semop()
,并将 sem_op
的值设置为 1。要等待某个事件,则可以使用 semop()
函数,并将 sem_op
的值设置为 -1,这样就可以阻塞调用进程,直到它变为有信号状态为止。
可以通过使用 semctl()
将信号量计数器的初始值设置为 0 来获得信号量。在使用完共享资源之后,可以使用 semop()
将信号量计数设置为 1。关于每个 System V 信号量的原型,请参阅本文中有关信号量一节的内容。
下面几个例子可以帮助您理解我们在这一节中所讨论的内容。
清单 4. Windows 无名事件对象的代码
// Main thread HANDLE hEvent; // Global Variable // Thread 1 DWORD dwRetCode; // Create Event hEvent = CreateEvent( NULL, // no security attributes FALSE, // Auto reset event FALSE, // initially set to non signaled state NULL); // un named event // Wait for the event be signaled dwRetCode = WaitForSingleObject( hEvent, // Mutex handle INFINITE); // Infinite wait switch(dwRetCode) { case WAIT_OBJECT_O : // Event is signaled // go ahead and proceed the work default : // Probe for error } // Completed the job, // now close the event handle CloseHandle(hEvent); // Thread 2 // Condition met for the event hEvent // now set the event SetEvent( hEvent); // Event Handle |
清单 5. Linux 使用 POSIX 信号量的等效代码
// Main thread sem_t sem ; // Global Variable // Thread 1 int retCode ; // Initialize event semaphore retCode = sem_init( sem, // handle to the event semaphore 0, // not shared 0); // initially set to non signaled state // Wait for the event be signaled retCode = sem_wait( &sem); // event semaphore handle // Indefinite wait // Event Signaled // a head and proceed the work // Completed the job, // now destroy the event semaphore retCode = sem_destroy( &sem); // Event semaphore handle // Thread 2 // Condition met // now signal the event semaphore sem_post( &sem); // Event semaphore Handle |
清单 6. Linux 中使用条件变量的等效代码
// Main thread pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; pthread_cond_t condvar = PTHREAD_COND_INITIALIZER; // Thread 1 ... pthread_mutex_lock(&mutex); // signal one thread to wake up pthread_cond_signal(&condvar); pthread_mutex_unlock(&mutex); // this signal is lost as no one is waiting // Thread 1 now tries to take the mutex lock // to send the signal but gets blocked ... pthread_mutex_lock(&mutex); // Thread 1 now gets the lock and can // signal thread 2 to wake up pthread_cond_signal(&condvar); pthread_mutex_unlock(&mutex); // Thread 2 pthread_mutex_lock(&mutex); pthread_cond_wait(&condvar, &mutex); pthread_mutex_unlock(&mutex); // Thread 2 blocks indefinitely // One way of avoiding losing the signal is as follows // In Thread 2 - Lock the mutex early to avoid losing signal pthread_mutex_lock (&mutex); // Do work ....... // This work may lead other threads to send signal to thread 2 // Thread 2 waits for indefinitely for the signal to be posted pthread_cond_wait (&condvar, &Mutex ); // Thread 2 unblocks upon receipt of signal pthread_mutex_unlock (&mutex); |
清单 7. Windows 中使用有名事件的例子
// Process 1 DWORD dwRetCode; HANDLE hEvent; // Local variable // Create Event hEvent = CreateEvent( NULL, // no security attributes FALSE, // Auto reset event FALSE, // initially set to non signaled state "myEvent"); // un named event // Wait for the event be signaled dwRetCode = WaitForSingleObject( hEvent, // Mutex handle INFINITE); // Infinite wait switch(dwRetCode) { case WAIT_OBJECT_O : // Event is signaled // go ahead and proceed the work default : // Probe for error } // Completed the job, // now close the event handle CloseHandle(hEvent); // Process 2 HANDLE hEvent; // Local variable // Open the Event hEvent = CreateEvent( NULL, // no security attributes FALSE, // do not inherit handle "myEvent"); // un named event // Condition met for the event hEvent // now set the event SetEvent( hEvent); // Event Handle // completed the job, now close the event handle CloseHandle(hEvent); |
清单 8. Linux 中使用 System V 信号量的等效代码
// Process 1 int main() { //Definition of variables key_t key; int semid; int Ret; int timeout = 0; struct sembuf operation[1] ; union semun { int val; struct semid_ds *buf; USHORT *array; } semctl_arg,ignored_argument; key = ftok(); /Generate a unique key, U can also supply a value instead semid = semget(key, // a unique identifier to identify semaphore set 1, // number of semaphore in the semaphore set 0666 | IPC_CREAT // permissions (rwxrwxrwx) on the new // semaphore set and creation flag ); if(semid < 0) { printf("Create semaphore set failed "); Exit(1); } //Set Initial value for the resource - initially not owned semctl_arg.val = 0; //Setting semval to 0 semctl(semid, 0, SETVAL, semctl_arg); // wait on the semaphore // blocked until it is signaled operation[0].sem_op = -1; operation[0].sem_num = 0; operation[0].sem_flg = IPC_WAIT; ret = semop(semid, operation,1); // access the shared resource ... ... //Close semaphore iRc = semctl(semid, 1, IPC_RMID , ignored_argument); } // Process 2 int main() { key_t key = KEY; //Process 2 shd know key value in order to open the // existing semaphore set struct sembuf operation[1] ; //Open semaphore semid = semget(key, 1, 0); // signal the semaphore by incrementing the semaphore count operation[0].sem_op = 1; operation[0].sem_num = 0; operation[0].sem_flg = SEM_UNDO; semop(semid, operation,0); } |
本文是这一系列的第 2 部分,这篇文章从信号量和事件入手,介绍了有关同步对象和原语的内容。第 3 部分的内容将涉及互斥、临界区和等待函数。
From: http://www.ibm.com/developerworks/cn/linux/l-ipc2lin2.html#SYNC