linux系统编程-8、线程

Linux系统编程的基础系列文章,随着不断学习会将一些知识点进行更新,前期主要是简单了解和学习,本期主要介绍线程。

线程

线程概念

线程和进程的关系

13、线程
  1. 轻量级进程,也有 PCB,创建线程使用的底层函数和进程是一样的,都是 clone
  2. 从内核里看进程和线程是一样的,都有各自不同的 PCB,但是 PCB中指向内存资源的三级页表是相同的
  3. 可以理解为线程就是寄存器和栈
  4. linux下,线程是最小的执行单位;进程是最小的分配资源单位
14、线程调度

可以发现,CPU为每个进程分配资源,而调度则是根据线程调度器进行调度

线程间共享资源

  1. 文件描述符表
  2. 每种信号的处理方式
  3. 当前工作目录
  4. 用户ID和组ID
  5. 内存地址空间(0~3G)
  6. Text
  7. data
  8. bss
  9. 共享库
15、线程共享资源

这里的Thread stack是用户栈,是不共享的

线程间非共享资源

  1. 线程id(不是lwp,只在进程内有效,为了方便进程进行识别身份)
  2. 处理器现场和栈指针(内核栈)
  3. 独立的栈空间(用户空间栈)
  4. errno变量
  5. 信号屏蔽字
  6. 调度优先级

线程优缺点

优点

  • 提高程序的并发性
  • 开销小,不用重新分配内存
  • 通信和共享数据方便

缺点

  • 线程不稳定(库函数实现的)
  • 线程调试比较困难(gdb支持不好)
  • 线程无法使用unix经典事件,例如信号

线程原语

pthread_create

#include <pthread.h>

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

参数如下:

  1. pthread_t *thread:
    1. 传递一个pthread_t变量地址进来,用于保存新线程的tid(线程ID)
  2. const pthread_attr_t *attr:
    1. 线程属性设置(栈大小,优先级),如使用默认属性,则传NULL
  3. void *(*start_routine) (void *):
    1. 函数指针,指向新线程应该加载执行的函数模块
  4. void *arg:
    1. 指定线程将要加载调用的那个函数的参数

返回值:

  • 成功返回0,失败返回错误号。以前学过的系统函数都是成功返回0,失败返回-1,而错误号保存在全局变量errno中,而pthread库的函数都是通过返回值返回错误号,虽然每个线程也都有一个errno,但这是为了兼容其它函数接口而提供的,pthread库本身并不使用它,通过返回值返回错误码更加清晰。

pthread_self

获取调用线程tid

#include <pthread.h>

pthread_t pthread_self(void);
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <pthread.h>

void *th_fun(void *arg) {
    int *p = (int*)arg;
    printf("Thread PID = %d\n", getpid());
    printf("Thread id = %x\n", (unsigned int)pthread_self());
    printf("Thread *arg = %d\n", *p);
    
    while(1);
    sleep(1);
}

int main(void) {
    pthread_t tid;
    int n = 10; 

    pthread_create(&tid, NULL, th_fun, (void*)&n);

    printf("Main thread is = %x\n", (unsigned int)pthread_self());
    printf("Main child thread id = %x\n", (unsigned int)tid);
    printf("Main PID = %d\n", getpid());
    
    while(1);
    sleep(2);
    return 0;
}
gcc -g -Wall -pthread pthread-test.c -o pthread-test

kudio@ubuntu:~/Study/linux/Thread$ ./pthread-test 
Main thread is = fffb3700
Main child thread id = ff7c0700
Main PID = 65607
Thread PID = 65607
Thread id = ff7c0700
Thread *arg = 10

对正在运行的进程线程进行查看

UID         PID   PPID    LWP  C NLWP STIME TTY          TIME CMD
kudio     65607  62246  65607 99    2 19:56 pts/17   00:00:36 ./pthread-test
kudio     65607  62246  65608 99    2 19:56 pts/17   00:00:36 ./pthread-test

kudio@ubuntu:~$ ps -Lw 65607
   PID    LWP TTY      STAT   TIME COMMAND
 65607  65607 pts/17   Rl+    1:48 ./pthread-test
 65607  65608 pts/17   Rl+    1:48 ./pthread-test
Thread *arg = 10

如果不在第13/27行加上 while(1)以及后面的sleep会导致进程执行到 return 0 后释放,并将线程释放

kudio@ubuntu:~/Study/linux/Thread$ ./pthread-test
Main thread is = 78c9700
Main child thread id = 70d6700
Main PID = 65721

pthread_exit

