C++中Semaphore内核对象用法实例

 更新时间:2023年01月28日 18:32:27   投稿:shichen2014  
这篇文章主要介绍了C++中Semaphore内核对象用法实例,有助于深入了解信号量(Semaphore)的基本用法,需要的朋友可以参考下

信号量 (semaphore) 是一种轻量的同步原件,用于制约对共享资源的并发访问。在可以使用两者时,信号量能比条件变量更有效率。1

下面是在 www.open-std.org 对 C++20 semaphore 的一点介绍内容。(semaphores、latch、barrier)

Semaphores are lightweight synchronization primitives used to constrain concurrent access to a shared resource. They are widely used to implement other synchronization primitives and, whenever both are applicable, can be more efficient than condition variables.
A counting semaphore is a semaphore object that models a non-negative resource count. A binary semaphore is a semaphore object that has only two states, also known as available and unavailable. [ Note: A binary semaphore should be more efficient than a counting semaphore with a unit magnitude count. – end note ]

信号量是用于限制对共享资源的并发访问的轻量级同步原语。它们被广泛应用于实现其他同步原语,并且,只要两者都适用,就可以比条件变量更有效。

计数信号量是模拟非负资源计数的信号量对象。二进制信号量是一个只有两种状态的信号量对象,也称为可用和不可用。[注意:二进制信号量应该比使用单位数量级计数的计数信号量更有效。–尾注]

cppreference.com 中的标准库头文件 <semaphore> 中也给出了详细定义。3。

C++20 中提供了两个信号量类。(其实binary_semaphore仅仅是counting_semaphore的一个特例。)

信号量类名含义
counting_semaphore实现非负资源计数的信号量
binary_semaphore仅拥有二个状态的信号量

cppreference.com中给出的关于semaphore的定义如下:

// 概要
namespace std {
  template<ptrdiff_t LeastMaxValue = /* 实现定义 */>
    class counting_semaphore;
 
  using binary_semaphore = counting_semaphore<1>;
}

// 类模板 std::counting_semaphore
namespace std {
  template<ptrdiff_t LeastMaxValue = /* 实现定义 */>
  class counting_semaphore {
  public:
    static constexpr ptrdiff_t max() noexcept;
 
    constexpr explicit counting_semaphore(ptrdiff_t desired);
    ~counting_semaphore();
 
    counting_semaphore(const counting_semaphore&) = delete;
    counting_semaphore& operator=(const counting_semaphore&) = delete;
 
    void release(ptrdiff_t update = 1);
    void acquire();
    bool try_acquire() noexcept;
    template<class Rep, class Period>
      bool try_acquire_for(const chrono::duration<Rep, Period>& rel_time);
    template<class Clock, class Duration>
      bool try_acquire_until(const chrono::time_point<Clock, Duration>& abs_time);
 
  private:
    ptrdiff_t counter;          // 仅用于阐释
  };
}

std::counting_semaphore4

counting_semaphore 是一个轻量同步元件,能控制对共享资源的访问。不同于 std::mutex 、 counting_semaphore 允许同一资源有多于一个同时访问,至少允许 LeastMaxValue 个同时的访问者若LeastMaxValue 为负则程序为谬构。
binary_semaphore 是 std::counting_semaphore 的特化的别名,其 LeastMaxValue 为 1 。实现可能将 binary_semaphore 实现得比 std::counting_semaphore 的默认实现更高效。
counting_semaphore 含有由构造函数初始化的内部计数器。由调用 acquire() 与相关方法减少此计数器,而它通过调用 release() 增加。计数器为零时, acquire() 阻塞该计数器直至它增加,但 try_acquire() 不阻塞; try_acquire_for() 与 try_acquire_until() 阻塞直至计数器增加或到达时限。
类似 std::condition_variable 的 wait() , counting_semaphore 的 try_acquire() 可能虚假地失败。
std::counting_semaphore的主要接口

接口含义
release增加内部计数器并除阻获取者
acquire减少内部计数器或阻塞到直至能如此
try_acquire尝试减少内部计数器而不阻塞
try_acquire_for尝试减少内部计数器,至多阻塞一段时长
try_acquire_until尝试减少内部计数器,阻塞直至一个时间点
max返回内部计数器的最大可能值(静态,常量)

注解

如其名所示, LeastMaxValue 是最小的最大值,而非实际最大值。从而 max() 能产生大于 LeastMaxValue 的值。

