30_多线程
多线程
多线程简介
线程,是一种允许一个正在运行的程序同时执行不止一个任务的机制。不同线程看起来是并行运行的;Linux操作系统对线程进行异步调度,不断中断它们的执行以给其它线程执行的机会。
线程与进程的区别:
- 线程是进程中的一个独立并发执行的路径,进程退出时,线程也会退出;
- 系统按照进程来分配资源,线程共享所在进程的运行地址空间对应的资源,但是每个线程有自己的栈空间;
- 进程切换的开销要远大于线程;
线程函数说明
pthread_create
/**
* @brief 创建一个新的线程,新的线程从start_routine开始执行,创建时通过arg给线程传递参数
* @param[out] thread 成功后,函数返回线程的ID保存在此变量中,用于区分和后续的线程操作
* @param[in] attr 线程属性,可以在创建线程时设置线程的属性,比如是否可被join、线程运行优先级的设置
* 在实际开发中用的比较多
* @param[in] start_routine 线程创建成功后,从这个函数开始运行,由用户自己实现线程的函数
* @param[in] arg
* @return 成功返回0,调用失败返回错误码
* @note 编译链接时需指定-lpthread库
**/
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *
(*start_routine) (void *), void *arg);
pthread_cancel
/**
* @brief 终止一个正在运行的线程执行
*
* @param[in] thread 表示想要终止的线程ID
*
* @return 成功返回0,错误返回错误码
* @note 编译链接时需指定-lpthread库
**/
int pthread_cancel(pthread_t thread);
pthread_join
/**
* @brief 函数会一直阻塞调用它的线程,直至目标线程执行结束(接收到目标线程的返回值),阻塞状态才会解除
*
* @param[in] thread 表示要等待的线程的ID
* @param[out] retval 表示线程结束返回的值,注意线程返回的是一个void*的指针,因此需要使用二级指针
* 接收返回值
* @return 成功返回0,错误返回错误码
* @note 编译链接时需指定-lpthread库
* */
int pthread_join(pthread_t thread, void **retval);
示例代码
pthread.c
#include<stdio.h>
#include <pthread.h>
#include <unistd.h>
void *pthread_start_routine(void *args)
{
int *nCount = args;
while(1)
{
sleep(1);
(*nCount)++;
if(*nCount > 2)
{
break;
}
printf("pthread count:%d\n", *nCount);
printf("pthread_start_routine\n");
}
return "I'm died";
}
int main(int argc, const char* argv[])
{
pthread_t tid;
int nCount = 0;
int nRet;
int *pRetVal = NULL;
nRet = pthread_create(&tid, NULL, pthread_start_routine, (void *)&nCount); //创建线程
if(nRet != 0)
{
perror("pthread_create is error\n");
return -1;
}
pthread_join(tid, &pRetVal); //阻塞等待线程释放
printf("ret:%s\n", pRetVal);
return 0;
}
运行结果
pthread_attr_init
/**
* @brief 初始化一个线程的属性对象
*
* @param attr 表示要设置线程属性的结构体
*
* 接收返回值
* @return 调用成功返回0,否则返回错误码
*
* */
int pthread_attr_init(pthread_attr_t *attr);
pthread_attr_destroy
/**
* @brief 释放线程的属性对象
*
* @param attr 表示要设置线程属性的结构体
*
* 接收返回值
* @return 成功返回0,错误返回错误码
*
* */
int pthread_attr_destroy(pthread_attr_t *attr);
pthread_attr_setdetachstate
/**
* @brief 设置线程可设置为是否可被joinable或者detached状态
*
* @param attr 表示线程属性结构体指针
* @param detachstate 设置detached属性 使用宏PTHREAD_CREATE_JOINABLE(等待别人释放自己)、PTHREAD_CREATE_DETACHED(不用等待别人释放自己,自己结束时清理自己的资源)
* 接收返回值
* @return 成功返回0,错误返回非0值。
*
* */
int pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate);
示例代码
pthread.c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
void *pthread_start_routine(void *args)
{
int *nCount = args;
while (1)
{
sleep(1);
(*nCount)++;
if (*nCount > 2)
{
break;
}
printf("pthread count:%d\n", *nCount);
printf("pthread_start_routine\n");
}
return "I'm died";
}
int main(int argc, const char *argv[])
{
pthread_t tid;
int nCount = 0;
int nRet;
int *pRetVal = NULL;
pthread_attr_t *attr;
pthread_attr_init(&attr); //初始化线程的属性对象
pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //设置线程可设置为是否可被joinable或者detached状态
nRet = pthread_create(&tid, &attr, pthread_start_routine, (void *)&nCount); // 创建线程
if (nRet != 0)
{
perror("pthread_create is error\n");
return -1;
}
pthread_attr_destroy(&attr); //释放线程的属性对象
while (1)
{
sleep(1);
printf("main\n");
}
return 0;
}
运行结果
线程优先级系列说明
Linux内核的三种调度策略:
1.SCHED_OTHER 分时调度策略
2.SCHED_FIFO 实时调度策略,先到先服务。一旦占用cpu则一直运行。一直运行直到有更高优先级任务到达或自己放弃
3.SCHED_RR实 时调度策略,时间片轮转。当进程的时间片用完,系统将重新分配时间片,并置于就绪队列尾。放在队列尾保证了所有具有相同优先级的RR任务的调度公平
Linux线程优先级设置:
首先,可以通过以下两个函数来获得线程可以设置的最高和最低优先级,函数中的策略即上述三种策略的宏定义:
int sched_get_priority_max(int policy);
int sched_get_priority_min(int policy);
注意:SCHED_OTHER 是不支持优先级使用的,而 SCHED_FIFO 和 SCHED_RR 支持优先级的使用,他们分别为1和99,数值越大优先级越高。
设置和获取优先级通过以下两个函数:
int pthread_attr_setschedparam(pthread_attr_t *attr, const struct sched_param *param);
int pthread_attr_getschedparam(const pthread_attr_t *attr, struct sched_param *param);
param.sched_priority = 51; //设置优先级
系统创建线程时,默认的线程是 SCHED_OTHER。所以如果我们要改变线程的调度策略的话,可以通过下面的这个函数实现。
int pthread_attr_setschedpolicy(pthread_attr_t *attr, int policy);
上面的param使用了下面的这个数据结构:
struct sched_param
{
int __sched_priority; // 所要设定的线程优先级
};
线程的竞争与互斥mutex
线程锁mutex简介
多个线程的同时执行,导致了一个问题:同一个资源可能会被多个线程同时访问,这个叫资源的竞争。
为了解决多线程导致的资源竞争问题,就要保证竞争的资源在被访问时是互斥的,不能同时访问。
线程的mutex可以看作是一把锁,当执行到竞争的资源时,执行以下步骤,完成资源的互斥访问,解决竞争的问题。
-
执行到竞争资源时,先获取锁。未上锁则执行竞争资源,如果被别人获取到,则睡眠等待;
-
执行完竞争资源的代码后,释放锁,并唤醒因这把锁睡眠的线程;
通过以上两点,可以保证竞争资源的互斥访问,通过竞争同一锁的方式来得到执行竞争资源的机会。
线程锁相关函数说明
pthread_mutex_init
/**
* @brief 初始化一个互斥锁,以mutexattr的方式创建
*
* @param[in] mutex 互斥量指针
* @param[in] mutexattr 互斥量属性指针,默认传递NULL,除非有以下使用需求,一般地可以不关心
* 通过pthread_mutexattrattr_setpshared函数设置PTHREAD_PROCESS_SHARED使得进程间共享锁
* 通过pthread_mutexattr_gettype/pthread_mutexattr_settype获取和设置锁的类型,有以下几种
* PTHREAD_MUTEX_NORMAL/PTHREAD_MUTEX_ERRORCHECK/PTHREAD_MUTEX_RECURSIVE,
* 默认使用PTHREAD_MUTEX_DEFAULT,不检测死锁,当重复锁定一个互斥量时会导致死锁,也不能释放别人锁定的锁
* PTHREAD_MUTEX_ERRORCHECK是递归锁,可以重复上锁,但是必须当前线程执行相同的锁定和解锁,别的线程才能得到这把锁
* PTHREAD_MUTEX_ERRORCHECK会自动检测死锁,如果一个线程试图对一个互斥锁重复锁定,将会返回一个错误代码
* 也可以通过以下的方式快速设置一把锁的属性:
* 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;
*
* @return 成功返回0,错误返回错误码
* */
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);
pthread_mutex_lock
/
* @brief 获取互斥锁
*
* @param[in] mutex 互斥量指针
* @return 成功返回0,错误返回错误码
* */
int pthread_mutex_lock(pthread_mutex_t *mutex);
pthread_mutex_trylock
/**
* @brief 尝试获取互斥锁,获取不到不会死锁,只会返回错误
*
* @param[in] mutex 互斥量指针
* @return 成功返回0,错误返回错误码
* */
int pthread_mutex_trylock(pthread_mutex_t *mutex);
pthread_mutex_unlock
/**
* @brief 互斥量解锁,并唤醒等待这把锁而睡眠的线程
*
* @param[in] mutex 互斥量指针
* @return 成功返回0,错误返回错误码
* */
int pthread_mutex_unlock(pthread_mutex_t *mutex);
pthread_mutex_destroy
/**
* @brief 释放互斥量
*
* @param[in] mutex 互斥量指针
* @return 成功返回0,错误返回错误码
* */
int pthread_mutex_destroy(pthread_mutex_t *mutex);
线程锁示例代码说明
以下代码创建了一个线程,主线程和子线程分别打印大小写的hello和world。但是hello和world中会有睡眠等待,在不用互斥锁的情况下可能的打印为:"hello, HELLO,WORLD world"这种交叉的方式。
我们将通过互斥锁的方式,保证两个线程都能够正常交替的打印完整的信息,预期的打印结果如下:
hello,world
HELLO,WORLD
线程锁示例代码
pthread_mutex.c
#include<stdio.h>
#include <pthread.h>
#include <unistd.h>
pthread_mutex_t mutex;
void *start_pthread(void *args)
{
while(1)
{
sleep(1);
pthread_mutex_lock(&mutex); //获取互斥锁
printf("hello, ");
fflush(stdout);
sleep(1);
printf("world\n");
fflush(stdout);
pthread_mutex_unlock(&mutex); //互斥量解锁
}
}
int main(int argc, const char* argv[])
{
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, start_pthread, NULL); //创建线程
if(ret != 0)
{
printf("pthread_create is error\n");
return -1;
}
ret = pthread_mutex_init(&mutex, NULL); //初始化一个互斥锁
if(ret != 0)
{
printf("pthread_mutex_init is error\n");
return -1;
}
while(1)
{
sleep(1);
pthread_mutex_lock(&mutex); //获取互斥锁
printf("HELLO, ");
fflush(stdout);
sleep(1);
printf("WORLD\n");
fflush(stdout);
pthread_mutex_unlock(&mutex); //互斥量解锁
}
pthread_join(tid, NULL); //阻塞等待进程结束
pthread_mutex_destroy(&mutex); //释放互斥量
return 0;
}
运行结果
线程的条件变量
线程条件变量简介
线程执行在有些时候会需要一个条件才能继续执行代码,如果条件不成立,线程最好能够睡眠,以减轻对CPU造成的负担。
线程条件变量既是这样一种机制:
- 线程A在条件不满足时,通过线程条件变量睡眠等待条件成立,直到等到其他线程改变满足的条件并唤醒;
- 线程B改变了线程A满足执行的条件,并唤醒A线程;
线程条件变量相关函数说明
pthread_cond_init
/**
* @brief 初始化线程条件变量
*
* @param[in] cond 线程条件变量指针
* @param[in] cond_attr 线程条件变量属性指针,默认传递NULL,可以通过以下设置为线程/进程间共享
* 通过pthread_condattr_getpshared/pthread_condattr_setpshared设置为线程或进程有效范围
* 也可以通过以下的方式快速设置一把锁的属性:
* 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;
*
* @return 成功返回0,错误返回错误码
* */
int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);
pthread_cond_signal
/**
* @brief 通知唤醒一个在cond上睡眠的线程
*
* @param[in] cond 线程条件变量
* @return 成功返回0,错误返回错误码
* */
int pthread_cond_signal(pthread_cond_t *cond);
pthread_cond_broadcast
/**
* @brief 通知唤醒所有在cond上睡眠等待的线程
*
* @param[in] cond 线程条件变量
* @return 成功返回0,错误返回错误码
* */
int pthread_cond_broadcast(pthread_cond_t *cond);
pthread_cond_wait
/**
* @brief 解锁mutex,并在cond条件上睡眠等待
*
* @param[in] cond 条件变量指针
* @param[in] mutex 互斥量指针
* @return 成功返回0,错误返回错误码
* */
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
pthread_cond_timedwait
/**
* @brief 解锁mutex,并在cond条件上睡眠等待abstime长的时间
*
*@param[in] cond 条件变量指针
* @param[in] mutex 互斥量指针
* @param[in] abstime 睡眠等待的时间
* @return 成功返回0,错误返回错误码
* */
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);
pthread_cond_destroy
/**
* @brief 释放条件变量
*
* @param[in] cond 线程条件变量
* @return 成功返回0,错误返回错误码
* */
int pthread_cond_destroy(pthread_cond_t *cond);
线程条件变量代码实现
以下代码实现了g_nFlag不大于0的时候,子线程阻塞等待。
直到另一个线程打破这个条件,并通过pthread_cond_signal/pthread_cond_broadcast唤醒由pthread_cond_wait睡眠的线程,继续执行代码。
这里g_nFlag被看作是一个任务,子线程没有任务的时候(g_nFlag<=0),子线程睡眠,当有任务(g_nFlag>0)时,子线程执行代码。虽然代码简单,但是它是一种事件驱动方式的编程实践,是一种程序设计的方式,开发者应该掌握和熟练使用它,以使得代码能够更高效的运行任务。
pthread_cond.c
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int g_nFlag = 0;
pthread_mutex_t mutex; // 线程互斥锁
pthread_cond_t cond; // 线程条件变量
void *start_pthread(void *args)
{
while (1)
{
pthread_mutex_lock(&mutex); // 获取互斥锁
while (g_nFlag <= 0)
{
pthread_cond_wait(&cond, &mutex); // 解锁mutex,并在cond条件上睡眠等待
}
pthread_mutex_unlock(&mutex); // 互斥量解锁
g_nFlag--;
printf("after pthread_cond_wait\n");
}
return NULL;
}
int main(int argc, const char *argv[])
{
pthread_t tid;
int ret;
ret = pthread_create(&tid, NULL, start_pthread, NULL); // 创建线程
if (ret != 0)
{
printf("pthread_create is error!\n");
return -1;
}
ret = pthread_mutex_init(&mutex, NULL); // 初始化互斥锁
if (ret != 0)
{
printf("pthread_mutex_init is error!\n");
return -1;
}
ret = pthread_cond_init(&cond, NULL); //初始化线程条件变量
if (ret != 0)
{
printf("pthread_cond_init is error!\n");
return -1;
}
while (1)
{
printf("input value:");
scanf("%d", &g_nFlag);
pthread_mutex_lock(&mutex); // 获取互斥锁
pthread_cond_signal(&cond); // 通知唤醒一个在cond上睡眠的线程
pthread_mutex_unlock(&mutex); // 互斥量解锁
}
pthread_join(tid, NULL); // 阻塞等待线程释放
pthread_cond_destroy(&cond); // 释放条件变量
pthread_mutex_destroy(&mutex); // 释放互斥量
return 0;
}
运行结果
实现万金油任务分发器
万金油任务分发器简介
有一类线程是事件驱动型的,当收到消息后,就开始执行代码,没有收到消息时,就睡眠等待。我们把线程执行的消息看作是一个任务,即线程任务。
线程要执行任务,需要另一个线程提供任务消息,因此需要定义一个消息的队列来存放这一类任务消息,以下代码提供了一个线程安全的消息队列,并且使用线程的互斥锁和条件变量来完成任务的收发、执行的过程。
为什么叫万金油任务分发器?这是因为从设计角度说,我们总是希望一个线程专门做一类任务,而不是把一个线程搞的很复杂和臃肿,并且这类任务由其他线程分发过来,我们只管有消息执行,无消息睡眠即可。以下代码抽象了基于线程安全的任务分发的机制,调用者只用关心处理自己的消息即可。