如果任意一个线程调用了exit或_exit,则整个进程的所有线程都终止,由于从main函数return也相当于调用exit,为了防止新创建的线程还没有得到执行就终止,我们在main函数return之前延时1秒,这只是一种权宜之计,即使主线程等待1秒,内核也不一定会调度新创建的线程执行,下一节我们会看到更好的办法。

#include <pthread.h>
void pthread_exit(void *retval);
void *retval:线程退出时传递出的参数,可以是退出值或地址,如是地址时,不能是线程内部申请的局部地址。

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了

pthread_join

等价于进程中的 wait

#include <pthread.h>

int pthread_join(pthread_t thread, void **retval);

参数如下:

  1. pthread_t thread:回收线程的tid
  2. void **retval:接收退出线程传递出的返回值
  3. 返回值:成功返回0,失败返回错误号

调用该函数的线程将挂起等待,直到id为thread的线程终止。
thread以不同的方法终止,通过pthread_join得到的终止状态是不同的,如下:

  1. thread通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
  2. thread被别的线程调用pthread_cancel(被别的线程的信号)异常终止,retval所指向的单元里存放的是常熟PTHREAD_CANCELED。
  3. thread是自己调用pthread_exit终止的,retval所指向的单元里存放的是传给pthread_exit的参数。

pthread_cancel

在进程内某个线程可以取消另一个线程。

#include <pthread.h>

int pthread_cancel(pthread_t thread);

被取消的线程退出值,定义在pthread库中常数

#define PTHREAD_CANCELED ((void *)-1)
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>

void *thr_fn1(void *arg) {
    printf("thread 1 returning\n");
    return (void*)1;
}

void *thr_fn2(void *arg) {
    printf(“thread 2 exiting\n”);
    pthread_exit((void *)2);
}

void *thr_fn3(void *arg) {
    while (1) {
        printf("thread 3 writing\n");    
        sleep(1);
    }
}

int main(void) {
    pthread_t tid;
    void *tret;

    pthread_create(&tid, NULL, thr_fn1, NULL);
    pthread_join(tid, &tret);///< 回收线程(阻塞)
    printf("thread 1 exit code %d by return.\n", (int)tret);

    pthread_create(&tid, NULL, thr_fn2, NULL);
    pthread_join(tid, &tret);
    printf("thread 2 exit code %d by pthread_exit.\n", (int)tret);

    pthread_create(&tid, NULL, thr_fn3, NULL);
    sleep(3);
    pthread_cancel(tid);
    pthread_join(tid, &tret);
    printf("thread 3 exit code %d by pthread_exit.\n", (int)tret);    

    return 0;
}
gcc -g -Wall -pthread pthread-exit.c -o pthread-exit

kudio@ubuntu:~/Study/linux/Thread$ ./pthread-exit
thread 1 returning
thread 1 exit code 1 by return.
thread 2 exiting
thread 2 exit code 2 by pthread_exit.
thread 3 writing
thread 3 writing
thread 3 writing
thread 3 exit code -1 by pthread_exit.

pthread_detach

分离线程。设置为分离态的线程,调用结束后系统将会自动回收,释放其资源。
主要用于不关心线程的返回值。(不关心该线程的返回值,但仍然使用join去等待回收,增加开销)

#include <pthread.h>

int pthread_detach(pthread_t tid);
pthread_t tid:分离线程id。
返回值:成功返回0,失败返回错误号。

一般情况,线程终止后,其终止状态一直保留到其他线程调用pthread_join获取它的状态为止。但线程被设置为detach后,线程一旦终止就会立刻回收所占用的资源,而不保留终止状态,即不需也不能对该线程调用pthread_join。

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

void *thr_fn(void *arg) {
    int n = 3;
    while (n--) {
        printf("thread count %d\n", n);
        sleep(1);
    }
    return (void *)1;
}

int main(void) {
    pthread_t tid;
    void *tret;
    int err;

    pthread_create(&tid, NULL, thr_fn, NULL);
    pthread_detach(tid);

    while (1) {
        err = pthread_join(tid, &tret);///< 阻塞等待回收该线程
        if (err != 0)
            fprintf(stderr, "thread %s\n", strerror(err));///< 打印出错值
        else
            fprintf(stderr, "thread exit code %d\n", (int)tret);
        sleep(1);
    }
    return 0;
}
  1. 第一次运行时,将 22行进行注释
pthread_detach(tid);
gcc -g -Wall -pthread pthread-detach.c -o pthread-detach

kudio@ubuntu:~/Study/linux/Thread$ ./pthread-detach
thread count 2
thread count 1
thread count 0
thread exit code 1
thread No such process
thread No such process