不同于 std::mutex , counting_semaphore 不捆绑到执行线程——能在不同于释放信号量的线程获取该信号量。能同时进行 counting_semaphore 上的所有操作而无需联系到任何特定的执行线程,除了不能同时执行,但能在一个不同的线程上执行析构函数。

信号量亦常用于发信/提醒而非互斥,通过初始化该信号量为 ​0​ 从而阻塞尝试 acquire() 的接收者,直至提醒者通过调用 release(n) “发信”。在此方面可把信号量当作 std::condition_variable 的替用品,通常它有更好的性能。

semaphore、mutex、condition_variable的区别

信号量 (semaphore) 是一种轻量的同步原件,用于制约对共享资源的并发访问。在可以使用两者时,信号量能比条件变量更有效率。

互斥(mutex)算法避免多个线程同时访问共享资源。这会避免数据竞争,并提供线程间的同步支持。

条件变量(condition_variable)是允许多个线程相互交流的同步原语。它允许一定量的线程等待(可以定时)另一线程的提醒,然后再继续。条件变量始终关联到一个互斥。

1: semaphore对acquire和release操作没有限制,可以在不同线程操作;可以仅在线程A里面acquire,仅在线程B里面release。
mutex的lock和unlock必须在同一个线程配对使用;也就是说线程A内mutex如果lock了,必须在线程A内unlock,线程B内lock了,也必须在线程B内unlock。
2: semaphore和mutex是可以独立使用的;condition_variable必须和mutex配对使用。
3: semaphore一般用于控制多个并发资源的访问或者控制并行数量;mutex一般是起到同步访问一个资源的作用。同一时刻,mutex保护的资源只能被一个线程访问;semaphore的保护对象上面是可以有多个线程在访问的。mutex是同步,semaphore是并行。
4: 由于condition_variable和mutex结合使用,condition_variable更多是为了通知、顺序之类的控制。
5: C++语言中的mutex、semaphore、condition和系统级的概念不同。都是线程级别的,也就是不能跨进程控制的。要区别于windows api的 mutex、semaphore、event。windows系统上这几个api创建有名对象时,是进程级别的。

C++中Semaphore内核对象的用法

// Semaphore.cpp : 定义控制台应用程序的入口点。 
// 
 
#include "stdafx.h" 
#include <Windows.h> 
#include <process.h>  
 
HANDLE g_hSemaphore; 
DWORD g_nConut1 = 0; 
DWORD g_nConut2 = 0; 
unsigned __stdcall ThreadProc1( void* pArguments ) 
{ 
    ::WaitForSingleObject(g_hSemaphore, INFINITE); 
    for (int i=0;i<10000;i++) 
    { 
        g_nConut1++; 
        g_nConut2++; 
    } 
    ::ReleaseSemaphore(g_hSemaphore, 1, NULL); 
    printf("ThreadProc1\n"); 
    return 0; 
} 
 
unsigned __stdcall ThreadProc2( void* pArguments ) 
{ 
    ::WaitForSingleObject(g_hSemaphore, INFINITE); 
    for (int i=0;i<10000;i++) 
    { 
        g_nConut1++; 
        g_nConut2++; 
    } 
    ::ReleaseSemaphore(g_hSemaphore, 1, NULL); 
    printf("ThreadProc2\n"); 
    return 0; 
} 
 
unsigned __stdcall ThreadProc3( void* pArguments ) 
{ 
    ::WaitForSingleObject(g_hSemaphore, INFINITE); 
    for (int i=0;i<10000;i++) 
    { 
        g_nConut1++; 
        g_nConut2++; 
    } 
    ::ReleaseSemaphore(g_hSemaphore, 1, NULL); 
    printf("ThreadProc3\n"); 
    return 0; 
} 
int _tmain(int argc, _TCHAR* argv[]) 
{ 
    g_hSemaphore = ::CreateSemaphore(NULL, 2, 2, NULL); 
    HANDLE hThread[3]; 
    hThread[0] = (HANDLE)::_beginthreadex(NULL, 0, ThreadProc1, NULL, 0, NULL); 
    hThread[1] = (HANDLE)::_beginthreadex(NULL, 0, ThreadProc2, NULL, 0, NULL); 
    hThread[2] = (HANDLE)::_beginthreadex(NULL, 0, ThreadProc3, NULL, 0, NULL); 
 
    ::WaitForMultipleObjects(2,hThread,TRUE, INFINITE); 
    printf("g_count1=%d\n", g_nConut1); 
    printf("g_count2=%d\n", g_nConut2); 
    printf("main finished.\n"); 
    return 0; 
}

