一文讲清楚C语言多线程、信号、互斥量、条件变量的原理与使用(含案例)

引入

线程(Thread):线程是一个程序内部的并发执行流,也被称为轻量级进程。与进程不同,线程之间共享相同的内存空间,因此可以更方便地共享数据和通信。线程可以通过pthread库进行创建、销毁、同步和通信等操作。

信号(Signal):信号是UNIX和类UNIX系统中的一种进程间通信机制,用于通知进程发生了某些特定事件。

互斥锁(Mutex):互斥锁是一种同步原语,用于保护共享资源,避免多个线程同时访问同一个资源导致的数据竞争问题。在访问共享资源时,线程需要先获得互斥锁,以防其他线程同时访问该资源。一旦线程完成了对资源的访问,就必须释放互斥锁,以便其他线程可以继续访问该资源。

当一个线程获得了锁并进入临界区时,其他线程在试图获得同一把锁时会被阻塞。这是互斥锁的核心作用,即保证同一时刻只有一个线程能够进入临界区,避免数据竞争和不可预测的结果。

其他线程并不是完全无法进行,而是在尝试获取锁的过程中会被阻塞,直到获得锁的线程释放锁并离开临界区,其他线程才有机会获取锁并进入临界区。在此期间,阻塞的线程可以等待、休眠或者执行其他任务,以避免浪费CPU资源。

上锁和释放锁之间的操作必须尽可能迅速,一般是简单的赋值操作等,避免一直阻塞其他线程。

条件变量(Condition Variable):通常与互斥锁一起使用,以允许线程在满足特定条件之前等待,以避免不必要的忙等待。当某个线程改变了共享资源并且希望通知其他线程时,可以通过条件变量发出信号。其他线程可以在条件变量上等待,直到接收到信号。

假设有一个多线程程序需要读写一个共享资源,那么就可以使用线程、信号、互斥锁、条件变量来实现线程间的同步和通信。

举个例子,比如一个多线程程序需要从一个共享队列中取出数据进行处理,但是如果队列为空,则需要等待直到队列中有数据再进行取出操作。这时可以使用一个互斥锁来保护队列的读写操作,防止多个线程同时访问队列造成数据错乱;使用条件变量来实现线程的等待和唤醒机制,当队列为空时,调用条件变量的等待函数将当前线程挂起,直到队列不为空时唤醒线程进行取出操作。同时使用信号来处理异常情况,例如程序需要结束时,使用信号通知所有线程退出。

这样,通过使用线程、信号、互斥锁、条件变量,可以实现多线程程序的同步和通信,提高程序的性能和并发处理能力。

程序一:创建线程(thread)

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg);

其中,thread是一个指向pthread_t类型的指针,用于存储新线程的ID;attr是一个指向pthread_attr_t类型的指针,表示新线程的属性,如果传递NULL,则使用默认属性;start_routine是新线程要执行的函数,它的返回值和参数均为void*类型;arg是传递给start_routine的参数,如果没有参数可以传递NULL。

调用pthread_create函数后,系统会创建一个新的线程,并开始执行start_routine函数。新线程的ID会存储在thread指向的内存中。如果创建线程成功,pthread_create函数返回0,否则返回一个非零的错误码。

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
static void *my_thread_func ()
{
    
    
    while (1)
    {
    
    
        sleep(1);
        /* code */
    }
    
}
int main()
{
    
    
    pthread_t tid;
    int ret;
    //创建线程
    ret=pthread_create(&tid,NULL,my_thread_func,NULL);
    //如果创建失败,输出error
    if(ret)
    {
    
    
        printf("error!\n");
        return -1;
    }
    while (1)
    {
    
    
        sleep(1);
        /* code */
    }
    return 0;
}

输出:

jing@ubuntu:~/Desktop/test$ gcc -o pthread pthread.c -lpthread
jing@ubuntu:~/Desktop/test$ ./pthread &
[2] 14553
jing@ubuntu:~/Desktop/test$ ps
   PID TTY          TIME CMD
 14376 pts/1    00:00:00 bash
 14533 pts/1    00:00:00 pthread
 14553 pts/1    00:00:00 pthread
 14556 pts/1    00:00:00 ps
jing@ubuntu:~/Desktop/test$ ps -T
   PID   SPID TTY          TIME CMD
 14376  14376 pts/1    00:00:00 bash
 14533  14533 pts/1    00:00:00 pthread
 14533  14534 pts/1    00:00:00 pthread
 14553  14553 pts/1    00:00:00 pthread
 14553  14554 pts/1    00:00:00 pthread
 14557  14557 pts/1    00:00:00 ps

程序二 信号(sem)

想要实现的功能:程序开始执行后,主线程等待用户输入,待用户输入后,转入分线程,执行打印输出操作

如何实现:使用信号机制

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
static char g_buf[1000];
static sem_t g_sem;
static void *my_thread_func ()
{
    
    
    while (1)
    {
    
    
        sem_wait(&g_sem);//等待信号,只有接收到信号时,才会执行下一步
        printf("recv:%s\n",g_buf);
        
    }
    
}
int main()
{
    
    
    pthread_t tid;
    int ret;
    sem_init(&g_sem,0,0);//信号初始化
    ret=pthread_create(&tid,NULL,my_thread_func,NULL);
    if(ret)
    {
    
    
        printf("error!\n");
        return -1;
    }
    while (1)
    {
    
    
        
        fgets(g_buf,1000,stdin);
        //发送信号
        sem_post(&g_sem);
    }
    return 0;
}

输出

jing@ubuntu:~/Desktop/test$ ./pthread2
12
1recv:12

123
1recv:123

123345
1recv:123345

出现的问题:假设用户正在输入数据,分线程还在执行,那么输出的结果可能是两者混合,因为这个buff量既可以被用户写入,也可以被分线程读,会产生冲突

程序三 互斥量(mutex)

函数

pthread_mutex_lock() 用于尝试获取互斥锁,如果该锁当前已被占用,则会阻塞等待,直到获得该锁才会返回。该函数的原型如下:

int pthread_mutex_lock(pthread_mutex_t *mutex);

pthread_mutex_unlock() 用于释放互斥锁,如果当前有线程阻塞等待该锁,则会唤醒其中一个线程继续执行。

int pthread_mutex_unlock(pthread_mutex_t *mutex);

代码

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
static char g_buf[1000];
static sem_t g_sem;
static pthread_mutex_t g_tMutex=PTHREAD_MUTEX_INITIALIZER;//创建锁
static void *my_thread_func ()
{
    
    
    while (1)
    {
    
    
        sem_wait(&g_sem);
        pthread_mutex_lock(&g_tMutex);
        printf("recv:%s\n",g_buf);
        pthread_mutex_unlock(&g_tMutex);
        
    }
    
}
int main()
{
    
    
    pthread_t tid;
    int ret;
    char buf[1000];
    sem_init(&g_sem,0,0);
    ret=pthread_create(&tid,NULL,my_thread_func,NULL);
    if(ret)
    {
    
    
        printf("error!\n");
        return -1;
    }
    while (1)
    {
    
    
        fgets(buf,1000,stdin);
        pthread_mutex_lock(&g_tMutex);//加锁
        memcpy(g_buf,buf,1000);
        pthread_mutex_unlock(&g_tMutex);//关锁
        sem_post(&g_sem);
    }
    return 0;
}

需要注意的是,不要直接给fgets输入的东西加锁,因为大部分执行在fgets里,可能会导致锁一直被占用,分线程无法获取

程序四 条件变量

条件变量和互斥量是同时使用的,用于设立一个条件,只有满足条件时才执行对某个资源进行操作

pthread_cond_wait