可以发现,线程执行时,pthread_join一直在阻塞等待,当线程return后,pthread_join的返回值为0,即成功,继续执行发现显示没有该线程。

  1. 第二次运行时,将该行恢复
kudio@ubuntu:~/Study/linux/Thread$ ./pthread-detach
thread Invalid argument
thread count 2
thread count 1
thread Invalid argument
thread count 0
thread Invalid argument
thread Invalid argument

此时,可以发现,pthread_join接受的参数一直显示无效,即使该线程仍然在运行,因为该线程已经为分离态(detach)

僵尸线程
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

void *thr_fn(void *arg) {
    int n = 3;
    while (n--) {
        printf("thread count %d\n", n);
        sleep(1);
    }
    return (void *)1;
}

int main(void) {
    pthread_t tid;
    // void *tret;
    // int err;

    pthread_create(&tid, NULL, thr_fn, NULL);
    // pthread_detach(tid);

    while (1);
    return 0;
}

执行该程序,并另开一个窗口进行查看

gcc -g -Wall -pthread pthread-zom.c -o pthread-zom

kudio@ubuntu:~/Study/linux/Thread$ ./pthread-zom
thread count 2
thread count 1
thread count 0
kudio@ubuntu:~$ ps -eLf
UID         PID   PPID    LWP  C NLWP STIME TTY          TIME CMD
kudio     11518   9822  11518 96    1 19:59 pts/5    00:00:04 ./pthread-zom
kudio     11520  11496  11520  0    1 19:59 pts/18   00:00:00 ps -eLf


kudio@ubuntu:~$ ps -Lw 11518
   PID    LWP TTY      STAT   TIME COMMAND
 11518  11518 pts/5    R+     3:37 ./pthread-zom

pthread_equal

比较两个线程id是否相等

#include <pthread.h>

int pthread_equal(pthread_t t1, pthread_t t2);

线程终止方式

  1. 线程主函数中进行return。
  2. 某线程调用pthread_cancel终止同一进程的另一个线程。(类似信号)
  3. 线程调用pthread_exit终止自己。(用于主控线程)

TIPS:
使用pthread_cancel终止线程时,系统并不会马上关闭被终止的线程,只有在被终止的线程下一次系统调用时,才会真正结束线程。或者可以调用pthread_testcalcel,让内核去检测是否需要取消当前线程。

系统调用时,用户空间切换到内核空间。

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

void *thr_fn(void *arg) {
    int *p = (int*)arg;

    printf("Thread *arg = %d\n", *p);
    while (1) {
        (*p)--;
        printf("Thread n = %d\n", *p);
        sleep(1);///< 系统调用
    }
}

int main(void) {
    pthread_t tid;
    int n = 10;

    pthread_create(&tid, NULL, thr_fn, (void*)&n);

    sleep(20);
    pthread_cancel(tid);

    while (1);
    return 0;
}
kudio@ubuntu:~/Study/linux/Thread$ ./pthread-cancel
Thread *arg = 10
Thread n = 9
Thread n = 8
Thread n = 7
...
Thread n = -8
Thread n = -9
Thread n = -10

pthread_cancel执行之前

kudio@ubuntu:~$ ps -eLf
UID         PID   PPID    LWP  C NLWP STIME TTY          TIME CMD
kudio     11664   9822  11664  0    2 20:44 pts/5    00:00:00 ./pthread-cancel
kudio     11664   9822  11665  0    2 20:44 pts/5    00:00:00 ./pthread-cancel

kudio@ubuntu:~$ ps -Lw 11664
   PID    LWP TTY      STAT   TIME COMMAND
 11664  11664 pts/5    Sl+    0:00 ./pthread-cancel
 11664  11665 pts/5    Sl+    0:00 ./pthread-cancel

thread_cancel执行之后

kudio@ubuntu:~$ ps -eLf
UID         PID   PPID    LWP  C NLWP STIME TTY          TIME CMD
kudio     11664   9822  11664 51    1 20:44 pts/5    00:00:21 ./pthread-cancel

kudio@ubuntu:~$ ps -Lw 11664
   PID    LWP TTY      STAT   TIME COMMAND
 11664  11664 pts/5    R+     0:28 ./pthread-cancel

这里举的例子不是很好,但可以在线程中调用pthread_testcalcel,如果已经被pthread_cancel,那么此时会立即释放。

线程属性

之前讨论的都是采用的线程默认属性。但如果对性能有更高的要求就需要设置线程属性,类如可以通过设置线程栈的大小来降低内存的使用,从而增加最大线程个数。