linux信号量semaphore的几种使用方法

以下提到的几种应用方式,下面都有示例代码。

注意:有个点容易遗忘的:当semop的实参sops设置>0的操作时,一般要给这个op动作添加SEM_UNDO标志,详情可参考另一篇博文:linux线程通信之信号量。

应用情景一:用信号量打造一个二值信号量(互斥量),也即:任何时刻只允许一个线程访问共享资源。P操作用于占用资源,V操作代表释放资源。

使用信号量,关键是要知道semop函数的特性:

① semop函数的第二形参sops可以以数组地址的形式输入多个动作(称为动作集),man手册上讲,semop函数会按照sops数组的顺序、原子性的执行动作集中的动作,要么一个都不执行,要么全部执行。手册上说的这句话我觉得是有点问题的,“要么全部执行”这句话实际上有个例外:如果动作集中某个动作设置的条件(如等待0)会使得线程堵塞在本函数中(或者本函数出错返回)的话,那么后面的动作就只能等解除堵塞之后才能被执行(堵塞时),或者得不到执行(semop出错返回时)。
② 如果在某时刻有多个线程都在等待互斥信号量的使用权,那么一旦占用该互斥量的线程把它释放后,这多个等待的线程中,只能有一个线程被解除堵塞

ps:当无法获得信号量资源时,semop到底是堵塞,还是设置错误并返回,取决于第四参数是否或了IPC_NOWAIT标志。

方法1:

步骤:
(1) 把信号量的值初始化为0(创建信号量之后默认值就是0,该步骤不做也行)
(2) P操作,用semop函数设置线程等待信号量的值semval为0,若不为0则堵塞或报错;然后用semop函数把信号量的值+1(也即:semval为0时可以立即通过,否则就要等待)。本步骤中的两个动作,必须通过semop的实参一次把两个动作都输入进去,而不能分别调用两次semop来实现。
(3) V操作,用semop函数设置信号值-1,注意:只有信号量值≥abs(-1)时,才能够立即减1后立即返回,否则本线程又得等待,直到信号量值≥abs(-1)。当然,因为P操作已经把信号量值+1了,所以这里信号量值肯定是≥abs(-1)。

分析:①整个程序中首次执行P操作的时候,情况是怎样的?看步骤(2),设置本线程为等待semval变为0,因为semval被初始化为0了,所以semop会立即返回或者继续执行形参指定的下一个动作:把semval+1。+1这种动作永不堵塞,于是本线程将继续向下执行开始访问共享资源。
    ②当某个线程A执行P之后,尚未V之前,又有另个线程B开始执行P了,情况是怎样的?还是看步骤(2),设置本线程B为等待semval变为0,因为线程A已经把semval设为1了,于是线程B被堵塞或报错。
    ③ 为什么步骤(2)中不允许把等待0和+1这两个动作分别用两次semop来实现?试想这样一种情形:semval初始化为0,当进程A等待0时,发现确实是0,于是继续向下执行semop的+1(进而开始访问共享资源),这时发生了进程/线程调度,切入了线程B,线程B恰好也要执行P操作,等待semval变为0,也发现确实是0,于是继续向下执行semop的+1 (进而开始访问共享资源),显然,没有达到预想的互斥的效果。semop函数提供了一种机制,把多个动作(称为动作集)通过形参一次性传入进去之后,操作系统可以保证,这些动作要么一个也不执行,要么全部被执行(除非:如果某个动作设置的条件会堵塞线程时,等堵塞解除后,后面的动作才会执行),这就杜绝了这一问题。

方法2:

步骤:
① 用semctl或者semop把信号量值初始化为1
② P操作,用semop函数设置线程等待,直到semval≥abs(-1)才解除等待(也即,semval≥1时可以立即通过,否则就要等待);
③ V操作,用semop函数设置semval+1

分析:①整个程序中首次执行P操作的时候,情况是怎样的?看步骤(2),因为semval被初始化为1了,故本次P操作并不堵塞或出错,而是把semval-1后直接返回,P操作完成后semval就变成0了;

 ②当某个线程A执行P之后,尚未V之前,又有另个线程B开始执行P了,情况是怎样的?还是看步骤(2),设置本线程B为等待等待semval≥abs(-1),因为线程A的P已经把semval设为0了,于是线程B被堵塞或报错;

