Linux线程学习总结
1.线程基础
1.1.什么是线程
在一个程序里的多个执行路线就叫做线程。更准确的定义是:线程是“一个进程内部的一个控制序列”。典型的unix进程可以看成只有一个控制线程:一个进程在同一时刻只做一件事情。有了多个控制线程以后,在程序设计时可以把进程设计成在同一时刻能够做不止一件事,每个线程处理各只独立的任务。
1.2. 线程的优点
通过为每种事件类型的处理分配单独的线程,能够简化处理异步时间的代码。
多个线程可以自动共享相同的存储地址空间和文件描述符。
有些问题可以通过将其分解从而改善整个程序的吞吐量。
交互的程序可以通过使用多线程实现相应时间的改善,多线程可以把程序中处理用户输入输出的部分与其它部分分开。
1.3. 线程的缺点
线程也有不足之处。编写多线程程序需要更全面更深入的思考。在一个多线程程序里,因时间分配上的细微偏差或者因共享了不该共享的变量而造成不良影响的可能性是很大的。调试一个多线程程序也比调试一个单线程程序困难得多。
2.线程的创建
从第一节我们对线程有了一个基础性的认识,其实线程就是一个轻量级的进程,那么我们应该怎么样创建线程呢?对于新增的线程可以通过pthread_create函数创建。函数如下:
Int pthread_create(pthread_t *restrict tidp, Const pthread_attr_t *restrict attr, Void *(*start_rtn)(void *), void *restrict arg); |
函数返回值:若成功则返回0,否则返回错误编号。成功返回时,tidp指向的内存单元被设置为新创建的线程ID,attr参数用于定制各种不同的线程属性,线程属性后面再做讨论,Void *(*start_rtn)(void *)为线程的运行函数,arg为传入到线程函数的参数,对于不止一个参数的线程函数,应该把那些参数定义成一个结构体,把结构体的地址传进去,对于传进去的参数是局部变量时,一定要注意,该局部变量要在销毁前确认是否线程函数还在使用,没有使用才能销毁,否则就会出问题,我在使用的过程中就遇到这一问题,线程函数的参数是在一个函数中定义的局部变量,函数退出(局部变量被销毁)后还在线程中使用,这导致那参数所指的内存被改写,导致出现问题。线程创建时并不能保证哪个线程会先运行。新创建的线程可以访问进程的地址空间,并且继续调用线程的浮点环境和信号屏蔽字,但是该线程的未决信号集被清除。创建线程的例子如下所示:
#include <pthread.h> #include <stdio.h>
void *pthread_func(void *arg) { printf("Hello world!\n"); }
int main(int argc, char *argv[]) { int err; pthread_t tid;
err = pthread_create(&tid, NULL, pthread_func, NULL); if (err != 0) { printf("Create thread failed: %s\n", strerror(err)); exit(0); } sleep(0);
return 0; } |
3.线程终止
如果进程中的任一线程调用exit,__Exit,那么整个进程都会终止,线程可以通过以下方式在不终止整个进程的情况下终止线程的运行
a.线程只是从启动里程中返回,返回值是线程的退出码(线程运行完毕)
b.线程可以被同一进程的其他线程取消
c.线程调用pthread_exit退出
#include <pthread.h>
Void pthread_exit(void *rval_ptr) |
Rval_ptr是一个无类型指针,如果不关心其返回值,可以传人NULL,进程中的其他线程可以通过调用pthread_join函数访问到这个指针。
int pthread_join(pthread_t thread, void **rval_ptr); |
调用线程将一直阻塞,直到指定的调用线程调用pthread_exit,从启动历程中返回或者被取消,如果所调用的线程出于分离状态,则调用会失败,返回EINVAL。线程可以调用pthread_cancle函数来请求取消同一进程中的其他线程,下面是一个例子
#include <pthread.h> #include <stdio.h>
void *pthread_func(void *arg) { printf("Hello world!\n");
pthread_exit(NULL); }
int main(int argc, char *argv[]) { int err; pthread_t tid;
err = pthread_create(&tid, NULL, pthread_func, NULL); if (err != 0) { printf("Create thread failed: %s\n", strerror(err)); return -1; }
pthread_join(tid, NULL); printf("test thread is finished.\n");
return 0; } |
4.线程同步
线程可以通过互斥量、读写锁、条件变量、自旋锁等来控制线程的同步
4.1 互斥量
可以通过使用pthread的互斥接口保护数据,确保同时只有一个线程访问数据,互斥量相关的函数有:
#include <pthread.h>
pthread_mutex_t fastmutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t recmutex = PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP;
pthread_mutex_t errchkmutex=PTHREAD_ERRORCHECK_MUTEX_INITIALIZER_NP;
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_trylock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
int pthread_mutex_destroy(pthread_mutex_t *mutex); |
互斥量(mutex)本质上就是一把锁,在使用互斥变量之前,先要对互斥量进行初始化,对静态变量可以通过PTHREAD_MUTEX_INITIALIZER进行初始化,对于动态变量,只能使用pthread_mutex_ini进行初始化,一般使用默认属性的互斥变量,mutexattr变量置为NULL,在工作中,我也都是使用默认属性,也没有遇到变态的问题,只要注意避免死锁,具体的也不再累述,具体使用如下所示:
pthread_mutex_t mut = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_init(&mut, NULL);/* 返回0表示初始化成功 */
pthread_mutex_lock(&mut); /* * 若想获取不到锁不阻塞,可以使用pthread_mutex_trylock(&mut),返回 * 表示获取锁成功 */ /* operate on x */ pthread_mutex_unlock(&mut); |
4.2 读写锁
读写锁与互斥量类似,不过读写锁允许更高的并行性,多个线程可以进行同时读,如果要保护的数据,大部分操作都是多个线程在读,少量操作是在写,这时候使用读写锁效率会比较高,读写锁主要有如下函数:
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock); int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock); int pthread_rwlock_timedrdlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); int pthread_rwlock_timedwrlock(pthread_rwlock_t *restrict rwlock, const struct timespec *restrict abs_timeout);
int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); |
函数的具体使用跟互斥量大致一样,所以这边也不再累述,我也有使用这种锁,也没有遇到什么问题,遇到的问题就是死锁,是获取锁后再次去获取锁导致死锁的,只要我们使用时小心点就能避免死锁了,在死锁的情况下可以使用GDB来调试,就可以知道是在哪里死锁了,具体怎么样使用GDB可以去网上搜索一下资料,这介绍挺全面了,对我们怎么样使用GDB有很大帮助http://wiki.ubuntu.org.cn/index.php?title=%E7%94%A8GDB%E8%B0%83%E8%AF%95%E7%A8%8B%E5%BA%8F&variant=zh-cn
4.3 条件变量
条件变量是线程可用的另一种同步机制,条件变量与互斥量一起使用时,允许线程以无竞争的方式等待特定的条件发生,条件变量是由互斥变量保护的,线程在改变条件状态前首先要锁住互斥量,其他线程在获得互斥量之前不会觉察到这种改变,与条件变量相关的函数如下所示:
pthread_cond_t cond = PTHREAD_COND_INITIALIZER; int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr); int pthread_cond_destroy(pthread_cond_t *cond);
/* 向线程或者条件发送信号 */ int pthread_cond_signal(pthread_cond_t *cond); int pthread_cond_broadcast(pthread_cond_t *cond);
/* 等待条件为真,一个是一直阻塞,一个是有超时时间 */ int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
以上函数的返回值都是:若成功则返回0,否则返回错误编号 |
这些函数的具体含义和参数及错误编号,我就不一一列举了,需要时候的上网查查,只要注意无法知道其向线程或者条件发送信号的次数,若给一个线程发送信号时,该线程还在跑,再次跑到pthread_cond_wait时就会阻塞在那,因为该线程错过了该信号,若要知道发送信号的次数,可以用一个统计值,下面就给实例来看看具体这些函数是怎么样使用的,程序清单如下所示:
#include <pthread.h> #include <stdio.h> #include <stdlib.h>
typedef struct msg_s { struct msg_s *m_next; /* add more stuff here */ } msg_t;
msg_t *workq; pthread_cond_t qready = PTHREAD_COND_INITIALIZER; pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
void receive_msg(void) { msg_t *mp;
while (1) { pthread_mutex_lock(&qlock); while (workq == NULL) { pthread_cond_wait(&qready, &qlock); } printf("%s, The work queue is not empty.\n", __func__); mp = workq; workq = mp->m_next; /* handle the msg */ free(mp); pthread_mutex_unlock(&qlock); } }
void send_msg(msg_t *mp) { pthread_mutex_lock(&qlock); mp->m_next = workq; workq = mp; pthread_mutex_unlock(&qlock); pthread_cond_signal(&qready);/* 也可以在解锁前发送 */ }
void *pthread_func(void *arg) { receive_msg();
pthread_exit(NULL); }
int main(int argc, char *argv[]) { int err, cnt; msg_t *mp; pthread_t tid;
err = pthread_create(&tid, NULL, pthread_func, NULL); if (err != 0) { printf("Create thread failed\n"); return -1; }
for (cnt = 0; cnt < 10; cnt++) { mp = (msg_t *)malloc(sizeof(msg_t)); if (mp == NULL) { printf("Can't alloc msg memory.\n"); sleep(1); continue; } send_msg(mp); sleep(1); }
pthread_join(tid, NULL); printf("test thread finished.\n");
return 0; } |
在调用pthread_cond_wait必须先调用pthread_mutex_lock获取锁,这点要特别注意,其在阻塞前会进行解锁。
5.线程属性
前面所有调用pthread_create函数的例子中,传入的参数都是NULL,而不是指向pthread_attr_t结构的指针,可以使用pthread_attr_t结构修改线程的属性,对于线程属性,我在编程过程中基本上都使用默认属性,所以这方面的注意点及使用也不是很了解,倒是有修改过线程的调度策略属性,结果没有修改成功,那为什么会修改不成功呢?修改线程的调用策略属性要超级用户才能修改即root用户,其它用户是不能修改的,而我们设备好像是什么用户都不是
将一个线程绑定到固定cpu或者绑定到固定的cpu集合,可以使用pthread_setaffinity_np函数,获取线程绑定cpu的信息使用pthread_getaffinity_np函数,这两个函数如下所示:
#define _GNU_SOURCE /* See feature_test_macros(7) */ #include <pthread.h>
int pthread_setaffinity_np(pthread_t thread, size_t cpusetsize, const cpu_set_t *cpuset);
int pthread_getaffinity_np(pthread_t thread, size_t cpusetsize, cpu_set_t *cpuset); Compile and link with -pthread |
如果这两个函数调用成功,则返回0,调用错误返回非0,具体返回的错误值可以参考man手册,里面解释的比较清楚,这边就不讨论了。另外,调用pthread_setaffinity_np函数要是线程服务函数中调用,如果在线程创建后就调用该函数是会发生绑定错误的,这点要特别注意,这在我使用的时候遇到过,所以印象比较深刻。下面给出的是使用该函数绑定cpu的例子:
#include<stdlib.h> #include<stdio.h> #include<sys/types.h> #include<sys/sysinfo.h> #include<unistd.h>
#define __USE_GNU #include<sched.h> #include<ctype.h> #include<string.h> #include <pthread.h>
void *pthread_bind_cpu_test(void *agrv) { int j, i; cpu_set_t mask; cpu_set_t get; int num;
num = = sysconf(_SC_NPROCESSORS_CONF); /* 获取cpu核的个数 */ CPU_ZERO(&mask); /* 当然这边也不需要是一个cpu集合,某个cpu也是可以的 */ for (j = 0; j < num; j++) { CPU_SET(j, &mask); } if (pthread_setaffinity_np(pthread_self(), sizeof(mask), &mask) < 0) { printf("Can't set CPU affinity...\n"); } printf("%s, %d.\n", __func__, __LINE__); j = 0; while (j++ < 50) { CPU_ZERO(&get); if (pthread_getaffinity_np(pthread_self(), sizeof(get), &get) < 0) { printf("warning: cound not get cpu affinity, continuing...\n"); }
for (i = 0; i < num; i++) { if (CPU_ISSET(i, &get)){ printf("this pthread is running processor : %d\n", i); } } } }
int main(int argc, char* argv[]) { int num = sysconf(_SC_NPROCESSORS_CONF); int created_thread = 0; int myid; int i; int j = 0; int ret; pthread_t tid; cpu_set_t mask; cpu_set_t get;
if (argc != 2) { printf("usage : ./cpu num\n"); exit(1); } myid = atoi(argv[1]);
ret = pthread_create(&tid, NULL, pthread_bind_cpu_test, NULL); if (ret < 0) { printf("%s, create thread failed.\n", __func__); } printf("login = %s.\n", getlogin()); printf("system has %i processor(s). \n", num);
CPU_ZERO(&mask); CPU_SET(myid, &mask);
/* 对于进程使用下面的函数绑定cpu */ if (sched_setaffinity(0, sizeof(mask), &mask) == -1){ printf("warning: could not set CPU affinity\n"); } while (j++ < 50) { CPU_ZERO(&get); if (sched_getaffinity(0, sizeof(get), &get) == -1) { printf("warning: cound not get cpu affinity\n"); } for (i = 0; i < num; i++) { if (CPU_ISSET(i, &get)){ printf("running processor : %d\n",i); } } } pthread_join(tid, NULL);
return 0; } |
想了解更多有关线程的函数可以参考帮助手册,可以man 相关函数,如想查看pthread_create的更多信息,可以在shell控制台上键入man pthread_create,如果没有该页条目,说明没有安装相关的帮助文档,需要安装glibc-doc和manpages-posix-dev库,在ubuntu下可以可以通过新立得安装
sudo apt-get install glibc-doc
sudo apt-get install manpages-posix-dev
如果还想知道线程相关的有什么函数,我们可以查看我们环境下的编译器的头文件,如现在我使用的mips的编译器,找到其安装编译器的目录,具体可以查看每个工程目录下tools文件夹,然后打开XXX-elf文件夹,pthread.h就在其include文件夹里面。线程相关的函数就在这里面,当然这不包括全部的函数,还有一些函数是没有被包含在这里面的,如pthread_setaffinity_np和pthread_getaffinity_np函数。
其它,要把信号发送到进程,可以调用kill函数;要把信号发送到线程,可以调用pthread_kill函数。
#include <pthread.h> #include <signal.h>
int pthread_sigmask(int how, const sigset_t *newmask, sigset_t *old‐mask); int pthread_kill(pthread_t thread, int signo); |