文章目录
一、前言
之前转载过4篇很好的文章,链接如下:
本片只是做一个简略介绍,详细示例教学可以看上面4篇。
下面是对 一些定义,函数接口的简单介绍。
二、相关 知识点 简介
额外补充一个很好的 C/C++多线程编程教程
-
当启动一个程序时,操作系统创建一个进程,并在该进程中执行程序。一个进程包括一个或多个线程。每个线程又是一个局部进程,它以独立于其他局部进程的方式执行一个命令序列。
-
当进程启动时,它的主线程则成为活动线程。这时,任何正在运行的线程都可以启动其他线程。当进程终止时,例如,通过在 main()函数中执行一个 return 语句或通过调用 exit()函数,所有已开启但还未结束的线程都会被终止。
-
系统调度器为所有可运行的线程平均分配可用的 CPU 时间。通常,调度器是抢占式的:它会中断正在执行的线程,给中央处理单元(CPU)留出可用的短暂时间,并将 CPU 分配给其他线程使用一段时间。
这种调度的结果是:即使是在单处理系统上,在用户面前运行的线程看上去像是在同时执行,实际上,只有在多处理器系统中,几个线程才可能真正地同时执行。 -
每一个进程在内存中都有自己的地址空间,并拥有独占的资源,例如,打开的文件。一个进程中的所有线程都继承该进程的资源。最具有意义的是,在一个进程中的几个线程共享一个地址空间。这使得在一个进程中的任务切换比在不同进程间的任务切换要简单得多。
-
然而,为了在不同线程间切换任务,每个线程也拥有自己的资源:包括栈存储器和 CPU 寄存器。这些资源允许每个线程在不受其他线程干扰的条件下,处理自身的本地数据。此外,一个线程也可以具有线程专用的永久内存。
-
对于一个给定进程,由于它内部的所有线程均使用相同的地址空间,所以它们共享全局数据与静态数据。
然而,这也意味着,同一个进程中的两个不同线程可以同时访问同一个内存单元。这种情况在 C 标准中被称为数据竞争(data race),或者通常称之为竞态条件(race condition)。 -
为了防止在共享数据时出现冲突,当这些不同线程使用内存中相同位置时,程序员必须明确地同步这些不同线程的写操作或读写操作(使用线程锁或原子操作)。
三、相关 类型/函数 介绍
线程
创建线程 pthread_create
线程创建函数包含四个变量,分别为:
- 一个线程变量名,被创建线程的标识
- 线程的属性指针,缺省为NULL即可
- 被创建线程的程序代码
- 程序代码的参数
For example:
- pthread_t thrd1; // 线程名
- pthread_attr_t attr; //NULL就行
- void thread_function(void argument); //线程里要跑的函数
- char *some_argument; //需要传入函数的参数
pthread_create(&thrd1, NULL, (void *)&thread_function, (void *) &some_argument);
结束线程 pthread_exit
线程结束调用实例:
pthread_exit(void *retval); //retval用于存放线程结束的退出状态
线程等待 pthread_join
pthread_create调用成功以后,新线程和老线程谁先执行,谁后执行用户是不知道的,这一块取决与操作系统对线程的调度,如果我们需要等待指定线程结束,需要使用pthread_join函数,这个函数实际上类似与多进程编程中的waitpid。
举个例子,以下假设 A 线程调用 pthread_join
试图去操作B线程,该函数将A线程阻塞,直到B线程退出,当B线程退出以后,A线程会收集B线程的返回码。
该函数包含两个参数:
- pthread_t th //th是要等待结束的线程的标识
- void **thread_return //指针thread_return指向的位置存放的是终止线程的返回状态。
调用实例:
pthread_join(thrd1, NULL);
#include <pthread.h>
void *TrainModelThread(void *id) {
...
pthread_exit(NULL); //线程退出
}
pthread_t *pt = (pthread_t *)malloc(num_threads * sizeof(pthread_t)); //创建 num_threads 个线程
for (a = 0; a < num_threads; a++)
pthread_create(&pt[a], NULL, TrainModelThread, (void *)a); //注册线程
for (a = 0; a < num_threads; a++)
pthread_join(pt[a], NULL); //线程执行
线程的同步与互斥
补充阅读 : 链接
互斥/锁
互相排斥(mutex exclusion)技术,简称为互斥(mutex),它用于防止多个线程同时访问共享资源。互斥技术采用一个对象控制独占访问权限,该对象称之为互斥。配合条件变量(condition variable),互斥可以实现广泛的同步访问控制。例如,它们允许程序员为数据访问操作指定执行次序。
在 C 程序中,一个互斥采用类型为 mtx_t 的对象表示,它能在一段时间内被一个线程锁定,而其他线程必须等待,直到它被解锁。在头文件 threads.h 中,包括了关于互斥操作的所有声明。
- 在主线程中初始化锁为解锁状态
//锁初始化
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, NULL);
- 在编译时初始化锁为解锁状态
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- 访问对象时的加锁操作与解锁操作
//加锁
pthread_mutex_lock(&mutex)
//释放锁
pthread_mutex_unlock(&mutex)
//在需要锁定的代码段前后加上即可。
最重要的互斥函数有:
创建一个互斥,该互斥的属性由 mutextype 指定。
如果成功创建了一个新互斥,函数 mtx_init()会将新互斥写入由参数 mtx 引用的对象,然后返回宏值 thrd_success。
pthread_mutex_t writable[100]; //实例化锁(互斥)对象
pthread_mutex_init(&writable[i], NULL); //锁的初始化
阻塞正在调用的线程,直到该线程获得参数 mtx 引用的互斥。除该互斥支持递归的情况以外,正在调用的线程不能是已持有该互斥的线程。
如果调用成功获得互斥,则函数返回值 thrd_success,否则,返回值 thrd_error。
pthread_mutex_lock(&writable[i]); //加锁
释放参数 mtx 引用的互斥。在调用函数 mtx_unlock()之前,调用者必须持有该互斥。
如果调用释放互斥成功,则函数返回值 thrd_success,否则,返回值 thrd_error。
pthread_mutex_unlock(&writable[i]); //解锁
pthread_mutex_destroy(mtx_t*mtx); //销毁mtx引用的(互斥)锁,并释放它所有资源
代码示例:
通过加锁,保证sharedi变量在进行变更的时候,只有一个线程能够取到,并在在该线程对其进行操作的时候,其它线程无法对其进行访问。
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
int sharedi = 0;
void increse_num(void);
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
int main(){
int ret;
pthread_t thrd1, thrd2, thrd3;
ret = pthread_create(&thrd1, NULL, (void *)increse_num, NULL);
ret = pthread_create(&thrd2, NULL, (void *)increse_num, NULL);
ret = pthread_create(&thrd3, NULL, (void *)increse_num, NULL);
pthread_join(thrd1, NULL);
pthread_join(thrd2, NULL);
pthread_join(thrd3, NULL);
printf("sharedi = %d\n", sharedi);
return 0;
}
void increse_num(void) {
long i,tmp;
for(i=0; i<=100000; i++) {
/*加锁*/
if (pthread_mutex_lock(&mutex) != 0) {
perror("pthread_mutex_lock");
exit(EXIT_FAILURE);
}
tmp = sharedi;
tmp = tmp + 1;
sharedi = tmp;
/*解锁锁*/
if (pthread_mutex_unlock(&mutex) != 0) {
perror("pthread_mutex_unlock");
exit(EXIT_FAILURE);
}
}
}
分析
-
锁保护的并不是我们的共享变量(或者说是共享内存),对于共享的内存而言,用户是无法直接对其保护的,因为那是物理内存,无法阻止其他程序的代码访问。事实上,锁之所以对关键区域进行了保护,在本例中,是因为所有线程都遵循了一个规则,那就是在进入关键区域钱加同一把锁,在退出关键区域钱释放同一把锁
-
我们从上述运行结果中可以看到,加锁是会带来额外的开销的,加锁的代码其运行速度,明显比不加锁的要慢一些,所以,在使用锁的时候,要合理,在不需要对关键区域进行保护的场景下,我们便不要画蛇添足,为其加锁了