百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术资源 > 正文

每个程序员应该彻底掌握的多线程编程(Linux C)

moboyou 2025-06-05 16:49 5 浏览

多线程编程可以说每个程序员的基本功,同时也是开发中的难点之一,本文以Linux C为例,讲述了线程的创建及常用的几种线程同步的方式,最后对多线程编程进行了总结与思考并给出代码示例。

一、创建线程

多线程编程的第一步,创建线程。创建线程其实是增加了一个控制流程,使得同一进程中存在多个控制流程并发或者并行执行。

线程创建函数,其他函数这里不再列出,可以参考pthread.h。

#include<pthread.h>

int pthread_create(
    pthread_t *restrict thread,  /*线程id*/
	const pthread_attr_t *restrict attr,    /*线程属性,默认可置为NULL,表示线程属性取缺省值*/
	void *(*start_routine)(void*),  /*线程入口函数*/ 
	void *restrict arg  /*线程入口函数的参数*/
	);

代码示例:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

char* thread_func1(void* arg) {
    pid_t pid = getpid();
    pthread_t tid = pthread_self();
    printf("%s pid: %u, tid: %u (0x%x)\n", (char*)arg, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid);

    char* msg = "thread_func1";
    return msg;
}

void* thread_func2(void* arg) {
    pid_t pid = getpid();
    pthread_t tid = pthread_self();
    printf("%s pid: %u, tid: %u (0x%x)\n", (char*)arg, (unsigned int)pid, (unsigned int)tid, (unsigned int)tid);
    char* msg = "thread_func2 ";
    while(1) {
        printf("%s running\n", msg);
        sleep(1);
    }
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    if (pthread_create(&tid1, NULL, (void*)thread_func1, "new thread:") != 0) {
        printf("pthread_create error.");
        exit(EXIT_FAILURE);
    }

    if (pthread_create(&tid2, NULL, (void*)thread_func2, "new thread:") != 0) {
        printf("pthread_create error.");
        exit(EXIT_FAILURE);
    }
    pthread_detach(tid2);

    char* rev = NULL;
    pthread_join(tid1, (void *)&rev);
    printf("%s return.\n", rev);
    pthread_cancel(tid2);

    printf("main thread end.\n");
    return 0;
}

二、线程同步

有时候我们需要多个线程相互协作来执行,这时需要线程间同步。线程间同步的常用方法有:

  • 互斥
  • 信号量
  • 条件变量

我们先看一个未进行线程同步的示例:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

#define LEN 100000
int num = 0;

void* thread_func(void* arg) {
    for (int i = 0; i< LEN; ++i) {
        num += 1;
    }
    
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, (void*)thread_func, NULL);
    pthread_create(&tid2, NULL, (void*)thread_func, NULL);

    char* rev = NULL;
    pthread_join(tid1, (void *)&rev);
    pthread_join(tid2, (void *)&rev);

    printf("correct result=%d, wrong result=%d.\n", 2*LEN, num);
    return 0;
}

运行结果:correct result=200000, wrong result=106860.。

分享更多关于 Linux后端开发网络底层原理知识学习提升 点击 正在跳转 获取,完善技术栈,内容知识点包括Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK等等。

【1】互斥

这个是最容易理解的,在访问临界资源时,通过互斥,限制同一时刻最多只能有一个线程可以获取临界资源。

其实互斥的逻辑就是:如果访问临界资源发现没有其他线程上锁,就上锁,获取临界资源,期间如果其他线程执行到互斥锁发现已锁住,则线程挂起等待解锁,当前线程访问完临界资源后,解锁并唤醒其他被该互斥锁挂起的线程,等待再次被调度执行。

“挂起等待”和“唤醒等待线程”的操作如何实现?每个Mutex有一个等待队列,一个线程要在Mutex上挂起等待,首先在把自己加入等待队列中,然后置线程状态为睡眠,然后调用调度器函数切换到别的线程。一个线程要唤醒等待队列中的其它线程,只需从等待队列中取出一项,把它的状态从睡眠改为就绪,加入就绪队列,那么下次调度器函数执行时就有可能切换到被唤醒的线程。

主要函数如下:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *restrict mutex,     
       const pthread_mutexattr_t *restrict attr);       /*初始化互斥量*/
int pthread_mutex_destroy(pthread_mutex_t *mutex);      /*销毁互斥量*/
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

用互斥解决上面计算结果错误的问题,示例如下:

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<pthread.h>

#define LEN 100000
int num = 0;

void* thread_func(void* arg) {
    pthread_mutex_t* p_mutex = (pthread_mutex_t*)arg;
    for (int i = 0; i< LEN; ++i) {
        pthread_mutex_lock(p_mutex);
        num += 1;
        pthread_mutex_unlock(p_mutex);
    }
    
    return NULL;
}

