Linux线程的创建结束类系统调用总结

 
Category: Linux-Shell

写在前面

总结一下Linux系统的线程创建/终止/等待等系统调用, 参考:

  1. Linux/Unix系统编程手册.

下面主要给出例子, 关于函数原型可以参考书中或者man 7 pthreads.

测试环境: Ubuntu 20.04 x86_64

gcc-9

为什么需要线程?

进程的限制

多进程往往存在如下限制:

  1. 进程间资源难以共享. 因为除去只读代码段, 父子进程并不共享内存, 所以必须采用进程间通信(IPC)的方式交换进程间信息.
  2. 调用fork()创建进程的代价很高. 虽然采用写时复制技术(Copy-on-write), 还是免不了造成资源的浪费.

线程的优势

  1. 线程之间可以方便/快速地共享信息. 只需将数据复制到共享变量(全局变量/堆内存)中即可.

  2. 创建线程比创建进程快10倍以上. Linux上通过clone()系统调用创建线程.

    线程创建调用clone速度快于fork的原因:

    • fork需要复制的很多属性在线程间是共享的.
    • 无需采用写时复制技术复制内存页和页表.

线程创建: pthread_create()

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static void* func(void* arg) {
    char* s = (char*)arg;
    printf("%s", s);
    return (void*)strlen(s);
}


int main(int argc, char* argv[]) {
    pthread_t t1;
    void* res;
    int s;
    s = pthread_create(&t1, NULL, func, "hello pthread\n");
    if (s) fprintf(stderr, "pthread_create");
    
    printf("msg from main():\n");
    
    s = pthread_join(t1, &res);
    if (s) fprintf(stderr, "pthread_join");

    printf("thread returned: %ld\n", (long)res);
    /* msg from main(): */
    /* hello pthread */
    /* thread returned: 14 */

    return 0;
}

线程信息: pthread_self()

线程分离:pthread_detach()

线程同步/互斥量

竞态条件

来看这样的一个例子:

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int glob = 0;

static void* func(void* arg) {
    int loops = *((int*)arg);
    int loc, j;
    // 并非原子操作
    for (j = 0; j < loops; ++j) {
        loc = glob;
        loc++;
        glob = loc;
        // ++glob; // maybe useful
    }
    return NULL;
}


int main(int argc, char* argv[]) {
    pthread_t t1, t2;
    int loops, s;
    loops = 100000;
    s = pthread_create(&t1, NULL, func, &loops);
    if (s) fprintf(stderr, "pthread_create");
    s = pthread_create(&t2, NULL, func, &loops);
    if (s) fprintf(stderr, "pthread_create");
    s = pthread_join(t1, NULL);
    if (s) fprintf(stderr, "pthread_join");
    s = pthread_join(t2, NULL);
    if (s) fprintf(stderr, "pthread_join");

    printf("glob = %d\n", glob);
    // loops = 100000
    /* glob = 200000 */

    // loops = 10000000
    /* glob = 12894835 */

    return 0;
}

在数据量小的时候, 没有问题; 但是当操作次数逐渐增加, 会出现不确定行为, 这是因为操作系统内核CPU调度的不确定性.

在一些系统上, 使用单独的一条语句++glob代替循环中的三条语句可能会解决这个问题, 但是一般来说还是应该使用互斥量(mutex, mutual exclusion)来解决这个问题.

互斥量

通过互斥量(我更喜欢叫互斥锁)的方法可以解决上面的问题.

基本用法

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

static int glob = 0;
// init mutex lock
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;

static void* func(void* arg) {
    int loops = *((int*)arg);
    int loc, j, s;
    /* s = pthread_mutex_lock(&mtx); */
    /* if (s) fprintf(stderr, "pthread_mutex_lock"); */
    for (j = 0; j < loops; ++j) {
        s = pthread_mutex_lock(&mtx);
        if (s) fprintf(stderr, "pthread_mutex_lock");
        loc = glob;
        ++loc;
        glob = loc;
        s = pthread_mutex_unlock(&mtx);
        if (s) fprintf(stderr, "pthread_mutex_unlock");
    }
    /* s = pthread_mutex_unlock(&mtx); */
    /* if (s) fprintf(stderr, "pthread_mutex_unlock"); */
    return NULL;
}


int main(int argc, char* argv[]) {
    pthread_t t1, t2;
    int loops, s;
    loops = 100000000;
    s = pthread_create(&t1, NULL, func, &loops);
    if (s) fprintf(stderr, "pthread_create");
    
    s = pthread_create(&t2, NULL, func, &loops);
    if (s) fprintf(stderr, "pthread_create");
    
    s = pthread_join(t1, NULL);
    if (s) fprintf(stderr, "pthread_join");
    
    s = pthread_join(t2, NULL);
    if (s) fprintf(stderr, "pthread_join");

    printf("glob = %d\n", glob);
    // after locked:
    /* glob = 20000000 */

    return 0;
}
// Lock outside the for loop
/* glob = 200000000 */
/*  */
/* real    0m0.896s */
/* user    0m0.889s */
/* sys     0m0.000s */

// inside:
/* glob = 200000000 */
/*  */
/* real    0m18.602s */
/* user    0m22.255s */
/* sys     0m13.218s */

可以看出, 两种加锁方式的耗时区别相当大, 所以一定要在合理位置加锁, 提高资源使用率.

死锁

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// init mutex lock
pthread_mutex_t mtx1 = PTHREAD_MUTEX_INITIALIZER,
                mtx2 = PTHREAD_MUTEX_INITIALIZER;

static void* func1(void* arg) {
    pthread_mutex_lock(&mtx1);
    char* s = (char*)arg;
    printf("%s", s);
    pthread_mutex_lock(&mtx2);
    return (void*)strlen(s);
}

static void* func2(void* arg) {
    pthread_mutex_lock(&mtx2);
    char* s = (char*)arg;
    printf("%s", s);
    pthread_mutex_lock(&mtx1);

    return (void*)strlen(s);
}

int main(int argc, char* argv[]) {
    pthread_t t1, t2;
    void* res;
    int s;
    s = pthread_create(&t1, NULL, func1, "hello pthread1\n");
    if (s) fprintf(stderr, "pthread_create");

    s = pthread_create(&t2, NULL, func2, "hello pthread2\n");
    if (s) fprintf(stderr, "pthread_create");

    printf("msg from main():\n");

    s = pthread_join(t1, &res);
    if (s) fprintf(stderr, "pthread_join");

    s = pthread_join(t2, &res);
    if (s) fprintf(stderr, "pthread_join");

    printf("thread returned: %ld\n", (long)res);

    return 0;
}

动态初始化互斥量

静态初始值PTHREAD_MUTEX_INITIALIZER只能用于经由静态分配且携带默认属性的互斥量的初始化.

其他情况下, 必须调用pthread_mutex_init()对互斥量进行动态初始化. 这些情况包括:

  1. 动态分配于堆中的互斥量(例如链表中的每一个节点)
  2. 互斥量在栈中分配的自动变量.
  3. 初始化经由静态分配, 但是不使用默认属性的互斥量
#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destory(pthread_mutex_t *mutex); // 销毁
  • 返回0: 调用成功
  • 返回正值: 表示错误码(errno)

条件变量

为了解决互斥量的一些缺点(例如需要一直等待, )而产生, 看下面的例子:

由若干线程生成一些产品单元供主线程消费(生产者/消费者模型), 使用一个由互斥量保护的变量avail代表待消费产品的数量.