【问题标题】:Implementing semaphore by using mutex operations and primitives使用互斥操作和原语实现信号量
【发布时间】:2013-12-30 08:23:16
【问题描述】:

前段时间面试,被要求实施 仅使用互斥操作和原语的信号量 (他允许 int 被认为是原子的)。我在下面提供了解决方案。 他不喜欢忙碌/等待部分——while (count >= size) {}——并要求通过使用更原始的方式来实现锁定 类型和互斥锁。我没有设法提供改进的解决方案。 有什么想法可以做到吗?

struct Semaphore {
int size;
atomic<int> count;
mutex updateMutex;

Semaphore(int n) : size(n) { count.store(0); }

void aquire() {
    while (1) {
        while (count >= size) {}
        updateMutex.lock();
        if (count >= size) {
            updateMutex.unlock();
            continue;
        }
        ++count;
        updateMutex.unlock();
        break;
    }
}

void release() {
    updateMutex.lock();
    if (count > 0) {
        --count;
    } // else log err
    updateMutex.unlock();
}
};

【问题讨论】:

标签: c++ c++11 mutex semaphore


【解决方案1】:

我敢打赌,如果没有繁忙循环仅使用互斥锁,这是不可能实现的。

如果不是忙循环,你必须在某个地方阻塞。您拥有的唯一阻塞原语是 互斥体。因此,当信号量计数器为零时,您必须阻止某些互斥锁。您只能被该互斥体的单一所有者唤醒。但是,您应该任意线程向信号量返回计数器时唤醒。

现在,如果允许使用条件变量,情况就完全不同了。

【讨论】:

  • "您只能被该互斥体的单一所有者唤醒。"为什么?让非拥有线程锁定和解锁的互斥锁的全部意义不在于?
  • @SebastianHojas 互斥锁只能在线程被锁定时阻塞。锁定的互斥锁只能由拥有它的线程解锁。因此,在互斥锁上阻塞的线程只能被某个特定线程唤醒。如果您遇到线程被阻塞并且两个线程中的任何一个可能执行需要解除阻塞的操作的情况,则不能使用互斥锁。
  • 没错。这是一项不可能完成的任务,大概正确的答案是解释为什么它不能完成。您可以建议使用各种特定于平台的功能(如 x86 和相关 CPU 上的“rep nop”或“pause”)来改善忙碌等待。您可以建议使用条件变量。
  • @DavidSchwartz 我问这个问题已经有一段时间了,但我非常感谢现在在这里阅读答案。谢谢!
  • C++ 也是如此。但是,在 Go 等其他语言中,您确实可以从另一个威胁(goroutine)中解锁互斥锁,并且您可以使用互斥锁轻松实现二进制信号量。见这里:play.golang.org/p/jkBWIoLqnTY
【解决方案2】:

正如@chill 指出的那样,我在这里写下的解决方案将不起作用,因为锁具有唯一的所有权。我猜你最终会恢复到忙等待(如果你不允许使用条件变量)。如果 ppl,我把它留在这里。有同样的想法,他们认为这不起作用;)

struct Semaphore {
int size;
atomic<int> count;
mutex protection;
mutex wait;

Semaphore(int n) : size(n) { count.store(0); }

void aquire() {
    protection.lock();
    --count;
    if (count < -1) {
        protection.unlock();
        wait.lock();
    }
    protection.unlock();
}

void release() {
    protection.lock();
    ++count;
    if (count > 0) {
        wait.unlock();
    }
    protection.unlock();
}
};

【讨论】:

  • 值得注意的是,这个解决方案实际上并不需要 count 是原子的,因为所有需要的同步都是通过 protection 互斥锁完成的。
  • 不,这不起作用。只有在 'acquire' 中执行 'wait.lock()' 的人才能在 release 中成功调用 'wait.unlock()' ,但是它不能,因为它在 'acquire' 中被阻塞,并且任何其他线程都会出错不是互斥锁的所有者。
  • 对我来说它与@Dolda2000 相同:我不知道 C++11 中的互斥锁具有该属性。这是有道理的,通常你会为此使用条件变量。
  • 如果您有一个特定的线程来处理“等待”的所有锁定和解锁,并且您只需要求该线程锁定和解锁(即实现任何人都可以获取或释放的互斥锁),这实际上可以工作)。
  • 这是一个非常有趣的想法,虽然我现在无法想象如何实现它。我的意思是我可以想象的是信号量是它自己的线程,客户端线程调用获取并且总是立即进入睡眠状态,如果获取操作。工作,它被线程唤醒,否则他们等待。但是,如果获取有效,我如何保证我不会错过那一次唤醒?还是我现在完全走错了路?
【解决方案3】:

编辑 - 使用第二个互斥体代替线程进行排队

由于互斥体已经有适当的线程支持,它可以用来对线程进行排队(而不是像我第一次尝试那样显式地这样做)。 除非互斥锁只允许所有者解锁它(一个?),那么这个解决方案不起作用。