typedef struct {
    int                 etachstate;///< 线程的分离状态
    int                 schedpolicy;///< 线程调度策略,优先级
    structsched_param   schedparam;///< 线程的调度参数
    int                 inheritsched;///< 线程的继承性
    int                 scope;///< 线程的作用域
    size_t              guardsize;///< 线程栈末尾的警戒缓冲区大小
    int                 stackaddr_set;///< 线程的栈设置
    void*               stackaddr;///< 线程栈的位置
    size_t              stacksize;///< 线程栈的大小
}pthread_attr_t;

线程属性初始化

先初始化线程属性,再pthread_create创建线程

#include <pthread.h>

int pthread_attr_init(pthread_attr_t *attr);///< 初始化线程属性
int pthread_attr_destroy(pthread_attr_t *attr);///< 销毁线程属性所占有的资源

线程的分离状态

线程的分离状态决定一个线程以什么样的方式来终止自己。

  1. 非分离状态:默认状态,原有的线程等待创建的线程结束,只有当pthread_join()返回时,创建的线程才算终止,才能释放自己占用的系统资源。
  2. 分离状态:自行运行结束,线程终止,立马释放系统资源。
#include <pthread.h>

int pthread_attr_setdetachstate(pthread_attr_t * attr, int detachstate);
int pthread_attr_getdetachstate(pthread_attr_t * attr, int detachstate);

TIPS
如果设置一个线程为分离线程,而该线程运行又非常快,可能在pthread_create函数返回之前就终止了,终止后其线程号和系统资源移交给其他线程使用,这样调用pthread_create就得到了错误的线程号。可以采取同步解决该问题。

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

void *th_fun(void *arg) {
    int n = 10;
    while (n--) {
        printf("%x  %d\n", (int)pthread_self(), n);
        sleep(1);
    }
    return (void *)1;
}

int main(void) {
    pthread_t tid;
    pthread_attr_t attr;
    int err;

    pthread_attr_init(&attr);///< attr里面保存创建线程的默认属性
    pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);///< 设置线程为分离属性

    pthread_create(&tid, &attr, th_fun, NULL);

    err = pthread_join(tid, NULL);
    while (1) {
        if (err != 0) {
            printf("%s\n", strerror(err));
            pthread_exit((void*)1);
        }
    }
    return 0;
}
kudio@ubuntu:~/Study/linux/Thread$ gcc -g -Wall -pthread pthread-attr.c -o pthread-attr
kudio@ubuntu:~/Study/linux/Thread$ ./pthread-attr
Invalid argument
30db3700  9
30db3700  8
30db3700  7
30db3700  6
30db3700  5
30db3700  4
30db3700  3
30db3700  2
30db3700  1
30db3700  0

kudio@ubuntu:~$ ps -eLf
UID         PID   PPID    LWP  C NLWP STIME TTY          TIME CMD
kudio      2940   2354   2940  0    2 03:41 pts/8    00:00:00 [pthread-attr] <defunct>
kudio      2940   2354   2941  0    2 03:41 pts/8    00:00:00 [pthread-attr] <defunct>

已经将该线程设置为分离状态,所以pthread_join为无效的参数
通过 ps -eLf查看,出现 ,即 “僵尸线程”

线程的栈相关

当系统中有很多线程时,可能需要减小每个线程栈的默认大小,防止进程的地址空间不够用,当线程调用的函数会分配很大的局部变量或者函数调用层次很深时,可能需要增大线程栈的默认大小。

#include <pthread.h>

int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);
int pthread_attr_getstack(pthread_attr_t *attr, void *stackaddr, size_t *stacksize);

attr        指向一个线程属性的指针
stackaddr   返回获取的栈地址
stacksize   返回获取的栈大小
返回值:若是成功返回0,否则返回错误的编号

NPTL

查看线程库版本

getconf GNU_LIBPTHREAD_VERSION、

TIPS

  1. 主线程退出其他线程不退出,主线程调用pthread_exit
  2. 避免僵尸线程
  3. malloc和mmap申请的内存可以被其他线程释放
  4. 应避免在多线程中调用fork,除非立马exec

线程同步

多个线程同时访问共享数据时可能会冲突,比如两个线程将同一变量+1,一般会分为三个步骤

  1. 从内存读取变量到寄存器
  2. 寄存器的值+1
  3. 将寄存器的值返回内存
    如果两个线程可能会出现如下的情况:
    线程1执行1,2后,线程1执行1,2,然后分别执行3,此时变量相当于只是增加了一次
