一、背景
想法源于微信、QQ、蓝信抢红包的热情,内部是怎么实现分配处理的呢?
对于单机的情况,是否可以使用多线程去模拟多个用户同时去抢红包?
二、相关知识
大概查找了一下相关的资料[1][2],我理解红包软件实现的主要难点是在存储、分配这两块;存储解决数据原子性的问题、分配则解决先后抢包期望值一致的问题;
2.1 并发操作
“
用户在微信中抢红包时分成抢包和拆包两个操作。抢包决定红包是否还有剩余金额,但如果行动不够迅速,在拆包阶段可能红包已经被其他用户抢走的情况。”[1];
解决方法是用的CAS去保证并发抢包下的数据原子性(多客户端多个机器的情况下),在本文的多线程模拟中,其实就可以使用线程锁去保证数据的原子性,防止出现1份红包分别被2个人同时拥有;
2.2 金额分配
“
红包的金额是拆的时候实时计算,而不是预先分配,实时计算基于内存,不需要额外存储空间,并且实时计算效率也很高。每次拆红包时,系统取0.01到剩余平均值*2之间作为红包的金额。"[1];
把红包做成份数去理解会比较清楚一些,红包的当前状态可以表示为 [份数, 金额],如[10,100] 表示当前为10人份总额100元的红包,当客户端拿到 [10,100] 这个状态后,根据seed去获取0.01份~1.99份的当前红包(非最后一份红包的情况);
在实际的微信实现中,这个seed应该就是跟红包id、用户id相关的数据吧,在多线程模拟下,简单起见,直接就用默认的seed就行了;
2.3 相关接口
互斥锁初始化:pthread_mutex_init(pthread_mutex_t *mutex,const pthread_mutexattr_t *attr);
上锁:pthread_mutex_lock(pthread_mutex_t *mutex);
解锁:pthread_mutex_unlock(pthread_mutex_t *mutex);
条件变量初始化:pthread_cond_init(pthread_cond_t *cond,const pthread_cond_t *attr);
线程挂起等待:pthread_cond_wait(pthread_cond_t *cond,pthread_mutex_t *mutex);
唤醒单个:pthread_cond_signal(pthread_cond_t *cond);
全部唤醒:pthread_cond_broadcast(pthread_cond_t *cond);
三、实现
定义部分,instance_t 为程序实例结构体,里面放置线程属性、互斥锁、条件量,
item_t 结构体表示红包条目,成员num为当前红包份数,tot表示当前红包总金额,单位是分;
#define THREAD_NUM 10
#define __RAND(min, max) (rand() % ((max) - (min)) + min)
typedef struct instance
{
pthread_attr_t attr;
pthread_mutex_t mutex;
pthread_cond_t cond;
} instance_t;
typedef struct item
{
int num;
int tot;
} item_t;
static item_t item = {0};
主线程为生产线程由stdin进行控制发红包,发出红包后通过条件变量广播唤醒所有子线程(10个工作子线程负责抢红包)
int main ()
{
int ret = FAILURE;
int ix = 0;
int num = 0;
int tot = 0;
instance_t inst = {0};
pthread_t tid;
ASSERT(SUCCESS, ret = pthread_mutex_init(&inst.mutex, NULL));
ASSERT(SUCCESS, ret = pthread_cond_init(&inst.cond, NULL));
ASSERT(SUCCESS, ret = pthread_attr_init(&inst.attr));
ASSERT(SUCCESS, ret = pthread_attr_setschedpolicy(&inst.attr, SCHED_OTHER));
for ( ix = 0; ix < THREAD_NUM; ix++ ) {
ASSERT(SUCCESS, ret = pthread_create(&tid, &inst.attr, __worker, &inst));
}
while ( 1 ) {
printf("Input: < number > < money > \n");
if ( fscanf(stdin, "%d %d", &num, &tot) == EOF ) {
break;
}
if ( num < 0 || num > 10 ) {
printf("Package number out of range: [1, 10]\n");
continue;
}
else if ( tot < 0 || tot > 200 ) {
printf("Total money out of range: [1, 200]\n");
continue;
}
pthread_mutex_lock(&inst.mutex);
item.num = num;
item.tot = tot * 100;
printf("Init: [%d, %d.%02d]\n",
item.num, item.tot / 100, item.tot % 100);
pthread_cond_broadcast(&inst.cond);
pthread_mutex_unlock(&inst.mutex);
sleep(1);
}
ASSERT(SUCCESS, ret = pthread_attr_destroy(&inst.attr));
ASSERT(SUCCESS, ret = pthread_cond_destroy(&inst.cond));
ASSERT(SUCCESS, ret = pthread_mutex_destroy(&inst.mutex));
_E1:
return EXIT_SUCCESS;
}
对于红包操作为共享资源,所以得用线程锁进行保护,各个子线程抢完红包后挂起休眠;
static void *__worker(void *args)
{
int money = 0;
instance_t *pinst = (instance_t *)args;
if ( !args ) {
return NULL;
}
printf("Start thread: %u\n", (u32)pthread_self());
while ( 1 ) {
pthread_mutex_lock(&pinst->mutex);
pthread_cond_wait(&pinst->cond, &pinst->mutex);
if ( item.num <= 0 ) {
printf("Thread #%d get %d.%02d, left [%d, %d.%02d]\n",
(u32)pthread_self(),
0, 0, 0, 0, 0);
pthread_mutex_unlock(&pinst->mutex);
continue;
}
else if ( item.num == 1 ) {
money = item.tot;
}
else {
/* roll is 0.01 ~ 1.99 */
money = item.tot * __RAND(1, 199) / 100 / item.num;
}
item.tot -= money;
item.num--;
printf("Thread #%d get %d.%02d, left [%d, %d.%02d]\n",
(u32)pthread_self(),
money / 100, money % 100, item.num, item.tot / 100, item.tot % 100);
pthread_mutex_unlock(&pinst->mutex);
}
printf("Stop thread: %u", (u32)pthread_self());
return NULL;
}
四、总结
本文通过一生产者多消费者的多线程编程去模拟微信抢红包的过程;
从执行结果上看,10个线程同时抢红包,每次发5个红包抢到的结果均是比较贴近手气红包的规律;
而且有个非常有趣的现象,每次居然都是前5个线程手快抢到了红包!修改了线程优先级也是一样的结果,所以猜测与广播唤醒的顺序机制有关;
参考文章:
[1] 微信红包金额分配的算法,http://www.open-open.com/lib/view/open1430473257443.html
[2] 微信红包的架构设计简介,https://www.zybuluo.com/yulin718/note/93148
[3] linux下条件变量、线程锁的使用,http://www.cppblog.com/converse/archive/2009/01/15/72064.aspx