本文最后更新于 119 天前。
c++使用semaphore.h(信号量)实现多线程之间的同步关系:
#include<semaphore.h>
class Foo {
private:
sem_t firstDone; // 定义信号量变量
sem_t secondDone;
public:
Foo() {
sem_init(&firstDone, 0, 0); // 初始化信号量变量
sem_init(&secondDone, 0, 0);
}
void first(function<void()> printFirst) {
// printFirst() outputs "first". Do not change or remove this line.
printFirst();
sem_post(&firstDone); //增加信号量变量
}
void second(function<void()> printSecond) {
sem_wait(&firstDone); //减少信号量变量的值
// printSecond() outputs "second". Do not change or remove this line.
printSecond();
sem_post(&secondDone); //增加信号量变量
}
void third(function<void()> printThird) {
sem_wait(&secondDone); //减少信号量变量的值
// printThird() outputs "third". Do not change or remove this line.
printThird();
}
};
具体解释:
sem_init:
sem_init
是初始化信号量的函数,它属于<semaphore.h>
头文件定义的功能。
&firstJobDone
和&secondJobDone
是信号量变量的地址。在C/C++中,通过传递变量的地址(即使用取地址符&
),可以允许函数修改这些变量的值。这里假设firstJobDone
和secondJobDone
都是已经声明为sem_t
类型的变量。
函数sem_init
接受三个参数:
- 第一个参数是指向要初始化的信号量的指针(在这里是
&firstJobDone
和&secondJobDone
)。 - 第二个参数
0
表示pshared
,说明这两个信号量是进程私有的,仅在同一进程中各线程间共享。如果该值为非0,则信号量可以在多个进程间共享,但这通常需要额外的权限并且与具体的系统实现相关。 - 第三个参数
0
是信号量的初始值。在这个例子中,两个信号量都被初始化为0。这意味着任何尝试立即获取(通过sem_wait
)这些信号量的线程都会阻塞,直到信号量的值被其他线程通过sem_post
增加。
sem_wait:
当信号量的值为0时,线程调用sem_wait
函数将会执行以下操作:
- 阻塞线程:调用
sem_wait
的线程会立即进入阻塞状态。这意味着该线程会暂停执行,将其自身置于等待状态,直到信号量的值变为非零。 - 等待唤醒:该线程会保持阻塞,直到其他某个线程对同一个信号量执行
sem_post
操作,这会将信号量的值增加1。一旦信号量的值从0变为了1,操作系统从等待该信号量的所有线程中选择一个(具体选择哪一个线程取决于操作系统的调度策略,通常为先进先出FIFO或优先级调度),并解除这个幸运线程的阻塞状态,允许它继续执行。 - 继续执行与减量:被唤醒的线程将继续执行,并且在恢复执行前,
sem_wait
函数会自动将信号量的值减1。这样,即使信号量因为sem_post
而变为1,被唤醒线程执行完sem_wait
后,信号量又会回到0,确保了后续的sem_wait
调用依然能够正确地进行同步控制。
sem_wait
不是忙等待(busy waiting)。忙等待是指一个进程或线程通过不断地检查某个条件是否满足来等待某个事件的发生,期间不做任何延时或让出CPU的操作,因此会持续占用处理器资源。而sem_wait
函数在设计上会避免这种行为。
当调用sem_wait
时,如果信号量的值为0,线程会主动放弃CPU的使用权,进入睡眠(阻塞)状态,直到另一个线程调用sem_post
增加了信号量的值,从而唤醒该等待的线程。在等待期间,该线程不消耗CPU资源,因此不会造成不必要的CPU占用或浪费,这与忙等待的行为截然不同。所以,sem_wait
提供了一种有效的同步机制,同时避免了忙等待带来的效率问题。
以下代码不能实现同步:
class Foo {
public:
Foo() {
}
bool firstSignal = false, secondSignal = false;
void first(function<void()> printFirst) {
// printFirst() outputs "first". Do not change or remove this line.
printFirst();
firstSignal = true;
}
void second(function<void()> printSecond) {
while(1){
if(firstSignal)break;
}
// printSecond() outputs "second". Do not change or remove this line.
printSecond();
secondSignal = true;
}
void third(function<void()> printThird) {
while(1){
if(secondSignal)break;
}
// printThird() outputs "third". Do not change or remove this line.
printThird();
}
};
这段代码试图通过无限循环(`while(1)`)和检查布尔标志(`firstSignal` 和 `secondSignal`)的方式来实现三个函数(`first`, `second`, `third`)之间的同步,以确保它们按“first -> second -> third”的顺序打印输出。然而,这样的实现方法存在几个问题,导致它不是一个有效的线程同步方案:
1. **忙等待**: 代码中使用了无限循环 (`while(1)`) 并不断检查标志,这是一种忙等待的实现方式。正如之前讨论的,这会导致执行这些循环的线程持续占用CPU资源,而不是释放CPU给其他线程或任务使用,从而降低了程序的整体效率。
2. **缺乏线程协调**: 仅依赖于循环检查和设置布尔变量无法保证线程间的正确交替执行。在多线程环境中,如果线程调度不巧,可能会导致死锁或饥饿现象。例如,如果`first`函数尚未被执行,而线程调度器一直让`second`或`third`函数所在的线程运行,那么程序将永远卡在检查`firstSignal`的循环中,无法继续。
3. **未利用现有同步机制**: C++ 提供了多种线程同步原语,如互斥锁(`std::mutex`)、条件变量(`std::condition_variable`)、原子操作(`std::atomic`)等,这些机制能更高效、安全地实现线程间的同步。而这段代码没有使用这些工具。
力扣1115.交替打印foobar
#include<semaphore.h>
class FooBar {
private:
int n;
sem_t foo_ok;
sem_t bar_ok;
public:
FooBar(int n) {
this->n = n;
sem_init(&foo_ok, 0, 0);
sem_init(&bar_ok, 0, 0);
}
void foo(function<void()> printFoo) {
for (int i = 0; i < n; i++) {
if(i != 0){
sem_wait(&bar_ok);
}
// printFoo() outputs "foo". Do not change or remove this line.
printFoo();
sem_post(&foo_ok);
}
}
void bar(function<void()> printBar) {
for (int i = 0; i < n; i++) {
sem_wait(&foo_ok);
// printBar() outputs "bar". Do not change or remove this line.
printBar();
sem_post(&bar_ok);
}
}
};