由于我懒得写完,而且懂的也不是很多,本文不会深入各个多线程库,不会涉及具体的高并发,原子操作等复杂内容,本文仅描述了C语言中多线程技术的基本使用方法~~,适合新人。~~
由于threads.h与pthread.h定位冲突且资料较少,我没有对thread.h深究,仅仅列出部分宏/函数表
实际上posix环境下的多线程库(还有C++的threads)都大差不差,思路都是互通的,只有Windows是个异类,所以本文重心在于pthread和Windows API的CreateThread。
阅读本文需要一定的基础。
现代操作系统通常以进程(process)作为CPU资源调度的最小单位,其实际上是线程的容器,而线程则是程序指令以及其资源的容器,从这样的角度,我们可以这样说:线程即是“程序”。
原因是多种多样的,扯远一点,由于摩尔定律的失效(CPU单核性能在短期内无法取得决定性的进步),红蓝两家都在往CPU上堆核心,广大程序员无法享用单核红利。而着眼于手边,你可能会为了实现一个好看的读条界面而转向超线程,或者可能是为了在读取文件的同时输出日志,或者仅仅是想给自己的CPU来一次压力测试…
高效利用你的CPU核心的方式多种多样,其中最朴实无华(但实际上也能复杂得让人头痛)的便是多线程技术。ISO C 11提供了一个标准库头文件threads.h以提供多线程功能(但截至目前,Visual Studio未提供相应实现),在posix环境中也可以使用pthread.h,对于Windows环境,也能使用Windows API来管理线程。
学会创建一个线程
学会管理线程
学会使用互斥锁缓解线程安全问题
…
实际上,多线程是一个大坑,非常坑,远远不止上述内容。
如果要深究,还得涉及到操作系统层面的调度,甚至是硬件层面的乱序发射。
使用pthread需要链接pthread.so(或windows上的libwinpthread),mingw-w64 gcc若使用posix线程模型则默认链接该库,而linux上的gcc则需要手动配置
# 手动编译示例
gcc main.c -o app -lpthread
# CMake示例
add_executable(learning_pthread)
target_sources(learning_pthread PUBLIC main.c)
target_link_libraries(learning_pthread PUBLIC pthread)
pthread.h的内容几乎都以pthread_开头,请看如下示例代码:
#include
#include
#include
static void say(const char* str)
{
while(true) puts(str);
}
int main(void)
{
pthread_t t1;
pthread_t t2;
// 原型为 pthread_create(pthread_t *restrict newthread, const pthread_attr_t *restrict attr, void *(*start_routine)(void *), void *restrict arg)
// 此处第四个参数进行了隐式转换
pthread_create(&t1, NULL, say, "hello world!");
pthread_create(&t2, NULL, say, "pthread is awesome!");
// 非阻塞
pthread_detach(t1);
// 阻塞当前线程,可以指定返回值到何处,但此处不需要,故设为NULL
pthread_join(t2,NULL);
while(true) puts("if you see this , the main thread is still alive!");
return 0;
}
// 另外,你可以使用如下函数强行终止一个线程
//void pthread_exit(void *retval);
// 你还可以使用下述函数获得当前线程标识
//pthread_t pthread_self(void);
编译并运行上述代码,你应该能发现“hello world”和“pthread is awesome”交替输出,但是一者输出一段时间后换为另一者进行输出,这是因为每个线程都分配到了一定的CPU时间片,当自己的时间片用尽时才会让出CPU,让其他线程执行,线程其实并没有同步执行,只是高速切换。
最后的“if you see this , the main thread is still alive!”是不会被输出的,因为主线程执行到“pthread_join(t2,NULL);”时就被堵塞了。
pthread中也提供了互斥锁以缓解线程安全问题,和其他的多线程库一样,其操作总结下来无非就是
创建mutex
对关键IO操作上锁
完成关键IO操作后解锁
放到代码中就像是这样:
pthread_mutex_t mut;
void counter(void* args)
{
int i = 1;
while (i <= 10000 / 4)
{
// 对非本线程资源的读写,可能存在冲突,故上锁
pthread_mutex_lock(&mut);
g_number++;
pthread_mutex_unlock(&mut);
i++;
}
}
这种直接上锁的方式尽管简单易用,但却实际上使程序的IO部分退化回了单线程,造成了一定的性能损失。而想要解决这个问题,我们就得来谈谈“原子操作”了。
原子操作是一种复杂且经过数次迭代的机制,简单来说,它是一个不可中断的,一系列的操作,它不会被线程调度机制打断,也不会有任何的上下文切换。使用原子操作,能完美解决传统mutex锁策略所带来的性能损失,同时保证线程安全。
你可能好奇如此神奇的机制到底是如何实现的,其实,时至今日,原子操作早已不单是标准库中的一套概念,而是整合进了CPU的一套指令集。原子操作的底层是由CPU硬件实现的。
在ISO C 11中标准库提供了stdatomic.h,我们可以用它来实现原子操作。
// 以下代码截取自zh.cppreference.com
#include
#include
#include
atomic_int acnt;
int cnt;
int f(void* thr_data)
{
for(int n = 0; n < 1000; ++n) {
++cnt;
++acnt;
// 对于此例,宽松内存顺序是足够的,例如
// atomic_fetch_add_explicit(&acnt, 1, memory_order_relaxed);
}
return 0;
}
int main(void)
{
thrd_t thr[10];
for(int n = 0; n < 10; ++n)
thrd_create(&thr[n], f, NULL);
for(int n = 0; n < 10; ++n)
thrd_join(thr[n], NULL);
printf("The atomic counter is %u\n", acnt);
printf("The non-atomic counter is %u\n", cnt);
}
其实在我看来,使用Windows API是迫不得已的下下策,但无可否认的是,如此的”迫不得已“,实在是太普遍了。
Windows API有个特色,就是参数列表又臭又长,如下:
// 创建线程
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
DWORD dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadID
);
// 其实这还不是最长的(小声
参数 | 描述 |
---|---|
lpThreadAttrivutes | 用于定义新线程的安全属性,一般为NULL |
dwStackSize | 线程堆栈的大小,单位字节,默认为0 |
lpStartAddress | 线程函数地址,即此线程需要执行的函数 |
lpParameter | 传给线程函数的参数 |
dwCreationFlags | 创建线程的运行状态,CREATE_SUSPEND表示挂起,0表示立即执行 |
lpThreadID | 回新创建的线程的ID |
// 等待线程结束,其实也可以是等待其他东西
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
参数 | 描述 |
---|---|
hHandle | 对象或时间的句柄 |
dwMilliseconds | 最长等待时间,单位毫秒,若超过此最长时间则强制返回,亦可设为INFINITE,然后等待戈多(不 |
Windows API也提供了与mutex锁类似的机制来缓解线程安全问题,叫”临界区“
其使用步骤大体为:
创建临界区: CRITICAL_SECTION cs
初始化临界区:InitializeCriticalSection(&cs)
在关键IO前进入临界区:EnterCriticalSection(&cs);
关键IO结束后退出临界区:LeaveCriticalSection(&cs);
下面是一端演示
int i = 0;
CRITICAL_SECTION cs;
DWORD WINAPI func(LPVOID parament)
{
EnterCriticalSection(&cs);
// certain IO
i++;
LeaveCriticalSection(&cs);
return 0;
}
int main(void)
{
HANDLE thread[2];
DWORD ret1;
DWORD ret2;
InitializeCriticalSection(&cs);
// 两个线程都设置为立即执行,从效果上说和pthread_detach类似
thread[0] = CreateThread(NULL, 0, func, NULL, NULL, NULL);
thread[1] = CreateThread(NULL, 0, func, NULL, NULL, NULL);
WaitForMultipleObjects(2, thread, TRUE, INFINITE);
CloseHandle(harrThread[0]);
CloseHandle(harrThread[1]);
return 0;
}
thread.h在ISO C 11标准后加入,请确保你的编译器支持且启用了ISO C 11标准。另外,Visual Studio尚未支持ISO C 11的threads.h,因为他们从来不关心C语言标准。
threads.h的函数均以thrd_开头,常用内容如下
标识符/原型 | 类型 | 描述 |
---|---|---|
thrd_t | 宏 | 用于存放线程的相关数据,可以当成线程对象来使用 |
int thrd_create(thrd_t *thr, thrd_start_t func, void *arg); | 函数 | 创建线程并指定其执行的函数 |
int thrd_detach(thrd_t thr); | 函数 | 将子线程与主线程分离,使其与主线程并行 |
int thrd_equal(thrd_t thr0, thrd_t thr1); | 函数 | 判断两个线程标识符是否标识同一线程 |
void thrd_exit(int res) | 函数 | 强制结束某线程 |
int thrd_join(thrd_t thr, int *res) | 函数 | 注意:我查到的资料中的描述和我实际测试的结果不同,我现有的资料都说此函数会阻塞当前线程,但实际测试结果并未阻塞,存疑。 |
void thrd_sleep(const xtime *xt) | 函数 | 使当前线程休眠指定时间 |
void thrd_yield(void) | 函数 | 挂起当前线程,让出CPU资源 |
(不分先后,随机排序)
C11新增多线程支持库-threads.h参考手册 – 坏蛋的博客 (ibadboy.net)
windows API(9)线程安全 临界区_飘零的落花的博客-CSDN博客
使用CreateThread函数创建线程_Valineliu的博客-CSDN博客_createthread函数
C语言多线程编程(一) - 知乎 (zhihu.com)
原子类型 - cppreference.com
原子操作_百度百科 (baidu.com)
什么是原子操作_辽宁大学的博客-CSDN博客_原子操作