Thread A                    |   Thread B                 |   变量 i
mov 0x8049500,%eax(eax = 5) |                            |   5
add $0x1,%eax(eax = 6)      |mov 0x8049500,%eax(eax = 5) |   5
mov %eax,0x8049500(eax = 6) |add $0x1,%eax(eax = 6)      |   6
                            |mov %eax,0x8049500(eax = 6) |   6
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

#define COUNT   8000

int counter = 0;

void *th_fun(void *arg) {
    int i = 0;

    for (i = 0; i < COUNT; i++) {
        counter++;
        printf("%s tid %x counter:%d\n", arg, (int)pthread_self(), counter);
    }
    return NULL;
}

int main(int argc, char **argv) {
    pthread_t tidA, tidB;

    pthread_create(&tidA, NULL, &th_fun, "tidA");
    pthread_create(&tidB, NULL, &th_fun, "tidB");

    pthread_join(tidA, NULL);
    pthread_join(tidB, NULL);

    return 0;
}

可以发现最终的counter小于16000,当然也可以将 14~17行改写为:

    for (i = 0; i < COUNT; i++) {
        temp = counter;
        printf("%s tid %x counter:%d", arg, (int)pthread_self(), temp + 1);
        counter = temp + 1;
    }

因此需要对全局资源counter进行上锁,即

    for (i = 0; i < COUNT; i++) {
        ///< 🔒,拿走钥匙,🔑和锁只有一把
        temp = counter;
        printf("%s tid %x counter:%d", arg, (int)pthread_self(), temp + 1);
        counter = temp + 1;
        ///< 🔑开🔒
    }

这里的锁都是建议锁。所有THhread在访问公共数据时先拿到🔒再访问,但是🔒本身不具备强制性。

线程为什么需要同步

  1. 多个线程都可对共享资源进行操作
  2. 线程操作共享资源的先后顺序不确定
  3. 处理器对存储器的操作一般不是原子操作

互斥量

mutex操作原语

pthread_mutex_t

/* Initialize a mutex.  */
extern int pthread_mutex_init (pthread_mutex_t *__mutex, const pthread_mutexattr_t *__mutexattr);

/* Destroy a mutex.  */
extern int pthread_mutex_destroy (pthread_mutex_t *__mutex);

/* Try locking a mutex.  🔒未被使用马上拿到🔒,若已经被使用则立马返回-1,不造成阻塞*/
extern int pthread_mutex_trylock (pthread_mutex_t *__mutex);

/* Lock a mutex.  */
extern int pthread_mutex_lock (pthread_mutex_t *__mutex);

/* Unlock a mutex.  */
extern int pthread_mutex_unlock (pthread_mutex_t *__mutex);

**lock,上🔒,阻塞Thread

**
trylock,尝试加🔒,🔒未被使用马上拿到🔒,若已经被使用则立马返回-1,不造成阻塞

临界区

任意时刻只允许一个线程对共享资源访问。
如果有多个线程试图同时访问临界区,那么在有一个线程进入后,其他所有试图访问此临界区的线程将被挂起,直到进入临界区的线程离开。临界区在被释放后,其他线程可以继续抢占,并以此达到用原子方式操作共享资源的目的。

临界区的选定应尽可能小,如果太大会影响程序的并行处理性能。

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

#define COUNT   5000

int counter = 0;
pthread_mutex_t counter_mutex = PTHREAD_MUTEX_INITIALIZER;///< 锁是全局变量,可以静态初始化
///< 可以当作一个0/1,初始化🔒=1,加🔒即--,解🔒即++>

void *th_fun(void *arg) {
    int i = 0, temp;

    for (i = 0; i < COUNT; i++) {
        pthread_mutex_lock(&counter_mutex);

        temp = counter;
        printf("%s tid %x counter:%d\n", arg, (int)pthread_self(), temp + 1);
        counter = temp + 1;

        pthread_mutex_unlock(&counter_mutex);
    }
    return NULL;
}

int main(int argc, char **argv) {
    pthread_t tidA, tidB;

    // pthread_mutex_init(&mutex, NULL);///< 动态初始化🔒

    pthread_create(&tidA, NULL, &th_fun, "tidA");
    pthread_create(&tidB, NULL, &th_fun, "tidB");

    pthread_join(tidA, NULL);
    pthread_join(tidB, NULL);

    // pthread_mutex_destroy(&mutex);///< 销毁🔒
    return 0;
}

死锁

  1. 同一个线程在拥有A锁的情况下再次请求获得A锁,即AA
  2. Thread1拥有A🔒,请求获得B🔒;Thread2拥有B🔒,请求获得A锁,即A

死锁会导致一直处于循环等待。

读写锁

