Loading

多线程编程同步:无锁设计

背景#

合集的前几篇都介绍了多线程的简单实现(锁设计),那么如何实现不带锁的多线程呢?

既然不能通过互斥锁、读写锁、信号量(有名和无名),那么只能通过全局变量标志来同步生产者线程和消费者线程。

实现#

方法一#

生产者线程每次往buff队列中写入一条数据后,需要更新这条数据的状态为: stored(注:数据的状态是一个枚举类型,仅有两个状态,分别是 emptystored)。当生产者线程将buff队列写满后,就不能往这个buff队列再写入新的数据,接下来应该等待消费者线程一条一条的读出数据(生产者写入一条数据后,生产者就可以读出数据了),当buff队列中的数据全部被读出后,需要将buff队列全部清空,并进入等待生产者线程写入新数据。

nolock_single_buffer.c

/*
 * @Description: mutil-threads implement that no lock design with single buffer
 * @Author: 
 * @version: 
 * @Date: 2023-11-14 17:00:24
 * @LastEditors: 
 * @LastEditTime: 2023-11-14 18:00:11
 */

//==============================================================================
// Include files
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <unistd.h>
#include <pthread.h>

//==============================================================================
// Constants
#define N_BUFF_SIZE 1024     // buff最大长度
#define BUFFER_QUEUE_LEN 100 // buff队列最大长度

#define MAX_N_ITEMS 1000000 // 生产消息最大数目

#define MAX(a,b) ((a)>(b) ? (a):(b)) 
#define MIN(a,b) ((a)<(b) ? (a):(b))

//==============================================================================
// types
/* buff队列结构体 */
 struct buff_quere
 {
    enum
    {
        empty = 0,
        stored
    }status;
    char buff[N_BUFF_SIZE];
};

//==============================================================================
// global varibles
static struct buff_quere g_queue_data[BUFFER_QUEUE_LEN]; // buff队列
static int g_cnt_of_queue = 0; //buff队列计数器
static int g_nItems = 0;
static bool g_exit = false;

//==============================================================================
// global functions
static void *produce(void *arg);
static void *consume(void *arg);