int main() {
    pthread_mutex_t m_mutex;
    pthread_mutex_init(&m_mutex, NULL);

    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, (void*)thread_func, (void*)&m_mutex);
    pthread_create(&tid2, NULL, (void*)thread_func, (void*)&m_mutex);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    pthread_mutex_destroy(&m_mutex);

    printf("correct result=%d, result=%d.\n", 2*LEN, num);
    return 0;
}

运行结果:correct result=200000, result=200000.

如果在互斥中还嵌套有其他互斥代码,需要注意死锁问题。

产生死锁的两种情况:

  • 一种情况是:如果同一个线程先后两次调用lock,在第二次调用时,由于锁已经被占用,该线程会挂起等待别的线程释放锁,然而锁正是被自己占用着的,该线程又被挂起而没有机会释放锁,因此就永远处于挂起等待状态了,产生死锁。
  • 另一种典型的死锁情形是:线程A获得了锁1,线程B获得了锁2,这时线程A调用lock试图获得锁2,结果是需要挂起等待线程B释放锁2,而这时线程B也调用lock试图获得锁1,结果是需要挂起等待线程A释放锁1,于是线程A和B都永远处于挂起状态了。

如何避免死锁:

  1. 不用互斥锁(这个很多时候很难办到)
  2. 写程序时应该尽量避免同时获得多个锁。
  3. 如果一定有必要这么做,则有一个原则:如果所有线程在需要多个锁时都按相同的先后顺序(常见的是按Mutex变量的地址顺序)获得锁,则不会出现死锁。 (比如一个程序中用到锁1、锁2、锁3,它们所对应的Mutex变量的地址是锁1<锁2<锁3,那么所有线程在需要同时获得2个或3个锁时都应该按锁1、锁2、锁3的顺序获得。如果要为所有的锁确定一个先后顺序比较困难,则应该尽量使用pthread_mutex_trylock调用代替pthread_mutex_lock调用,以避免死锁。

【2】条件变量

条件变量概括起来就是:一个线程需要等某个条件成立(而这个条件是由其他线程决定的)才能继续往下执行,现在这个条件不成立,线程就阻塞等待,等到其他线程在执行过程中使这个条件成立了,就唤醒线程继续执行。

相关函数如下:

#include <pthread.h>

int pthread_cond_destroy(pthread_cond_t *cond);
int pthread_cond_init(pthread_cond_t *restrict cond,
       const pthread_condattr_t *restrict attr);
int pthread_cond_timedwait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex,
       const struct timespec *restrict abstime);
int pthread_cond_wait(pthread_cond_t *restrict cond,
       pthread_mutex_t *restrict mutex);
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond);

举个最容易理解条件变量的例子,“生产者-消费者”模式中,生产者线程向队列中发送数据,消费者线程从队列中取数据,当消费者线程的处理速度大于生产者线程时,会产生队列中没有数据了,一种处理办法是等待一段时间再次“轮询”,但这种处理方式不太好,你不知道应该等多久,这时候条件变量可以很好的解决这个问题。下面是代码:

#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>

#define LIMIT 1000

struct data {
    int n;
    struct data* next;
};

pthread_cond_t condv = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER; 
struct data* phead = NULL;

void producer(void* arg) {
    printf("producer thread running.\n");
    int count = 0;
    for (;;) {
        int n = rand() % 100;
        struct data* nd = (struct data*)malloc(sizeof(struct data));
        nd->n = n;

        pthread_mutex_lock(&mlock);
        struct data* tmp = phead;
        phead = nd;
        nd->next = tmp;
        pthread_mutex_unlock(&mlock);
        pthread_cond_signal(&condv);

        count += n;

        if(count > LIMIT) {
            break;
        }
        sleep(rand()%5);
    }
    printf("producer count=%d\n", count);
}

void consumer(void* arg) {
    printf("consumer thread running.\n");
    int count = 0;
    for(;;) {
        pthread_mutex_lock(&mlock);
        if (NULL == phead) {
            pthread_cond_wait(&condv, &mlock);
        } else {
            while(phead != NULL) {
                count += phead->n;
                struct data* tmp = phead;
                phead = phead->next;
                free(tmp);
            }
        }
        pthread_mutex_unlock(&mlock);
        if (count > LIMIT)
            break;
    }
    printf("consumer count=%d\n", count);
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, (void*)producer, NULL);
    pthread_create(&tid2, NULL, (void*)consumer, NULL);
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