应用情景二:例如,某个资源最多只允许5个线程同时访问

这种应用场景的一个更贴近生活的例子:某开水房的水管上(这跟水管就是个共享资源),只有5个水龙头,那么这跟水管最多只允许5个人同时打水。每来一个人打水,信号量减1,每走一个人,信号量+1,也即:只要空闲水龙头的数目(信号量)≥1,就可以放人进来打水,否则,都得排队等。

这种应用情景的处理方法,和上面提到的方法(2)是一样的,唯一的区别就是初始化时,要信号量的值初始化为n:

方法3:

步骤:
① 用semctl或者semop把信号量值初始化为5;
② P操作,用semop函数设置线程等待,直到semval≥abs(-1)才解除等待(也即,semval≥1时可以立即通过,否则就要等待);

③ V操作,用semop函数设置semval+1;

分析: 程序把信号量值初始化为5以后,同时有13个线程发起了P操作,请求访问共享资源,这时情况是怎样的?名义上是同时,实际上在该信号量的信息维护链表中,发起P操作的线程仍然是有先后的,第一名开始执行P,信号量发现自己的值是5,可以满足第一名提出的semval≥abs(-1)无需等待条件,于是第一名无需等待,P操作直接返回(<的操作返回时会把信号量值减掉),从而可以访问共享资源了;第二名线程开始执行P操作,信号量发现自己是4,也可以满足不等待的条件semval≥abs(-1)······,也即,前5名线程执行P操作,完全不用等待,都可以直接获得共享资源,而其余的13-7=7个线程执行P操作会被阻塞。前5名当中,一旦有其中一个用完了资源并释放了资源(执行V操作)之后,那么第6名线程就会解除等待,从而获得共享资源的访问权。

希望本文所述对大家的C++程序设计有所帮助。

相关文章

  • C语言详解如何实现带头双向循环链表

    C语言详解如何实现带头双向循环链表

    带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单
    2022-04-04
  • Opencv EigenFace人脸识别算法详解

    Opencv EigenFace人脸识别算法详解

    这篇文章主要为大家详细介绍了Opencv EigenFace人脸识别算法的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-05-05
  • C语言实现动态顺序表详解

    C语言实现动态顺序表详解

    这篇文章主要介绍了C语言实现动态顺序表的实现代码的相关资料,动态顺序表在内存中开辟一块空间,可以随我们数据数量的增多来扩容,需要的朋友可以参考下
    2021-08-08
  • C语言中递归和排列组合详解

    C语言中递归和排列组合详解

    大家好,本篇文章主要讲的是C语言中递归和排列组合详解,感兴趣的同学赶快来看一看吧,对你有帮助的话记得收藏一下,方便下次浏览
    2022-01-01
  • C++制作DLL文件的方法详解

    C++制作DLL文件的方法详解

    本文主要介绍如何制作DLL,将代码类中的方法以接口的形式暴露出来给exe程序使用。会涉及类厂创建方法实例、声明DLL接口、.def文件的使用等,感兴趣的可以了解一下
    2023-04-04
  • C++中回调函数及函数指针的实例详解

    C++中回调函数及函数指针的实例详解

    这篇文章主要介绍了C++中回调函数及函数指针的实例详解的相关资料,希望通过本文能帮助到大家,让大家理解掌握这部分内容,需要的朋友可以参考下
    2017-10-10
  • 老生常谈C++getline使用方法

    老生常谈C++getline使用方法

    下面小编就为大家带来一篇老生常谈C++getline使用方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-06-06
  • 详解C++11原子类型与原子操作

    详解C++11原子类型与原子操作

    这篇文章主要介绍了C++11原子类型与原子操作的相关资料,帮助大家更好的理解和学习c++,感兴趣的朋友可以了解下
    2020-08-08
  • C++ CTreeview的checkbox使用方法

    C++ CTreeview的checkbox使用方法

    这篇文章主要介绍了C++ CTreeview的checkbox使用方法的相关资料,需要的朋友可以参考下
    2015-06-06
  • c语言实现整蛊朋友小程序(附源码)

    c语言实现整蛊朋友小程序(附源码)

    这篇文章主要给大家介绍了关于c语言实现整蛊朋友小程序的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-02-02

最新评论