C语言中pthread或Windows API在多线程编程中的基本应用

文章目录

  • 多线程概述
  • 掌握多线程需要学习什么?
  • 使用pthread.h实现多线程
  • 使用Windows API实现多线程
  • 使用threads.h实现多线程
  • 参考资料

警告

  1. 由于我懒得写完,而且懂的也不是很多,本文不会深入各个多线程库,不会涉及具体的高并发,原子操作等复杂内容,本文仅描述了C语言中多线程技术的基本使用方法~~,适合新人。~~

  2. 由于threads.h与pthread.h定位冲突且资料较少,我没有对thread.h深究,仅仅列出部分宏/函数表

  3. 实际上posix环境下的多线程库(还有C++的threads)都大差不差,思路都是互通的,只有Windows是个异类,所以本文重心在于pthread和Windows API的CreateThread。

  4. 阅读本文需要一定的基础。

多线程概述

现代操作系统通常以进程(process)作为CPU资源调度的最小单位,其实际上是线程的容器,而线程则是程序指令以及其资源的容器,从这样的角度,我们可以这样说:线程即是“程序”。

  • 我们为什么要使用多线程?

原因是多种多样的,扯远一点,由于摩尔定律的失效(CPU单核性能在短期内无法取得决定性的进步),红蓝两家都在往CPU上堆核心,广大程序员无法享用单核红利。而着眼于手边,你可能会为了实现一个好看的读条界面而转向超线程,或者可能是为了在读取文件的同时输出日志,或者仅仅是想给自己的CPU来一次压力测试…

  • 如何获得多核红利?

高效利用你的CPU核心的方式多种多样,其中最朴实无华(但实际上也能复杂得让人头痛)的便是多线程技术。ISO C 11提供了一个标准库头文件threads.h以提供多线程功能(但截至目前,Visual Studio未提供相应实现),在posix环境中也可以使用pthread.h,对于Windows环境,也能使用Windows API来管理线程。

掌握多线程需要学习什么?

  1. 学会创建一个线程

  2. 学会管理线程

  3. 学会使用互斥锁缓解线程安全问题

实际上,多线程是一个大坑,非常坑,远远不止上述内容。

如果要深究,还得涉及到操作系统层面的调度,甚至是硬件层面的乱序发射。

使用pthread.h实现多线程

使用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中也提供了互斥锁以缓解线程安全问题,和其他的多线程库一样,其操作总结下来无非就是

  1. 创建mutex

  2. 对关键IO操作上锁

  3. 完成关键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是迫不得已的下下策,但无可否认的是,如此的”迫不得已“,实在是太普遍了。

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锁类似的机制来缓解线程安全问题,叫”临界区“

其使用步骤大体为:

  1. 创建临界区: CRITICAL_SECTION cs

  2. 初始化临界区:InitializeCriticalSection(&cs)

  3. 在关键IO前进入临界区:EnterCriticalSection(&cs);

  4. 关键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;
}

使用threads.h实现多线程

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博客_原子操作

你可能感兴趣的:(c语言)