pthread_cond_wait 是一个线程阻塞函数,用于等待条件变量的触发,通常与互斥锁配合使用,可以实现线程的同步。

int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);

pthread_cond_wait 会自动释放 mutex 上的锁,并将调用线程放到条件变量等待队列中等待条件变量的信号。当另外一个线程调用 pthread_cond_signalpthread_cond_broadcast 通知条件变量时,pthread_cond_wait 函数返回,并重新获取 mutex 上的锁。此时,该线程就可以对共享资源进行操作了。

在使用条件变量时,一般的做法是将等待条件变量的操作和修改共享资源的操作放到一个临界区内,以确保数据的一致性和正确性。

pthread_cond_signal

pthread_cond_signal()函数用于唤醒正在等待特定条件变量的一个线程,它的函数原型如下:

int pthread_cond_signal(pthread_cond_t *cond);

该函数会通知一个等待条件变量cond的线程,使其从pthread_cond_wait()函数中阻塞的状态中返回。如果多个线程都在等待条件变量,那么只有其中一个线程会被唤醒,唤醒的线程将会重新竞争条件变量所对应的互斥锁,如果成功获取互斥锁,那么该线程将会继续执行,否则将会再次进入阻塞状态。如果成功唤醒一个线程,该函数返回0,否则返回一个非零值。

使用条件变量控制信号锁的好处如下:

  1. 减少CPU资源占用:在没有条件成立的情况下,线程会一直处于等待状态,不需要使用CPU资源。这可以避免忙等待(busy waiting)的情况,从而减少CPU资源占用。
  2. 避免死锁:使用条件变量可以更安全地处理多个线程之间的互斥关系,避免死锁的出现。
  3. 更好地控制线程:使用条件变量可以更好地控制线程的执行顺序和同步。
  4. 更好地处理复杂的线程交互关系:在多个线程之间存在复杂的交互关系时,使用条件变量可以更好地处理这些关系,使得代码更加简洁、易于理解和维护。

代码

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <semaphore.h>
#include <string.h>
static char g_buf[1000];
static sem_t g_sem;
static pthread_cond_t g_tConVar=PTHREAD_COND_INITIALIZER;//条件量
static pthread_mutex_t g_tMutex=PTHREAD_MUTEX_INITIALIZER;
static void *my_thread_func ()
{
    
    
    while (1)
    {
    
    
        sem_wait(&g_sem);
        pthread_mutex_lock(&g_tMutex);
        pthread_cond_wait(&g_tConVar,&g_tMutex);//只有条件满足时,才会获取信号锁
        
        printf("recv:%s\n",g_buf);
        pthread_mutex_unlock(&g_tMutex);
        
    }
    
}
int main()
{
    
    
    pthread_t tid;
    int ret;
    char buf[1000];
    sem_init(&g_sem,0,0);
    ret=pthread_create(&tid,NULL,my_thread_func,NULL);
    if(ret)
    {
    
    
        printf("error!\n");
        return -1;
    }
    while (1)
    {
    
    
        fgets(buf,1000,stdin);
        pthread_mutex_lock(&g_tMutex);
        memcpy(g_buf,buf,1000);
        pthread_cond_signal(&g_tConVar);//释放信号
        pthread_mutex_unlock(&g_tMutex);
        sem_post(&g_sem);
    }
    return 0;
}

pthread_cond_wait(&g_tConVar,&g_tMutex) 会先释放g_tMutex这个线程锁,然后挂起当前线程,直到另一个线程发送条件通知(通过pthread_cond_signalpthread_cond_broadcast函数发送),此时这个被挂起的线程将被唤醒并再次获取到g_tMutex线程锁。

linux命令补充

./pthread &//后台执行程序
ps //查看线程
ps -T //查看进程
kill-9 xxxx//杀线程
top //查看正在执行的程序,最新的在最上面

猜你喜欢

转载自blog.csdn.net/weixin_44738872/article/details/129659077