我在搜索时遇到的Anthony Howe's pdf 中找到了解决方案。还有另外两种解决方案。我更改了名称以使这个示例更有意义。

或多或少的伪代码:

Semaphore{
    int n;
    mutex* m_count;         //unlocked initially
    mutex* m_queue;         //locked initially
};

void wait(){
    m_count.lock();
    n = n-1;
    if(n < 0){
        m_count.unlock();
        m_queue.lock();     //wait
    }
    m_count.unlock();       //unlock signal's lock
}

void signal(){
    m_count.lock();
    n = n+1;

    if(n <= 0){
        m_queue.unlock();   //leave m_count locked
    }
    else{
        m_count.unlock();
    }
}

【讨论】:

  • 这行不通。如果你有一个线程wait(),那么在它释放m1之后但在它阻塞之前由操作系统切换上下文,那么你有另一个线程调用signal(),查看队列中的线程,通知该线程唤醒(它将)。但是第一个线程是下一条要阻塞的指令,所以它会立即休眠。
  • 编辑了我的答案。现在意识到它与此处的另一个答案相同。
  • 我认为这仍然是坏的。假设 t1 从信号开始。然后,“m_count”被锁定,而 m_queue 被解锁。现在,让 t2 等待。由于 'count' 被锁定,它在等待进入 'wait()' 函数时停止。此外,除非您在等待之前从不发出信号,否则“m_count”永远不会被解锁,这不是您所追求的行为。
  • @jcw 对不起,我打错了。 m_countwait 开始时解锁(现在将对其进行编辑)。此外,m_queue 只有在n&lt;=0 时才会解锁,这意味着必须有一个线程在等待。 m_count 从一开始就被解锁,所以wait 可以在信号之前调用。另外,从信号开始没有意义吗?必须避免这种情况,并且根据我所见,通常留给编码人员。
【解决方案4】:

确实如此,因为从技术上讲,您的代码中的某些部分需要更多处理时间并且不需要存在。 1-您使用了原子数据类型atomic&lt;int&gt; count;,只要递增和递减被updateMutex.lock();代码锁定,这将花费更多时间来执行并且无用,因此在锁定状态期间没有其他线程可以更改它。

2- 你放了while (count &gt;= size) {},这也是没用的,因为你在 spinlock 语句之后再次检查了count,这是必要的,这里很重要,但这个不是。 "请记住,当互斥锁被另一个线程占用时,自旋锁是一个 while(1)。

如果你决定使用int count; 和一些编译器的优化,计数值不会改变,所以你需要使用 volatile。 最后,让我以更高效的方式重写您的代码。

struct Semaphore {
int size;
volatile int count;
mutex updateMutex;

Semaphore(int n) : size(n), count(0) {}

void aquire() {
    while (1) {
        updateMutex.lock();
        if (count >= size) {
            updateMutex.unlock();
            continue;
        }
        ++count;
        updateMutex.unlock();
        break;
    }
}

void release() {
    updateMutex.lock();
    if (count > 0) {
        --count;
    } // else log err
    updateMutex.unlock();
    }

 };

【讨论】:

    【解决方案5】:

    让我试试这个

    `

    # number of threads/workers
    w = 10
    # maximum concurrency
    cr = 5
    r_mutex = mutex()
    w_mutex = [mutex() for x in range(w)]
    
    # assuming mutex can be locked and unlocked by anyone 
    # (essentially we need a binary semaphore)
    
    def acquire(id):
    
        r_mutex.lock()
        cr -= 1
        # r_mutex.unlock()
        
        # if exceeding maximum concurrency
        if cr < 0:
            # lock twice to be waken up by someone
            w_mutex[id].lock()
            r_mutex.unlock()
            w_mutex[id].lock()
            w_mutex[id].unlock()
            return
    
        r_mutex.unlock()
    
        
        
           
    def release(id):
    
        r_mutex.lock()
        cr += 1
        # someone must be waiting if cr < 0
        if cr <= 0:
            # maybe you can do this in a random order
            for w in w_mutex:
                if w.is_locked():
                    w.unlock()
                    break
        r_mutex.unlock()
    

    `

    【讨论】:

    • @Sneftel 愿意提出这个方案吗?
    • 一个线程运行acquire并在释放r_mutex后被抢占。另一个线程运行release,但在没有找到第一个要唤醒的线程的情况下通过循环。
    • 此外,互斥锁不是这样工作的。 POSIX 和 Widows 互斥锁,以及标准 C 或 C++ 互斥锁都不能被不拥有它们的线程释放。
    • @Sneftel,感谢您的指出。我已经指出我需要的是一个二进制信号量
    猜你喜欢
    • 2013-12-07
    • 2012-03-20
    • 2016-08-30
    • 1970-01-01
    • 2011-04-20
    • 2011-10-11
    • 1970-01-01
    • 2017-08-24
    • 2011-06-24
    相关资源
    最近更新 更多