//==============================================================================
// The main entry-point function.
int main(int argc, char **argv)
{
    pthread_t tid_produce = 0;
    pthread_t tid_consume = 0;

    if (argc != 2)
    {
        printf("usage: %s <#items>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    g_nItems = MIN(atoi(argv[1]), MAX_N_ITEMS);
    printf("the number of items is %d\n\n", g_nItems);
    /* create 1 producer and 1 consumer */
    pthread_setconcurrency(2);
    pthread_create(&tid_produce, NULL, produce, NULL);
    pthread_create(&tid_consume, NULL, consume, NULL);

    /* wait for 2 threads */
    pthread_join(tid_produce, NULL);
    pthread_join(tid_consume, NULL);

    exit(EXIT_SUCCESS);
}

static void *produce(void *arg)
{
    int index = 0;
    char buff_write[N_BUFF_SIZE] = {0};
    char tmp[N_BUFF_SIZE] = {0};
    int cnt_of_msg = 0;
    FILE *fp = fopen("produce_single.log", "w");
    while (true)
    {
        if (cnt_of_msg >= g_nItems)
        {
            g_exit = true;
            sprintf(tmp, "produce thread exit, cnt_of_msg = %d\n", cnt_of_msg);
            fwrite(tmp, sizeof(char), strlen(tmp), fp);
            fclose(fp);
            return NULL;
        }
        sprintf(tmp, "进入缓冲区进行写入缓冲区index=%d\n", index);
        fwrite(tmp, sizeof(char), strlen(tmp), fp);
        snprintf(buff_write, sizeof(buff_write), "Message %d", cnt_of_msg++);
        /* 写入数据 */
        memcpy(&g_queue_data[index].buff, &buff_write, sizeof(buff_write));
        g_queue_data[index].status = stored;
        sprintf(tmp, "%s write\n", buff_write);
        fwrite(tmp, sizeof(char), strlen(tmp), fp);
        index++;
        if (index == BUFFER_QUEUE_LEN) 
        {
            sprintf(tmp, "缓冲区已满,无可用空间\n");
            fwrite(tmp, sizeof(char), strlen(tmp), fp);
            /* g_cnt_of_queue 仅通过消费者线程修改 */
            while (g_cnt_of_queue != 0) // 判断buff队列是否已满,若已满,则等待
            {
                sprintf(tmp, "%%警告:缓冲区还未清空,停止缓存数据\n");
                fwrite(tmp, sizeof(char), strlen(tmp), fp);
                usleep(100);
            }
            index = 0; // 从头开始写入
        }
    }
}

static void *consume(void *arg)
{
    int index = 0;
    char buff_read[N_BUFF_SIZE] = {0};
    char tmp[N_BUFF_SIZE] = {0};
    int cnt_of_wait = 0;
    FILE *fp = fopen("consume_single.log", "w");
    while (true)
    {
        if (g_queue_data[index].status == stored) // 判断buff队列中的第index个元素是否已写入数据
        {
            sprintf(tmp, "进入缓冲区进行读取缓冲区数据index=%d\n", index);
            fwrite(tmp, sizeof(char), strlen(tmp), fp);
            /* 读出数据 */
            memcpy(&buff_read, &g_queue_data[index].buff, sizeof(g_queue_data[index].buff));
            sprintf(tmp, "%s read\n", buff_read);
            fwrite(tmp, sizeof(char), strlen(tmp), fp);
            index++;
            if (index == BUFFER_QUEUE_LEN) // 判断buff队列是否已全部读完
            {
                sprintf(tmp, "缓冲区已全部读取,index=%d,清空此缓冲区\n", index);
                fwrite(tmp, sizeof(char), strlen(tmp), fp);
                index = 0;
                memset(g_queue_data, 0, sizeof(g_queue_data)); // 清空buff队列的空间
            }
            cnt_of_wait = 0;
        }
        else // buff队列没有数据可读,进行等待
        {
            if (g_exit == true && cnt_of_wait >= 10)
            {
                sprintf(tmp, "consume thread exit\n");
                fwrite(tmp, sizeof(char), strlen(tmp), fp);
                fclose(fp);
                return NULL;
            }
            cnt_of_wait++;
            sprintf(tmp, "缓冲区为空或已读,请等待...\n");
            fwrite(tmp, sizeof(char), strlen(tmp), fp);
            usleep(100);
        }
        g_cnt_of_queue = index; // 更新buff队列的cnt
    }
}

验证结果:

当items=250时,查看produce和consume的log文件。比对后,可以实现预期目标。但有一个缺陷,当生产者写入到buff队列最后一个位置后,而消费者还没读到最后一个位置,那么生产者必须等待,直到消费者读出最后一个位置的数据,并将buff队列清空。

(生产者线程被阻塞的告警)

方法二#

生产者和消费者线程的结构体中分别设置 写计数器 cnt_of_wr读计数器 cnt_of_rd ,生产者线程每写入一条数据,并将cnt_of_wr++,当cnt_of_wr和cnt_of_rd的距离到达一个队列的最大长度时,生产者线程就要停下来等待消费者。当cnt_of_wr > cnt_of_rd 时,消费者线程才能读取队列中的数据,并将cnt_of_rd++。

nolock_global_wr.c

/*
 * @Description: mutil-threads implement that no lock design
 * @Author: caojun
 * @version: 
 * @Date: 2023-11-20 12:00:24
 * @LastEditors: caojun
 * @LastEditTime: 2023-11-20 13:00:11
 */

//==============================================================================
// Include files
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include <unistd.h>
#include <pthread.h>

//==============================================================================
// Constants
#define N_BUFF_SIZE 1024     // buff最大长度
#define BUFFER_QUEUE_LEN 100 // buff队列最大长度

#define MAX_N_ITEMS 1000000 // 生产消息最大数目

#define MAX(a,b) ((a)>(b) ? (a):(b)) 
#define MIN(a,b) ((a)<(b) ? (a):(b))

//==============================================================================
// types
/* buff队列结构体 */
struct buff_t
{
    char payload[N_BUFF_SIZE];
};

struct buff_queue_t
{
    struct buff_t buff[BUFFER_QUEUE_LEN];
    long long int cnt_of_wr;
    long long int cnt_of_rd;
};

//==============================================================================
// global varibles
static struct buff_queue_t g_queue_data; // buff队列
//static int g_cnt_of_queue = 0; //buff队列计数器
static int g_nItems = 0;
//static bool g_exit = false;

//==============================================================================
// global functions
static void *produce(void *arg);
static void *consume(void *arg);

//==============================================================================
// The main entry-point function.
int main(int argc, char **argv)
{
    pthread_t tid_produce = 0;
    pthread_t tid_consume = 0;

    if (argc != 2)
    {
        printf("usage: %s <#items>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    g_nItems = MIN(atoi(argv[1]), MAX_N_ITEMS);
    printf("the number of items is %d\n\n", g_nItems);
    /* create 1 producer and 1 consumer */
    pthread_setconcurrency(2);
    pthread_create(&tid_produce, NULL, produce, NULL);
    pthread_create(&tid_consume, NULL, consume, NULL);

    /* wait for 2 threads */
    pthread_join(tid_produce, NULL);
    pthread_join(tid_consume, NULL);

    exit(EXIT_SUCCESS);
}

static void *produce(void *arg)
{
    int index = 0;
    char buff_write[N_BUFF_SIZE] = {0};
    char tmp[N_BUFF_SIZE] = {0};
    long long int diff = 0;
    FILE *fp = fopen("produce_write.log", "w");
    while (true)
    {
        /* 若数据全部生产完毕,则退出生产者线程 */
        if (g_queue_data.cnt_of_wr >= g_nItems)
        {
            fclose(fp);
            return NULL;
        }
        /*若消费者线程比生产者线程计数差值为BUFFER_QUEUE_LEN,则等待消费者线程。*/
        diff = (g_queue_data.cnt_of_wr - g_queue_data.cnt_of_rd) % BUFFER_QUEUE_LEN;
        if (diff >= BUFFER_QUEUE_LEN - 1) 
        {
            sprintf(tmp, "%%警告:生产者线程与消费者线程计数差值过大 %lld\n", diff);
            fwrite(tmp, sizeof(char), strlen(tmp), fp);
            usleep(500);
            continue;
        }

        index = g_queue_data.cnt_of_wr % BUFFER_QUEUE_LEN;
        sprintf(tmp, "进入缓冲区进行写入缓冲区index=%d\n", index);
        fwrite(tmp, sizeof(char), strlen(tmp), fp);
        snprintf(buff_write, sizeof(buff_write), "Message %d", g_queue_data.cnt_of_wr);
        /* 写入数据 */
        memcpy(&g_queue_data.buff[index].payload, &buff_write, sizeof(buff_write));
        sprintf(tmp, "%s write\n", buff_write);
        fwrite(tmp, sizeof(char), strlen(tmp), fp);
        g_queue_data.cnt_of_wr++;

    }
}

static void *consume(void *arg)
{
    int index = 0;
    char buff_read[N_BUFF_SIZE] = {0};
    char tmp[N_BUFF_SIZE] = {0};
    int cnt_of_wait = 0;
    FILE *fp = fopen("consume_read.log", "w");
    while (true)
    {
        /* 若数据全部读取完毕,则退出消费者线程 */
        if (g_queue_data.cnt_of_rd >= g_nItems)
        {
            fclose(fp);
            return NULL;
        }
        /* 当生产者比消费者计数大,消费者才能读出数据 */
        while (g_queue_data.cnt_of_wr > g_queue_data.cnt_of_rd)
        {
            index = g_queue_data.cnt_of_rd % BUFFER_QUEUE_LEN;
            sprintf(tmp, "进入缓冲区进行读取缓冲区数据index=%d\n", index);
            fwrite(tmp, sizeof(char), strlen(tmp), fp);
            /* 读出数据 */
            memcpy(&buff_read, &g_queue_data.buff[index].payload, sizeof(g_queue_data.buff[index].payload));
            sprintf(tmp, "%s read\n", buff_read);
            fwrite(tmp, sizeof(char), strlen(tmp), fp);
            g_queue_data.cnt_of_rd++;
        }
    }
}

验证结果:生产者和消费者线程之间的同步没有问题。但有生产者一个警告:%警告:生产者线程与消费者线程计数差值过大 99,这说明消费者比生产者线程速度慢。

(生产者被阻塞警告)

如果能保证消费者线程速度比生产者快,那么在生产者线程中就不必计算两者的计数器差值,也不阻塞生产者线程。

💡 扩展

若确定消费者线程速度比生产者慢,那么可以设计成 单个生产者线程--多个消费者线程,此时不能再使用一个buff队列,应创建多个buff队列(取决于消费者线程数量)。buff队列结构中设置消费者线程标志,不同的消费者仅能读取与消费者线程标志相关的buff队列。

  • 若数据之间为同一个topic,生产者线程可以将数据均匀写入到不同的buff队列。
  • 若数据之间为不同的topic,生产者线程按照topic区分写入不同的buff队列。

引用#

[1] c语言多线程缓冲队列无锁设计思路

作者:caojun97

出处:https://www.cnblogs.com/caojun97/p/17833784.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

posted @   eiSouthBoy  阅读(132)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗
点击右上角即可分享
微信分享提示
more_horiz
keyboard_arrow_up dark_mode palette
选择主题
menu