Linux系统编程的基础系列文章,随着不断学习会将一些知识点进行更新,前期主要是简单了解和学习,本期主要介绍线程。
文章目录
线程
线程概念
线程和进程的关系
- 轻量级进程,也有 PCB,创建线程使用的底层函数和进程是一样的,都是 clone
- 从内核里看进程和线程是一样的,都有各自不同的 PCB,但是 PCB中指向内存资源的三级页表是相同的
- 可以理解为线程就是寄存器和栈
- linux下,线程是最小的执行单位;进程是最小的分配资源单位
可以发现,CPU为每个进程分配资源,而调度则是根据线程调度器进行调度
线程间共享资源
- 文件描述符表
- 每种信号的处理方式
- 当前工作目录
- 用户ID和组ID
- 内存地址空间(0~3G)
- Text
- data
- bss
- 堆
- 共享库
这里的Thread stack是用户栈,是不共享的
线程间非共享资源
- 线程id(不是lwp,只在进程内有效,为了方便进程进行识别身份)
- 处理器现场和栈指针(内核栈)
- 独立的栈空间(用户空间栈)
- errno变量
- 信号屏蔽字
- 调度优先级
线程优缺点
优点
- 提高程序的并发性
- 开销小,不用重新分配内存
- 通信和共享数据方便
缺点
- 线程不稳定(库函数实现的)
- 线程调试比较困难(gdb支持不好)
- 线程无法使用unix经典事件,例如信号
线程原语
pthread_create
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine) (void *), void *arg)
参数如下:
- pthread_t *thread:
- 传递一个pthread_t变量地址进来,用于保存新线程的tid(线程ID)
- const pthread_attr_t *attr:
- 线程属性设置(栈大小,优先级),如使用默认属性,则传NULL
- void *(*start_routine) (void *):
- 函数指针,指向新线程应该加载执行的函数模块
- void *arg:
- 指定线程将要加载调用的那个函数的参数
返回值:
- 成功返回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);
参数如下:
- pthread_t thread:回收线程的tid
- void **retval:接收退出线程传递出的返回值
- 返回值:成功返回0,失败返回错误号
调用该函数的线程将挂起等待,直到id为thread的线程终止。
thread以不同的方法终止,通过pthread_join得到的终止状态是不同的,如下:
- thread通过return返回,retval所指向的单元里存放的是thread线程函数的返回值。
- thread被别的线程调用pthread_cancel(被别的线程的信号)异常终止,retval所指向的单元里存放的是常熟PTHREAD_CANCELED。
- 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;
}
- 第一次运行时,将 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,即成功,继续执行发现显示没有该线程。
- 第二次运行时,将该行恢复
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);
线程终止方式
- 线程主函数中进行return。
- 某线程调用pthread_cancel终止同一进程的另一个线程。(类似信号)
- 线程调用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);///< 销毁线程属性所占有的资源
线程的分离状态
线程的分离状态决定一个线程以什么样的方式来终止自己。
- 非分离状态:默认状态,原有的线程等待创建的线程结束,只有当pthread_join()返回时,创建的线程才算终止,才能释放自己占用的系统资源。
- 分离状态:自行运行结束,线程终止,立马释放系统资源。
#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
- 主线程退出其他线程不退出,主线程调用pthread_exit
- 避免僵尸线程
- malloc和mmap申请的内存可以被其他线程释放
- 应避免在多线程中调用fork,除非立马exec
线程同步
多个线程同时访问共享数据时可能会冲突,比如两个线程将同一变量+1,一般会分为三个步骤
- 从内存读取变量到寄存器
- 寄存器的值+1
- 将寄存器的值返回内存
如果两个线程可能会出现如下的情况:
线程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在访问公共数据时先拿到🔒再访问,但是🔒本身不具备强制性。
线程为什么需要同步
- 多个线程都可对共享资源进行操作
- 线程操作共享资源的先后顺序不确定
- 处理器对存储器的操作一般不是原子操作
互斥量
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;
}
死锁
- 同一个线程在拥有A锁的情况下再次请求获得A锁,即AA
- 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
- 阻塞等待条件变量cond满足
- 释放已掌握的互斥锁,相当于pthread_mutex_unlock(&mutex)
- 当被唤醒时,pthread_cond_wait函数返回时,解除阻塞并重新申请获取互斥锁pthread_mutex_lock(&mutex)
1\2两步为一个原子操作,即不会被打断
3中的唤醒为pthread_cond_signal和pthread_cond_broadcast
被唤醒前一直处于阻塞状态
生产者-消费者模型
- 创建🔒 pthread_mutex_t mutex
- 初始化🔒 pthread_mutex_init(&mutex, NULL)
生产者
- 生产数据
- 加🔒 pthread_mutex_lock(&mutex)
- 将数据放到公共区域
- 解锁 pthread_mutex_unlock(&mutex)
- 通知阻塞在条件变量上的线程pthread_cond_signal/pthread_cond_broadcast
- 循环生产后续数据
消费者
- 加🔒 pthread_mutex_lock(&mutex)
- 等待条件满足 pthread_cond_wait(&cond, &mutex)
- 阻塞等待条件变量
- 解锁unlock(mutex)
- 加锁lock(mutex)
- 访问共享数据
- 解🔒,释放条件变量和🔒
#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.
进程间
如果线程终止时没有释放加锁的互斥量,则该互斥量不能再被使用
信号的复杂语义很难和多线程共存,应避免在多线程中引入信号机制