读共享,写独占,写优先级高,适用于读的次数远大于写的次数
🔒只有一把

pthread_rwlock_t

extern int pthread_rwlock_init (pthread_rwlock_t *__restrict __rwlock, const pthread_rwlockattr_t *__restrict__attr);

/* Destroy read-write lock RWLOCK.  */
extern int pthread_rwlock_destroy (pthread_rwlock_t *__rwlock);

/* Acquire read lock for RWLOCK.  */
extern int pthread_rwlock_rdlock (pthread_rwlock_t *__rwlock);
     
/* Try to acquire read lock for RWLOCK.  */
extern int pthread_rwlock_tryrdlock (pthread_rwlock_t *__rwlock);
  
/* Acquire write lock for RWLOCK.  */
extern int pthread_rwlock_wrlock (pthread_rwlock_t *__rwlock);     

/* Try to acquire write lock for RWLOCK.  */
extern int pthread_rwlock_trywrlock (pthread_rwlock_t *__rwlock);
     
/* Unlock RWLOCK.  */
extern int pthread_rwlock_unlock (pthread_rwlock_t *__rwlock);

restrict只可以用于限定和约束指针,表明指针是访问一个数据对象的唯一且初始的方式。即它告诉编译器,所有修改该指针所指向内存中内容的操作都必须通过该指针来修改,而不能通过其它途径(其它变量或指针)来修改。
这样做能帮助编译器进行更好的优化代码,生成更有效率的汇编代码。如:

int *restrict ptr,
ptr 指向的内存单元只能被 ptr 访问到,任何同样指向这个内存单元的其他指针都是未定义的,直白点就是无效指针
未使用读写锁:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

int counter;

///< 3个线程不定时写同一全局资源,5个线程不定时读同一全局资源
void *th_write(void *arg) {
    for (int i = 0; i < 2; i++) {
        int temp = counter;
        printf("thread %d write %x  counter %d  counter+1   %d.\n", \
                (int)arg, (int)pthread_self(), counter, counter + 1);
        counter = temp + 1;
        sleep(1);///< 失去CPU,供其他线程占用CPU
    }
    return NULL;
}

void *th_read(void *arg) {
    while (1) {
        printf("thread %d read  %x  %d.\n", (int)arg, (int)pthread_self(), counter);
        sleep(1);///< 失去CPU,供其他线程占用CPU
    }
    return NULL;
}

int main(int argc, char **argv) {
    int i = 0;
    pthread_t tid[8];

    for (i = 0; i < 3; i++)
        pthread_create(&tid[i], NULL, th_write, (void*)i);
    for (i = 4; i < 8; i++)
        pthread_create(&tid[i], NULL, th_read, (void*)i);
    for (i = 0; i < 8; i++)
        pthread_join(tid[i], NULL);

    return 0;
}
kudio@ubuntu:~/Study/linux/Thread$ ./pthread-rwlock
thread 0 write  15e94700  counter       0       counter+1       1.
thread 1 write  15693700  counter       0       counter+1       1.
thread 2 write  14e92700  counter       1       counter+1       2.
thread 4 read   ffff700 2.
thread 7 read   e7fc700 2.
thread 5 read   f7fe700 2.
thread 6 read   effd700 2.
thread 1 write  15693700  counter       2       counter+1       3.
thread 6 read   effd700 2.
thread 0 write  15e94700  counter       2       counter+1       3.
thread 5 read   f7fe700 2.
thread 7 read   e7fc700 2.
thread 4 read   ffff700 3.
thread 2 write  14e92700  counter       2       counter+1       3.
thread 5 read   f7fe700 3.
thread 7 read   e7fc700 3.
thread 6 read   effd700 3.

可以发现counter最后应该是6,实则为3

使用读写锁:
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

int counter;
pthread_rwlock_t rwlock;

///< 3个线程不定时写同一全局资源,5个线程不定时读同一全局资源

void *th_write(void *arg) {
    for (int i = 0; i < 2; i++) {
        pthread_rwlock_wrlock(rwlock);
        int temp = counter;
        printf("thread %d write %x  counter %d  counter+1   %d.\n", \
                (int)arg, (int)pthread_self(), counter, counter + 1);
        counter = temp + 1;
        pthread_rwlock_unlock(rwlock);
        sleep(1);
    }
    return NULL;
}pthread_mutex_destroy();

void *th_read(void *arg) {
    while (1) {
        pthread_rwlock_rdlock(rwlock);
        printf("thread %d read  %x  %d.\n", (int)arg, (int)pthread_self(), counter);
        pthread_rwlock_unlock(rwlock);
        sleep(1);
    }
    return NULL;
}

