前言
最近回答了一个C语言多线程的小问题.
大概有些初涉多线程编程的学习者,在未学到 “锁” 这个概念的时候就已经开始写多线程代码。
但有个问题,要先明白,对于没有任何规划的多线程,很容易得到与预想相别万里的结果。
因为逻辑变了,单线程是顺序逻辑,多线程是乱序逻辑,需要人为干涉规划执行顺序。
举例
下面是问题的叙述,在无锁情况(并非可无锁并发的数据结构)导致的乱序:
C语言多线程操作多函数冲突
问题遇到的现象和发生背景
通过移动光标的方式,用两个线程执行两个不同函数分别在某列输出不同的数字。由于线程并发,输出的结果不对
问题代码可以参考链接:C语言多线程操作多函数 冲突
程序调用两个线程,运行两个函数:fun1,fun2,这两个函数分别调用 gotoxy 函数,定位光标,然后打印字符。
单线程顺序执行没有问题,多线程有很大问题。
两个线程会乱序的定位光标,乱序打印,导致这样一种情况:A挪光标,还没打印,B打印了,随后A才打印,或者相反,总之不是想要的结果。
为了梳理程序流程,我们需要加锁,让光标移动和打印原子化。
OK,我们在光标移动前和打印后加锁。
现在的问题是,可以有很多地方加锁,具体放哪里?
答案是放在刚刚要定位光标之前,刚刚打印完毕。
这看似是废话,其实还有有一定道理的。
一旦一个进程持有锁,另外一个进程就要等,我可以粗糙的放在 gotoxy 函数之前和 printf 函数之后,没有问题,但是我们想想,gotoxy 内部的变量初始化及句柄获取要不要占时间,它们要占时间,但是和移动光标没有关系,完全可以先准备出来,等持有锁的线程释放后立刻定位光标。
一句话,先把能干的都干了,只等时机到来。多么哲学。
当然这只是一个最简单的用例,基本没有死锁的风险,也没有恶性数据竞争,属于入门的入门。
以下代码:
#include <pthread.h>
#include <stdio.h>
#include <windows.h>
pthread_mutex_t m;
void gotoxy(short x, short y)
{
COORD pos;
HANDLE hOutput;
pos.X = x;
pos.Y = y;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
pthread_mutex_lock(&m);
SetConsoleCursorPosition(hOutput, pos);
}
void *fun1(void *unused)
{
short t = 0;
while (t < 20)
{
gotoxy(10, t++);
printf("11");
pthread_mutex_unlock(&m);
}
pthread_exit(NULL);
return NULL;
}
void *fun2(void *unused)
{
short t = 0;
while (t < 20)
{
gotoxy(1, t++);
printf("22");
pthread_mutex_unlock(&m);
}
pthread_exit(NULL);
return NULL;
}
int main()
{
pthread_mutex_init(&m, NULL);
pthread_t pthread1;
pthread_t pthread2;
pthread_create(&pthread1, NULL, &fun1, NULL);
pthread_create(&pthread2, NULL, &fun2, NULL);
pthread_join(pthread1, NULL);
pthread_join(pthread2, NULL);
pthread_mutex_destroy(&m);
return 0;
}