多线程编程同步:无锁设计
背景#
合集的前几篇都介绍了多线程的简单实现(锁设计),那么如何实现不带锁的多线程呢?
既然不能通过互斥锁、读写锁、信号量(有名和无名),那么只能通过全局变量标志来同步生产者线程和消费者线程。
实现#
方法一#
生产者线程每次往buff队列中写入一条数据后,需要更新这条数据的状态为: stored
(注:数据的状态是一个枚举类型,仅有两个状态,分别是 empty
和 stored
)。当生产者线程将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队列。
引用#
作者:caojun97
出处:https://www.cnblogs.com/caojun97/p/17833784.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 通过 API 将Deepseek响应流式内容输出到前端
· AI Agent开发,如何调用三方的API Function,是通过提示词来发起调用的吗