int main(int argc, char **argv) {
    int i = 0;
    pthread_t tid[8];
    pthread_rwlock_init(&rwlock, NULL);

    for (i = 0; i < 3; i++) 
        pthread_create(&tid[i], NULL, th_write, (void*)i);
    for (i = 4; i < 8; i++) 
        pthread_create(&tid[i], NULL, th_read, (void*)i);
    for (i = 0; i < 8; i++)
        pthread_join(tid[i], NULL);

    pthread_rwlock_destroy(&rwlock);
    return 0;
}
kudio@ubuntu:~/Study/linux/Thread$ ./pthread-rwlock
thread 0 write fea14700  counter 0  counter+1   1.
thread 1 write fe213700  counter 1  counter+1   2.
thread 5 read  fca10700  2.
thread 6 read  f7fff700  2.
thread 4 read  fd211700  2.
thread 2 write fda12700  counter 2  counter+1   3.
thread 7 read  f77fe700  3.
thread 1 write fe213700  counter 3  counter+1   4.
thread 2 write fda12700  counter 4  counter+1   5.
thread 0 write fea14700  counter 5  counter+1   6.
thread 4 read  fd211700  6.
thread 6 read  f7fff700  6.
thread 7 read  f77fe700  6.
thread 5 read  fca10700  6.

条件变量

**条件变量不是🔒的一种,但通常结合互斥锁进行使用

pthread_cond_t

extern int pthread_cond_init (pthread_cond_t *__restrict __cond, 
                    const pthread_condattr_t *__restrict __cond_attr);

/* Destroy condition variable COND.  */
extern int pthread_cond_destroy (pthread_cond_t *__cond);

/* Wake up one thread waiting for condition variable COND.  */
extern int pthread_cond_signal (pthread_cond_t *__cond);

/* Wake up all threads waiting for condition variables COND.  */
extern int pthread_cond_broadcast (pthread_cond_t *__cond);

/* Wait for condition variable COND to be signaled or broadcast.
   MUTEX is assumed to be locked before.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int pthread_cond_wait (pthread_cond_t *__restrict __cond, 
                    pthread_mutex_t *__restrict __mutex);

/* Wait for condition variable COND to be signaled or broadcast until
   ABSTIME.  MUTEX is assumed to be locked before.  ABSTIME is an
   absolute time specification; zero is the beginning of the epoch
   (00:00:00 GMT, January 1, 1970).

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int pthread_cond_timedwait (pthread_cond_t *__restrict __cond,
				   pthread_mutex_t *__restrict __mutex,
				   const struct timespec *__restrict __abstime);

pthread_cond_wait
  1. 阻塞等待条件变量cond满足
  2. 释放已掌握的互斥锁,相当于pthread_mutex_unlock(&mutex)
  3. 当被唤醒时,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex)

1\2两步为一个原子操作,即不会被打断
3中的唤醒为pthread_cond_signal和pthread_cond_broadcast

被唤醒前一直处于阻塞状态

生产者-消费者模型
  1. 创建🔒 pthread_mutex_t mutex
  2. 初始化🔒 pthread_mutex_init(&mutex, NULL)

生产者

  1. 生产数据
  2. 加🔒 pthread_mutex_lock(&mutex)
  3. 将数据放到公共区域
  4. 解锁 pthread_mutex_unlock(&mutex)
  5. 通知阻塞在条件变量上的线程pthread_cond_signal/pthread_cond_broadcast
  6. 循环生产后续数据

消费者

  1. 加🔒 pthread_mutex_lock(&mutex)
  2. 等待条件满足 pthread_cond_wait(&cond, &mutex)
    1. 阻塞等待条件变量
    2. 解锁unlock(mutex)
    3. 加锁lock(mutex)
  3. 访问共享数据
  4. 解🔒,释放条件变量和🔒
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>

#define element int

typedef struct msg_node_s {
    struct msg_node_s *next;
    element data;
}msg_node_t;

msg_node_t *head;

pthread_cond_t has_product = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

///< 生产者
void *producer(char *arg) {
    msg_node_t *msg_node;
    // msg_node = (msg_node_t*)malloc(sizeof(msg_node_t));

    printf("Now in pro.\r\n");
    while (1) {
        msg_node = (msg_node_t*)malloc(sizeof(msg_node_t));
        msg_node->data = rand() % 1000 + 1;///< 模拟生产
        printf("----------Produce %d.\n", msg_node->data);

        pthread_mutex_lock(&mutex);
        msg_node->next = head;
        head = msg_node;
        pthread_mutex_unlock(&mutex);

        pthread_cond_signal(&has_product);
        sleep(rand() % 5);
    }
}

///< 消费者
void *consumer(char *arg) {
    msg_node_t *msg_node;

    printf("Now in con.\r\n");
    while (1) {
        pthread_mutex_lock(&mutex);

        while (head == NULL)
            pthread_cond_wait(&has_product, &mutex);///< 阻塞等待条件变量
        msg_node = head;
        head = msg_node->next;

        pthread_mutex_unlock(&mutex);
        printf("-----Conseme %d.\n", msg_node->data);
        free(msg_node);
        sleep(rand() % 5);
    }
}

int main(int argc, char **argv) {
    pthread_t pid, cid;

    printf("Start create thread.\r\n");
    pthread_create(&pid, NULL, producer, NULL);
    pthread_create(&cid, NULL, consumer, NULL);

    pthread_join(pid, NULL);
    pthread_join(cid, NULL);
    return 0;
}
kudio@ubuntu:~/Study/linux/Thread$ ./pthread-cond
----------Produce 384.
-----Conseme 384.
----------Produce 916.
-----Conseme 916.
----------Produce 387.
-----Conseme 387.
----------Produce 422.
-----Conseme 422.
----------Produce 691.
-----Conseme 691.
----------Produce 927.
----------Produce 427.
-----Conseme 427.
-----Conseme 927.
----------Produce 369.
-----Conseme 369.
----------Produce 783.
----------Produce 863.
-----Conseme 863.

// todo 为什么在48行用while而不是if

信号量

相当于初始化值为N的互斥量,N表示可以同时访问共享数据区的线程数。

sem_t
extern int sem_init (sem_t *__sem, int __pshared, unsigned int __value);
/* Free resources associated with semaphore object SEM.  */
extern int sem_destroy (sem_t *__sem);

