如何在纯C中启动线程?

61

我在C语言中使用了fork()来启动另一个进程。那么我如何启动一个新线程?


这取决于平台。以下是在Windows上执行此操作的方法:http://msdn.microsoft.com/en-us/library/ms682453.aspx - Eric Z Beard
正如您提供的CreateThread文档所解释的那样,如果程序使用C运行时库,则应使用_beginthreadex而不是CreateThread。 - ChrisN
谢谢,我没有注意到。你怎么知道你在使用 CRT?我以为所有东西都在使用它。 - Eric Z Beard
你可以告诉编译器不要包含默认库。这样一来,你就不会意外使用 CRT,并且可以完全使用 Win32 函数。 - Zan Lynx
6个回答

66

因为您提到了fork(),我认为您正在使用类Unix系统,在这种情况下,POSIX线程(通常称为pthread)是您要使用的。

具体来说,pthread_create()是创建新线程所需的函数。其参数为:

int  pthread_create(pthread_t  *  thread, pthread_attr_t * attr, void *
   (*start_routine)(void *), void * arg);

第一个参数是线程ID的返回指针。第二个参数是线程参数,可以为NULL,除非你想以特定优先级启动线程。第三个参数是由线程执行的函数。第四个参数是在线程函数执行时传递给该函数的单个参数。


1
现在C11标准定义了POSIX线程,那么它们不再被视为“纯C”吗? - Hydronium
2
尽管C11多线程API受到POSIX线程的_严重_影响,但它们并不相同。因此,我认为说POSIX线程是“纯C”的并不合适。 - Ben Ylvisaker

17

据我所知,ANSI C没有定义线程,但是有各种可用的库。

如果您正在运行Windows操作系统,请链接到msvcrt并使用_beginthread或_beginthreadex。

如果您正在运行其他平台,请查看pthreads库(我相信还有其他库可供选择)。


17

C11线程+C11 atomic_int

添加到glibc 2.28。在Ubuntu 18.10 amd64(带有glic 2.28)和 Ubuntu 18.04(带有glibc 2.27)中测试,通过从源代码编译glibc 2.28:多个主机上的多个glibc库

示例改编自:https://en.cppreference.com/w/c/language/atomic

main.c

#include <stdio.h>
#include <threads.h>
#include <stdatomic.h>

atomic_int atomic_counter;
int non_atomic_counter;

int mythread(void* thr_data) {
    (void)thr_data;
    for(int n = 0; n < 1000; ++n) {
        ++non_atomic_counter;
        ++atomic_counter;
        // for this example, relaxed memory order is sufficient, e.g.
        // atomic_fetch_add_explicit(&atomic_counter, 1, memory_order_relaxed);
    }
    return 0;
}

int main(void) {
    thrd_t thr[10];
    for(int n = 0; n < 10; ++n)
        thrd_create(&thr[n], mythread, NULL);
    for(int n = 0; n < 10; ++n)
        thrd_join(thr[n], NULL);
    printf("atomic     %d\n", atomic_counter);
    printf("non-atomic %d\n", non_atomic_counter);
}

GitHub 上游

编译并运行:

gcc -ggdb3 -std=c11 -Wall -Wextra -pedantic -o main.out main.c -pthread
./main.out

可能的输出:

atomic     10000
non-atomic 4341

由于在非原子操作变量上跨线程进行竞争访问,非原子计数器很可能比原子计数器小。

另请参见:如何在C中执行原子增量和提取?

反汇编分析

使用以下命令进行反汇编:

gdb -batch -ex "disassemble/rs mythread" main.out
包含:
17              ++non_atomic_counter;
   0x00000000004007e8 <+8>:     83 05 65 08 20 00 01    addl   $0x1,0x200865(%rip)        # 0x601054 <non_atomic_counter>

18              __atomic_fetch_add(&atomic_counter, 1, __ATOMIC_SEQ_CST);
   0x00000000004007ef <+15>:    f0 83 05 61 08 20 00 01 lock addl $0x1,0x200861(%rip)        # 0x601058 <atomic_counter>

因此,我们可以看到原子增量是在指令级别上使用f0锁定前缀完成的。

使用aarch64-linux-gnu-gcc 8.2.0时,我们得到的结果是:

11              ++non_atomic_counter;
   0x0000000000000a28 <+24>:    60 00 40 b9     ldr     w0, [x3]
   0x0000000000000a2c <+28>:    00 04 00 11     add     w0, w0, #0x1
   0x0000000000000a30 <+32>:    60 00 00 b9     str     w0, [x3]

12              ++atomic_counter;
   0x0000000000000a34 <+36>:    40 fc 5f 88     ldaxr   w0, [x2]
   0x0000000000000a38 <+40>:    00 04 00 11     add     w0, w0, #0x1
   0x0000000000000a3c <+44>:    40 fc 04 88     stlxr   w4, w0, [x2]
   0x0000000000000a40 <+48>:    a4 ff ff 35     cbnz    w4, 0xa34 <mythread+36>

因此,原子版本实际上具有一个cbnz循环,该循环运行直到stlxr存储成功为止。请注意,ARMv8.1可以使用单个LDADD指令完成所有操作。

这类似于C++的std::atomicWhat exactly is std::atomic?

基准测试

待办事项。创建基准测试以显示原子操作较慢。

POSIX线程

main.c

#define _XOPEN_SOURCE 700
#include <assert.h>
#include <stdlib.h>
#include <pthread.h>

enum CONSTANTS {
    NUM_THREADS = 1000,
    NUM_ITERS = 1000
};

int global = 0;
int fail = 0;
pthread_mutex_t main_thread_mutex = PTHREAD_MUTEX_INITIALIZER;

void* main_thread(void *arg) {
    int i;
    for (i = 0; i < NUM_ITERS; ++i) {
        if (!fail)
            pthread_mutex_lock(&main_thread_mutex);
        global++;
        if (!fail)
            pthread_mutex_unlock(&main_thread_mutex);
    }
    return NULL;
}

int main(int argc, char **argv) {
    pthread_t threads[NUM_THREADS];
    int i;
    fail = argc > 1;
    for (i = 0; i < NUM_THREADS; ++i)
        pthread_create(&threads[i], NULL, main_thread, NULL);
    for (i = 0; i < NUM_THREADS; ++i)
        pthread_join(threads[i], NULL);
    assert(global == NUM_THREADS * NUM_ITERS);
    return EXIT_SUCCESS;
}

编译并运行:

gcc -std=c99 -Wall -Wextra -pedantic -o main.out main.c -pthread
./main.out
./main.out 1

第一次运行正常,第二次由于缺少同步而失败。

似乎没有POSIX标准化的原子操作:UNIX 可移植原子操作

在Ubuntu 18.04上进行了测试。GitHub 上游

GCC __atomic_* 内置函数

对于那些没有C11的人,可以通过使用__atomic_* GCC扩展来实现原子递增。

main.c

#define _XOPEN_SOURCE 700
#include <pthread.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>

enum Constants {
    NUM_THREADS = 1000,
};

int atomic_counter;
int non_atomic_counter;

void* mythread(void *arg) {
    (void)arg;
    for (int n = 0; n < 1000; ++n) {
        ++non_atomic_counter;
        __atomic_fetch_add(&atomic_counter, 1, __ATOMIC_SEQ_CST);
    }
    return NULL;
}

int main(void) {
    int i;
    pthread_t threads[NUM_THREADS];
    for (i = 0; i < NUM_THREADS; ++i)
        pthread_create(&threads[i], NULL, mythread, NULL);
    for (i = 0; i < NUM_THREADS; ++i)
        pthread_join(threads[i], NULL);
    printf("atomic     %d\n", atomic_counter);
    printf("non-atomic %d\n", non_atomic_counter);
}

编译并运行:

gcc -ggdb3 -O3 -std=c99 -Wall -Wextra -pedantic -o main.out main.c -pthread
./main.out

输出和生成的汇编:与“C11 线程”示例相同。

在 Ubuntu 16.04 amd64、GCC 6.4.0 中测试通过。


7

pthreads是一个很好的开始,看看这里


7

线程不是C标准的一部分,因此使用线程的唯一方法是使用某个库(例如:在Unix/Linux中使用POSIX线程,在该线程中使用C运行时的_beginthread/_beginthreadex,或者只是使用CreateThread Win32 API)


2

请查看pthread(POSIX线程)库。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接