写在前面
总结一下Linux系统的线程创建/终止/等待等系统调用, 参考:
- Linux/Unix系统编程手册.
下面主要给出例子, 关于函数原型可以参考书中或者man 7 pthreads
.
测试环境: Ubuntu 20.04 x86_64
gcc-9
为什么需要线程?
进程的限制
多进程往往存在如下限制:
- 进程间资源难以共享. 因为除去只读代码段, 父子进程并不共享内存, 所以必须采用进程间通信(IPC)的方式交换进程间信息.
- 调用
fork()
创建进程的代价很高. 虽然采用写时复制技术(Copy-on-write), 还是免不了造成资源的浪费.
线程的优势
-
线程之间可以方便/快速地共享信息. 只需将数据复制到共享变量(全局变量/堆内存)中即可.
-
创建线程比创建进程快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()
对互斥量进行动态初始化. 这些情况包括:
- 动态分配于堆中的互斥量(例如链表中的每一个节点)
- 互斥量在栈中分配的自动变量.
- 初始化经由静态分配, 但是不使用默认属性的互斥量
#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
代表待消费产品的数量.