/* Wait for SEM being posted.

   This function is a cancellation point and therefore not marked with
   __THROW.  */
extern int sem_wait (sem_t *__sem);

/* Test whether SEM is posted.  */
extern int sem_trywait (sem_t *__sem) __THROWNL;

extern int sem_timedwait (sem_t *__restrict __sem,
			  const struct timespec *__restrict __abstime);

/* Post SEM.  */
extern int sem_post (sem_t *__sem) __THROWNL;

/* Get current value of SEM and store it in *SVAL.  */
extern int sem_getvalue (sem_t *__restrict __sem, int *__restrict __sval);

sem_wait()一次调用,做一次–操作,信号量值为0时,再次–就会阻塞
sem_post()一次调用,做一次++操作,信号量为N时,再次++就会阻塞

生产者-消费者问题

设置两种信号量:empty(空的缓冲区的数量)和full(已经填入的数量)

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

#define NUM 5
int queue[NUM];
sem_t empty_num;///< 生产的产品数
sem_t full_num;///< 可生产产品数

///< 生产者
void *producer(void *arg) {
    int p = 0;
    while (1) {
        sem_wait(&full_num);///< 阻塞等待满队列减少
        queue[p] = rand() % 1000 + 1;
        printf("Produce %d.\n", queue[p]);
        sem_post(&empty_num);///< 产品数++

        p = (p + 1) % NUM;
        sleep(rand()%5);
    }
}

///< 消费者
void *consumer(void *arg) {
    int c = 0;
    while (1) {
        sem_wait(&empty_num);///< 消耗产品,产品数--
        printf("----Consume %d.\n", queue[c]);
        queue[c] = 0;
        sem_post(&full_num);///< 可生产产品数++

        c = (c + 1) % NUM;
        sleep(rand()%5);
    }
}

int main(int argc, char **argv) {
    pthread_t pid, cid;

    sem_init(&full_num, 0, NUM);
    sem_init(&empty_num, 0, 0);   
    pthread_create(&pid, NULL, producer, NULL);
    pthread_create(&cid, NULL, consumer, NULL);

    pthread_join(pid, NULL);
    pthread_join(cid, NULL);
    sem_destroy(&empty_num);
    sem_destroy(&full_num);

    return 0;
}

kudio@ubuntu:~/Study/linux/Thread$ ./semaphore-test
Start
Produce 384.
----Consume 384.
Produce 916.
----Consume 916.
Produce 387.
----Consume 387.
Produce 422.
----Consume 422.
Produce 691.
----Consume 691.
Produce 927.
Produce 427.
----Consume 927.
Produce 212.
----Consume 427.

进程间

如果线程终止时没有释放加锁的互斥量,则该互斥量不能再被使用
信号的复杂语义很难和多线程共存,应避免在多线程中引入信号机制

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值