关于线程

40 阅读4分钟

线程是操作系统调度器可以调度的最小执行单元。一个进程包含一个或多个线程。同一进程中的多个线程共享进程的内存地址空间。线程间切换的代价要比进程小的多,因为线程是在进程内切换的。
多线程同时执行可以实现并发,提高系统的吞吐量,也可以提高响应能力。比如,顺序执行的程序可能会因为等待输入一直阻塞下去,而多线程则可以分配一个线程进行等待,其他线程继续执行其他任务,从而提高效率。

创建线程

#include <pthread.h>
int pthread_create(pthread_t *thread, 
                   const pthread_attr_t *attr,
                   void *(*start_routine)(void *),
                   void *arg);

thread保存新创建的线程ID,attr用来设置新创建线程的属性,start_routinearg分别为执行新线程将运行的函数及参数
start_routine必须包含如下特征

void *start_thread(void *arg);

创建成功返回0,失败返回error number

示例:

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

void *work(void *arg) {
    printf("work function\n");
    printf("arg is %d", *(int *)arg);
    return NULL;
}

int main() {
    pthread_t tid;
    int arg = 666;
    int ret = pthread_create(&tid, NULL, work, (void *)&arg);
    if (ret < 0) {
        perror("pthread_create");
        exit(1);
    }
    printf("new thread id is %ld\n", tid);
    sleep(1);
    return 0;
}

pthread_create用来创建一个线程,属性为默认,线程转去执行work函数,传入一个参数,主线程通过sleep等待work执行完成最后结束。这里有一个技巧,如果work需要传入多个参数,可以封装一个结构体,传入整个结构体实现。
编译时需要添加-lpthread

退出线程

#include <pthread.h>
void pthread_exit(void *retval);

函数通过retval传递信息给调用者。不会返回调用者,而且永远不会失败。

join线程

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

函数阻塞等待tid为thread的线程结束,前提是目标线程是可回收的,retval保存线程退出状态。如果被等待线程已经结束,函数立即返回。执行成功返回0,失败返回error number

detach线程

#include <pthread.h>
int pthread_detach(pthread_t thread);

默认情况下,线程是创建成可join的。但是,线程也可以detach(分离),使得线程不可join。当分离的线程终止时,它的资源会自动释放回收,而不需要另一个线程join等待。成功时,pthread_detach会分离thread指定的线程,并返回0。

终止线程

#include <pthread.h>
int pthread_cancel(pthread_t thread);

函数可以通过指定thread来终止线程
不过,接收到取消请求的目标线程可以决定是否允许被取消以及如何取消,分别由两个函数完成:

int pthread_setcancelstate(int state, int *oldstate);
int pthread_setcanceltype(int type, int *oldtype);

这两个函数的第一个参数分别用于设置线程的取消状态(是否允许取消)和取消类型(如何取消),第二个参数分别记录线程原来的取消状态和取消类型。

以上是关于线程的一些基本函数,下面看几个示例

一,创建一个线程,执行work函数,传入一个地址,线程执行函数先睡眠两秒,再继续执行。

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

void *work(void *arg) {
    sleep(2); //睡眠两秒
    printf("gaoyuelong is %d years old!\n", *(int *)arg);
    return NULL;
}

int main() {
    pthread_t tid;
    int arg = 18;
    int ret = pthread_create(&tid, NULL, work, (void *)&arg);
    if (ret < 0) {
        perror("pthread_create");
        exit(1);
    }
    arg = 19;

    pthread_join(tid, NULL);
    return 0;
}

程序运行,预期得到gaoyuelong is 18 years old!,而实际输出为19岁,因为在线程函数睡眠的时候,参数地址中的内容已被修改。因此,实际得到与预期不符。而对于此类问题可以设置一个缓存区来保存信息,传参时传入缓存区的地址,或者直接传入值而不是地址。

二、创建两个线程,执行同一个work函数。

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

void *work(void *arg) {
    printf("%s\n", (char *)arg);
    return NULL;
}

int main() {
    pthread_t tid1, tid2;
    char *msg1= "msg1";
    char *msg2 = "msg2";

    pthread_create(&tid1, NULL, work, (void *)msg1);
    pthread_create(&tid2, NULL, work, (void *)msg2);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

程序输出

msg2
msg1

msg1
msg2

两个线程执行顺序是不确定的,导致输出结果会不同,但printf并没有输出异常的信息,因为它是个线程安全的函数,也就是并没有发生竞争。

一个函数被称为线程安全的,当且仅当被多个并发线程反复地调用时,它会一直产生正确的结果。如果一个函数不是线程安全的,我们就说它是线程不安全的

三、既然线程是共享同一进程内存空间,那么给定一个num,我们希望开两个线程,对这一个变量累加到100。

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

int num = 0;

void *work() {
    while (1) {
        if (num >= 100break;
        num++;
        printf("num = %d\n", num);
    }
    return NULL;
}

int main() {
    pthread_t tid1, tid2;

    pthread_create(&tid1, NULL, work, NULL);
    pthread_create(&tid2, NULL, work, NULL);

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);

    return 0;
}

两个线程执行顺序是不确定的,因此可能会出现同一个值被加了两次的情况,因而数据发生错误。这时就需要用到线程同步,确保数据在某一时刻只能被一个线程访问,其他线程来了只能先等待。关于线程同步,将在下一篇总结。

多线程的程序可以提高效率,但随之而来的是编程的复杂性,从而也就导致了程序难懂,较难调试。

参考资料
[1] 游双.Linux高性能服务器编程[M].北京:机械工业出版社,2013.
[2] 龚奕利,贺莲译.深入理解计算机系统[M].北京:机械工业出版社,2016.
[3] 祝洪凯,李妹芳,付途译.Linux系统编程[M].北京:人民邮电出版社,2014.