C使用信号量和线程打印乒乓球

C print ping-pong using semaphores and threads

我不确定我是否理解信号量和线程,所以我决定尝试一个相对简单的例子。我试图让 2 个线程交替打印,一个打印 "ping" 另一个打印 "pong" 每个通知另一个它是通过使用信号量完成的。但是当我执行下面的代码时,它会打印数百次 ping,然后打印数百次 pong 并稍作停顿。

#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
sem_t pingsem;

void ping(){
    printf("Ping started\n");
    while(1){
        sem_wait(&pingsem);
        printf("ping\n");
        sem_post(&pingsem);
    }
}

void pong(){
    printf("Pong started\n");
    while(1){
        sem_wait(&pingsem);
        printf("pong\n");
        sleep(1);
        sem_post(&pingsem);
    }
}

int main(){
    sem_destroy(&pingsem);  //make sure the semaphore starts dead
    sem_init(&pingsem, 0, 1);  //initialize semaphore
    pthread_t ping_thread, pong_thread;  //start the threading
    pthread_create(&ping_thread, NULL, ping, NULL);
    pthread_create(&pong_thread, NULL, pong, NULL);
    pthread_join(ping_thread,NULL);
    pthread_join(pong_thread,NULL);

    return 0;
}

我编译使用:

gcc stest.c -o stest -lpthread -lrt

没有错误或警告,但是当我 运行 它时,我得到:

$ ./stest
Ping started
ping
ping
ping
ping
Pong started
ping
ping
.
. hundreds of pings
.
ping
ping
ping
pong
pong
pong
pong
.
. hundreds of pongs
.

它最终会关闭,但为什么线程不会每隔一个线程交替打印?

因为当您创建一个线程时,它会在 OS 说可以时立即执行! 所以你像这样创建了两个线程:

pthread_create(&ping_thread, NULL, ping, NULL);
// scheduler interrupt from OS
pthread_create(&pong_thread, NULL, pong, NULL);

这很好,但是 OS 看到了第一个新线程并且 运行 它直到它的时间片用完。只有这样主线程才能重新获得足够长的控制权来创建下一个线程。

至于为什么不交替就是另外一回事了!你知道线程同步有多难吗?你有这个代码:

while(1){
    sem_wait(&pingsem);
    printf("ping\n");
    sem_post(&pingsem);
}

但是您将信号量初始化为值 1。因此 sem_wait 递减为 0,然后打印一条消息,然后递增回 1。没问题,对吗?

好吧,sem_post 和后续(下一个循环)sem_wait 之间的延迟只有 1 条指令,即跳回到循环开头。因此,除非碰巧 OS 在 sem_post 之后中断线程,但在 sem_wait 之前,单个线程将继续自行打印。

您的示例中显示的问题是,这是一场竞赛,因为读取没有有效地阻止另一个。在调度程序允许的情况下,两个线程 运行。按照它的编码方式,每个线程都可以在其时间片内多次 free-运行 (循环),并满足自己的信号量测试。在具有典型调度的 multi-core/multi-CPU 系统上,两个线程都可以同时 运行 并且可以任意地跨过彼此。

这是一个有效的 ping-pong 线程示例,它使用互补信号量来创建您想要的 ping-pong 互锁。

#include <semaphore.h>
#include <pthread.h>
#include <stdio.h>

sem_t pingsem, pongsem;

void *
ping(void *arg) 
{
    for (;;) {
        sem_wait(&pingsem);
        printf("ping\n");
        sem_post(&pongsem);
    }
}

void *
pong(void *arg) 
{
    for (;;) {
        sem_wait(&pongsem);
        printf("pong\n");
        sem_post(&pingsem);
    }
}

int 
main(void) 
{
    sem_init(&pingsem, 0, 0);
    sem_init(&pongsem, 0, 1);
    pthread_t ping_thread, pong_thread; 
    pthread_create(&ping_thread, NULL, ping, NULL);
    pthread_create(&pong_thread, NULL, pong, NULL);
    pthread_join(ping_thread, NULL);
    pthread_join(pong_thread, NULL);
    return 0;
}

James T. Smith 的答案是正确的.. 因为你在再次获取锁之前没有安排时间,所以你会期望它几乎每次都能在线程完成之前重新获取锁是时间片。

如果你想强制安排出来让另一个可以运行,你可以尝试睡眠(0)或sched_yield()。这将强制调度,以便如果另一个线程正在等待 运行 它会。这将更有可能看到你的 ping,pong,ping,pong。但仍然不能保证并且完全依赖于您的 OS 调度程序(并且可能只适用于两个线程具有相同优先级的单核系统)。

试试这个:(就像我说的.. 不能保证.. 但很有可能会奏效)

void ping(){
    printf("Ping started\n");
    while(1){
        sem_wait(&pingsem);
        printf("ping\n");
        sem_post(&pingsem);
        sched_yield(); // Schedule out.
    }
}

void pong(){
    printf("Pong started\n");
    while(1){
        sem_wait(&pingsem);
        printf("pong\n");
        sleep(1);
        sem_post(&pingsem);
        sched_yield(); // Schedule out.
    }
}

编辑:更改为 sched_yield()

其他答案是正确的,我只是发布代码 运行 如您所愿(无论如何在 OSX 10.10.3 上)

#include <semaphore.h>
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <unistd.h>
sem_t pingsem;

void ping(){
    printf("Ping started\n");
    while (1) {
        sem_wait(&pingsem);
        printf("ping\n");
        sem_post(&pingsem);
    }
}

void pong(){
    printf("Pong started\n");
    while (1) {
        sem_wait(&pingsem);
        printf("pong\n");
        sem_post(&pingsem);
    }
}

int main(){
    sem_destroy(&pingsem);  //make sure the semaphore starts dead
    sem_init(&pingsem, 0, 1);  //initialize semaphore
    pthread_t ping_thread, pong_thread;  //start the threading
    pthread_create(&ping_thread, NULL, ping, (void *)0);
    pthread_create(&pong_thread, NULL, pong, (void *)1);


    pthread_exit(NULL);

    return 0;
}

例如,您需要两个信号量,'pingsem' 和 'pongsem'。将一个初始化为 1,将另一个初始化为零。那么:

ping_thread:

while(true){
  wait(pingsem);
  doWork();
  send(pongsem);
}

pong_thread:

while(true){
  wait(pongsem);
  doWork();
  send(pingsem);
}

初始化为一个信号量的一个单元然后充当工作令牌并在线程之间来回发送信号。只有拥有令牌的线程才能完成工作,其他线程必须等到它获得令牌。