分享更多关于 Linux后端开发网络底层原理知识学习提升 点击 正在跳转 获取,完善技术栈,内容知识点包括Linux,Nginx,ZeroMQ,MySQL,Redis,线程池,MongoDB,ZK,Linux内核,CDN,P2P,epoll,Docker,TCP/IP,协程,DPDK等等。

完整视频链接点击:C/C++Linux服务器开发/后台架构师【零声学院】-学习视频教程-腾讯课堂

条件变量中的执行逻辑:

关键是理解执行到int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex) 这里时发生了什么,其他的都比较容易理解。执行这条函数前需要先获取互斥锁,判断条件是否满足,如果满足执行条件,则继续向下执行后释放锁;如果判断不满足执行条件,则释放锁,线程阻塞在这里,一直等到其他线程通知执行条件满足,唤醒线程,再次加锁,向下执行后释放锁。(简而言之就是:释放锁-->阻塞等待-->唤醒后加锁返回

上面的例子可能有些繁琐,下面的这个代码示例则更为简洁:

#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>

#define NUM 3
pthread_cond_t condv = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mlock = PTHREAD_MUTEX_INITIALIZER; 

void producer(void* arg) {
    int n = NUM;
    while(n--) {
        sleep(1);
        pthread_cond_signal(&condv);
        printf("producer thread send notify signal. %d\t", NUM-n);
    }
}

void consumer(void* arg) {
    int n = 0;
    while (1) {
        pthread_cond_wait(&condv, &mlock);
        printf("recv producer thread notify signal. %d\n", ++n);
        if (NUM == n) {
            break;
        }
    }
}

int main() {
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, (void*)producer, NULL);
    pthread_create(&tid2, NULL, (void*)consumer, NULL);
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

运行结果:

producer thread send notify signal. 1   recv producer thread notify signal. 1
producer thread send notify signal. 2   recv producer thread notify signal. 2
producer thread send notify signal. 3   recv producer thread notify signal. 3

【3】信号量

信号量适用于控制一个仅支持有限个用户的共享资源。用于保持在0至指定最大值之间的一个计数值。当线程完成一次对该semaphore对象的等待时,该计数值减一;当线程完成一次对semaphore对象的释放时,计数值加一。当计数值为0时,线程挂起等待,直到计数值超过0.

主要函数如下:

#include <semaphore.h>

int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_post(sem_t * sem);
int sem_destroy(sem_t * sem);

代码示例如下:

#include<sys/types.h>
#include<unistd.h>
#include<stdlib.h>
#include<stdio.h>
#include<pthread.h>
#include<errno.h>
#include<string.h>
#include<semaphore.h>

#define NUM 5

int queue[NUM];
sem_t psem, csem; 

void producer(void* arg) {
    int pos = 0;
    int num, count = 0;
    for (int i=0; i<12; ++i) {
        num = rand() % 100;
        count += num;
        sem_wait(&psem);
        queue[pos] = num;
        sem_post(&csem);
        printf("producer: %d\n", num); 
        pos = (pos+1) % NUM;
        sleep(rand()%2);
    }
    printf("producer count=%d\n", count);
}

void consumer(void* arg){
    int pos = 0;
    int num, count = 0;
    for (int i=0; i<12; ++i) {
        sem_wait(&csem);
        num = queue[pos];
        sem_post(&psem);
        printf("consumer: %d\n", num);
        count += num;
        pos = (pos+1) % NUM;
        sleep(rand()%3);
    }
    printf("consumer count=%d\n", count);    
} 

int main() {
    sem_init(&psem, 0, NUM);
    sem_init(&csem, 0, 0);

    pthread_t tid[2];
    pthread_create(&tid[0], NULL, (void*)producer, NULL);
    pthread_create(&tid[1], NULL, (void*)consumer, NULL);
    pthread_join(tid[0], NULL);
    pthread_join(tid[1], NULL);
    sem_destroy(&psem);
    sem_destroy(&csem);

    return 0;
}

信号量的执行逻辑:

当需要获取共享资源时,先检查信号量,如果值大于0,则值减1,访问共享资源,访问结束后,值加1,如果发现有被该信号量挂起的线程,则唤醒其中一个线程;如果检查到信号量为0,则挂起等待。

三、多线程编程总结与思考

最后,我们对多线程编程进行总结与思考。

  • 第一点就是在进行多线程编程时一定注意考虑同步的问题,因为多数情况下我们创建多线程的目的是让他们协同工作,如果不进行同步,可能会出现问题。
  • 第二点,死锁的问题。在多个线程访问多个临界资源时,处理不当会发生死锁。如果遇到编译通过,运行时卡住了,有可能是发生死锁了,可以先思考一下是那些线程会访问多个临界资源,这样查找问题会快一些。
  • 第三点,临界资源的处理,多线程出现问题,很大原因是多个线程访问临界资源时的问题,一种处理方式是将对临界资源的访问与处理全部放到一个线程中,用这个线程服务其他线程的请求,这样只有一个线程访问临界资源就会解决很多问题。
  • 第四点,线程池,在处理大量短任务时,我们可以先创建好一个线程池,线程池中的线程不断从任务队列中取任务执行,这样就不用大量创建线程与销毁线程,这里不再细述。

相关推荐

Spring Boot3 中多线程技术的使用指南

在当今互联网应用场景下,高并发、大数据量处理已成为常态。用户对应用的响应速度和处理能力要求越来越高。以一个电商平台的订单处理系统为例,在促销活动期间,短时间内会涌入大量订单请求,如果采用单线程理,所有...

多线程会带来的一些问题

前言前面我们已经知道了,在使用多线程会给我们带来一些性能上的提升。但一个东西的出现总是会存在优缺点。1、多线程会在线程安全问题什么是线程安全?在深入理解JVM这本书中有句话可以很简单的去理解“如果一个...

经典面试题:SpringBoot 应用可以同时并发处理多少请求

前言hello,大家好,,最近逛帖子看到一个面试题:SpringBoot应用可以同时并发处理多少请求?看到这个问题大多数朋友也许都会回答200,这样你也许第二天就会收到如下拒信:你可能会很难疑惑,...

Java并发包(java.util.concurrent)探秘

Java并发包(java.util.concurrent)探秘在当今高并发、高负载的应用场景下,Java的并发包(java.util.concurrent)成为了开发者手中的神器。它不仅简化了并发编程...

操作系统-多线程编程-并发编程机制

十四、多线程编程POSIX标准中定义的现程,属性,操作方法被广泛认可和遵循。最贴近POSIX标准的线程实现,NPTL(NativePOSIXThreadsLibrary)线程可以看作进程的一个...

Java 多线程:让你的程序像开挂一样干活!

你有没有想过,当你的Java程序在处理大量任务时,能不能像哪吒三头六臂一样,同时做好几件事?答案就在Java多线程!它能让你的程序瞬间“开挂”,大大提高效率。别被“多线程”这个听起来高大...

记一次Synchronized使用不合理,导致的多线程下线程阻塞问题排查

在为客户进行性能诊断调优时,碰到了一个Synchronized关键字使用不合理导致多线程下线程阻塞的情况。用文字记录下了问题的整个发现-排查-分析-优化过程,排查过程中使用了我司商业化产品——XLan...

Disruptor—2.并发编程相关简介

大纲1.并发类容器2.volatile关键字与内存分析3.Atomic系列类与UnSafe类4.JUC常用工具类5.AQS各种锁与架构核心6.线程池的最佳使用指南1.并发类容器(1)Concurren...

实战项目:手把手带你实现一个高并发内存池

项目介绍1.这个项目做的是什么?当前项目是实现一个高并发的内存池,他的原型是google的一个开源项目tcmalloc,tcmalloc全称Thread-CachingMalloc,即线程缓存的ma...

JAVA多线程详解(超详细)

一、线程简介1、进程、线程程序:开发写的代码称之为程序。程序就是一堆代码,一组数据和指令集,是一个静态的概念。进程(Process):将程序运行起来,我们称之为进程。进程是执行程序的一次执行过程,它...

ConcurrentModificationException 并发修改异常的真相与破解之道

在Kotlin开发中,ConcurrentModificationException(并发修改异常)是让开发者头疼的“老熟人”。无论是单线程遍历时修改集合,还是多线程并发操作集合,这个异常都可能突然出...

Java并发编程核心技巧:让程序飞速奔跑的秘密武器

Java并发编程核心技巧:让程序飞速奔跑的秘密武器在这个信息化的时代,我们的应用程序需要处理越来越多的数据,同时还要保证响应速度和稳定性。Java作为一门广泛应用于企业级开发的语言,其强大的并发编程能...

如何使用CompletableFuture进行大数据量并发处理

面试官:有这样的一个需求,一批几百万的用户数据,需调用第三方的接口给用户发送消息。如何在一分钟内快速给这批用户发送完消息。在处理这种要求在一分钟内向大量用户发送消息的场景中,可以使用以下方法结合Com...

线程安全集合 --- Concurrent

引言最近看一些代码的时候,发现有人用System.Collections.Concurrent下的BlockingCollection很便利的实现了生产者-消费者模式,这是之前没有注意到的...

Java并发包(Concurrent)详解:让你的程序跑得更快更稳

Java并发包(Concurrent)详解:让你的程序跑得更快更稳提到Java并发包(Concurrent),我们就像是进入了武侠世界的“少林武当”,这里高手云集,各种工具类和框架应有尽有,它们就像武...