马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
×
目次
1.线程互斥
(1)临界资源和临界区
(2)互斥和原子性
①互斥
②原子性
(3)加锁息争锁(互斥锁)的原理
(4)pthread_mutex系列函数和变量
①lock、unlock
②pthread_mutex_t
(5)加锁常见错误
(6)对互斥锁的熟悉
2.线程同步
(1)线程互斥引发的标题
(2)pthread_cond系列函数和变量
①pthread_cond_t
②wait、signal
1.线程互斥
(1)临界资源和临界区
临界资源:多实行流中必要被掩护的共享的资源。如某一个函数可被全部线程访问,但是在同一时候全部线程都进入该函数会导致这个函数实行非常(如打印紊乱),那么这个函数就是不可重入函数,也就是必要被掩护的共享资源,即临界资源。对于线程来说,全部函数代码都是共享的,因此对多线程来说临界资源就是要防止多个线程同时访问的资源。
临界区:每个线程内部访问临界资源的代码。临界区是代码,掩护临界资源的本领就是掩护临界区,只要访问临界资源的代码受到掩护,那么对应的临界资源就会受到掩护。简而言之,掩护临界区是本领,掩护临界资源才是目的。
(2)互斥和原子性
①互斥
先看一下下面的代码,末了m的结果应该是多少呢?
- #include <iostream>
- #include <unistd.h>
- #include <pthread.h>
- using namespace std;
- int m = 10;
- void *fun(void *p)
- {
- while (m > 0)
- {
- sleep(1);
- m--;
- }
- return nullptr;
- }
- int main()
- {
- pthread_t pid_1, pid_2, pid_3, pid_4, pid_5;
- pthread_create(&pid_1, nullptr, fun, (void *)nullptr);
- pthread_create(&pid_2, nullptr, fun, (void *)nullptr);
- pthread_create(&pid_3, nullptr, fun, (void *)nullptr);
- pthread_create(&pid_4, nullptr, fun, (void *)nullptr);
- pthread_create(&pid_5, nullptr, fun, (void *)nullptr);
- pthread_join(pid_1, nullptr);
- pthread_join(pid_2, nullptr);
- pthread_join(pid_3, nullptr);
- pthread_join(pid_4, nullptr);
- pthread_join(pid_5, nullptr);
- printf("m = %d\n", m);
- return 0;
- }
复制代码 结果是
毕竟上,每次结果都大概不雷同,在以上实例中,m可以被全部线程访问,即共享资源,访问修改这个共享资源的代码就是fun函数。如果全部线程同时访问这块代码,就有大概出现错误。
比方在m = 1时,pid_1线程实行到while (m > 0)这个语句时,会先将m的值从内存读到寄存器中,当读完之后,此时该线程时间片到了被切走后,该线程的寄存器的上下文被生存了下来。切到另一个线程之后,如果它实行到while (m > 0)这个语句时,它也会将m的值从内存读到寄存器中,别的线程也云云,如许就会导致判定语句将多个线程放进去了。每个线程的状态都不一样,当pid_1将m--完成之后,大概pid_2才会实行到这里,这个时间m又被--,同理,pid_3和pid_4都到场m--,导致m终极为负数。
因此这个m着实是必要被掩护起来的,即临界资源,同样,fun函数中访问临界资源的代码(临界区)也必要被掩护。我们通过掩护临界区来掩护临界资源。
这就叫线程互斥,即恣意时候只能有一个实行流进入临界区,访问临界资源。
②原子性
实行判定语句,CPU分为3步实行,即从内存获取数据到寄存器、分析、实行指令三步。
自减语句也分为3步,即从内存获取数据到寄存器、自减、写回内存三步。
以上两个语句均会被拆分为三个指令实行,而且这三个指令都划一告急,当恣意一个指令被实行后就立马切走线程,就有大概导致出现错误。也就是说,实行判定语句和自减不是原子性的。
但是,导致上述代码的错误并不是单纯是由于这两个语句不是原子的,而是while循环的代码这个团体不是原子的。原子指的就是要么完成,要么还没完成,它不会被任何调理机制打断。从while循环的第一个判定语句的第一条指令开始,就视为这个代码块开始实行了,如果这个while循环的代码是原子的,就只有实行完和还没实行完两种状态,那么当第一个线程开始实行第一条指令后,就算存在时间片的轮转,别的线程也无法进入while的实行,由于第一个线程还没实行完。
从另一个角度来明白,一行指令肯定是原子的,由于无论时间片是怎样的,这条指令要么还没实行,要么就必须实行完,不存在实行了一半就被切走了。对于这条指令来说,同一时间内肯定只有一个线程在实行它,只要当火线程开始实行它,那么就必须等这个线程实行完这个指令后,别的线程才有时机实行这个指令。
如今我们拓展一条指令为一个代码块,就是我前面所讲的例子。
总结:导致临界资源出现紊乱的缘故原由是临界区同时被多个线程访问。其根本缘故原由是while循环代码块团体不是原子的,能同时放进来多个线程。如果while循环团体是原子的,那么一次性就只能允许一个线程进入,也就不会导致临界资源的错误了。
下面我们就要详细讲讲怎样将一整个代码块变为原子的。
(3)加锁息争锁(互斥锁)的原理
起首当锁被创建之后,内存中就会新建一个变量mutex,这个变量默以为1。CPU内部有一个寄存器eax,在实行lock指令前默认先设为0,当调用lock指令时,eax和mutex内里的数据会举行交换(寄存器和内存内里的值举行交换),这个时间eax内里就为1,mutex内里的值为0。mutex不会主动修改自己的值,因此它会不绝是0。如果实行lock指令之后eax为1,那么就让这个线程实行被加锁的代码。
在该线程实行代码期间,如果有别的线程来申请锁同样必要上面的步调,eax设置为0,和mutex交换,判定eax是否为1。关键之处在于,当第二个线程交换mutex和eax时,eax自己是0不必多说,但mutex内里也是0,由于第一个线程已经把mutex内里的1交换走了!当线程切换时,第一个线程的eax的1被当作硬件上下文生存起来了,只留下了一个值为0的mutex在内存中。也就是说不管线程怎么切换,只要第一个线程不把mutex的1交换归去,其他线程无论怎么申请,都无法得到谁人1。如果eax不是1,那么线程会被壅闭在加锁的函数里,隔一段时间后再次设eax为0,和mutex交换,判定eax。
因此加锁的焦点就是谁人交换寄存器和内存变量值的指令,只要谁先把内存中mutex的1拿走,谁就拥有临界区的访问权,而别的线程都必须等在原地。只有解锁时,线程把谁人1还给mutex之后,别的线程才有时机进入临界区。通过这种方式,就实现了对整个代码块加锁,使其在线程看来是原子的。
形象的明白:mutex就是一间屋子表面墙上挂着的钥匙,而且这钥匙一次性只能给一个人用,谁拿到钥匙谁就可以用这个房间,而且其他人永世进不来,只能在表面等着。利用屋子的人可以中途上茅厕、用饭等,只要不重新把钥匙挂回墙上,这个屋子始终是他的。对其他人来说,拿着钥匙的人怎么用屋子这件事就是原子的,要么没用完,要么用完了。当利用这屋子的人将钥匙重新挂回墙上,别的人才气来抢钥匙,劫掠占据权。
为了实现互斥锁的利用,大多数体系布局都提供了swap和exchange指令,这个指令可以让寄存器和内存单位交换数据。
(4)pthread_mutex系列函数和变量
①lock、unlock
pthread_mutex_lock( pthread_mutex_t * mutex )加锁,pthread_mutex_unlock( pthread_mutex_t * mutex )解锁。
pthread_mutex_lock就是实行上述eax设置为0,交换eax和mutex,判定eax是否为1的指令聚集,谁人1就是锁,凡是没有申请到锁的线程会被壅闭在函数中,直到1被还返来了。相对的,unlock就是还回那把锁的过程,也就是说,lock和unlock两行代码作为临界区的首尾,在这两个函数中央的代码对线程来说就是原子的,一次性只能通行一个线程。也要注意,锁有借有还,lock之后肯定要unlock。
除此之外,尚有一个函数pthread_mutex_trylock,它和lock唯一的区别是当它申请不到锁时不会不绝壅闭在函数中,它申请锁后会返回一个值,让线程自主判定要不要继续申请。
②pthread_mutex_t
lock和unlock会用到一个变量,这个变量就是锁pthread_mutex_t。我们通过pthread_mutex_t mutex来界说它,它的底层界说如下,是一个团结体。
这把锁就对应内存中创建的mutex的1,每当创建一个pthread_mutex_t就会有一个1在内存中创建,可以说这个变量就是互斥锁的焦点。团结前面的知识,领会它在加锁的整个流程的作用,领会为什么申请锁要传pthread_mutex_t *
下面是对上面代码的改进
通过加锁,锁间的代码在线程看来就酿成原子的了,即实现了线程互斥,因此临界区的代码就不会被多个线程同时访问,也就不会出现临界资源错误了。
当创建的锁是全局或静态对象时,我们就用宏PTHREAD_MUTEX_INITIALIZER来对这个锁初始化,就像上图那样。我们之后也无需对锁举行任何别的利用。
除此之外,这个锁对象还可以是局部的,但这个时间必要手动init和destroy。
- #include <iostream>
- #include <unistd.h>
- #include <pthread.h>
- using namespace std;
- int m = 10;
- void *fun(void *p)
- {
- pthread_mutex_t* pmutex = static_cast<pthread_mutex_t*>(p);
- pthread_mutex_lock(pmutex);
- while (m > 0)
- {
- sleep(1);
- m--;
- }
- pthread_mutex_unlock(pmutex);
- return nullptr;
- }
- int main()
- {
- pthread_mutex_t mutex;
- pthread_mutex_init(&mutex, nullptr);
- pthread_t pid_1, pid_2, pid_3, pid_4, pid_5;
- pthread_create(&pid_1, nullptr, fun, (void *)&mutex);
- pthread_create(&pid_2, nullptr, fun, (void *)&mutex);
- pthread_create(&pid_3, nullptr, fun, (void *)&mutex);
- pthread_create(&pid_4, nullptr, fun, (void *)&mutex);
- pthread_create(&pid_5, nullptr, fun, (void *)&mutex);
- pthread_join(pid_1, nullptr);
- pthread_join(pid_2, nullptr);
- pthread_join(pid_3, nullptr);
- pthread_join(pid_4, nullptr);
- pthread_join(pid_5, nullptr);
- pthread_mutex_destroy(&mutex);
- printf("m = %d\n", m);
- return 0;
- }
复制代码 注意init函数要在还处于单线程时就利用了,如果在多线程的代码下利用,每个线程访问这行代码后都能申请到锁,那就没故意义了。同理destroy就是烧毁一把锁,肯定要包管在完全用完这把锁之后才气利用。
(5)加锁常见错误
①init和destroy利用位置不妥,每个线程来访问都能申请到锁
②创建锁的位置不妥,多个线程天生了多把锁,临界区相当于没加锁
③部分临界区在锁外,导致加锁后肯定会稳固地发生错误
④每次循环后都会解锁,直接粉碎了代码块的原子性,和没加锁没区别
⑤加锁解锁没有配对,导致不绝有线程被壅闭在lock函数里,这使得代码永世无法竣事。
(6)对互斥锁的熟悉
我们可以大概熟悉到锁自己也是共享资源,但由于加解锁的焦点指令是原子的,使得锁自己是安全的。通过锁,我们可以大概将一段代码变为原子性的,使得线程访问必须一个一个进去,制止了同时访问会带来的错误。
加锁针对的是代码块,但肯定不能无脑对大块代码加锁,要包管细粒度,最好只对临界区加锁。加锁会导致运行速率变慢,由于多线程执利用命时,在加锁段只能一个线程一个线程已往,好比独木桥,这座桥在包管充足用的情况下应只管短。
我们前面讲的互斥锁都是软件实现的,加锁这件事还可以靠硬件实现。如OS不绝在时钟停止,每次停止后会减去历程的时间片。当把外部停止和时钟停止禁用,不减时间片,直到实行完代码后规复时,就实现了硬件的加锁。
2.线程同步
(1)线程互斥引发的标题
当一个线程申请到锁之后,别的线程再申请锁就会被lock,必要期待锁被还返来,当unlock后全部线程再去争取同一把锁。这看似公道,但本来持有锁的谁人线程在新一轮竞争锁的过程中最占据上风。就好比一个人还完钥匙后别的人还没反应过来的时间,他就可以再把锁拿走。这种不公平会引发服从标题,大概导致不绝是同一个线程在利用锁。
要办理长期得不到锁引发的饥饿标题,必要界说一个新的规则,即被lock的线程都放到一个新的期待队列,一个线程还了锁后必须排到队列末了。这些在队列里的线程只有被唤醒后才气申请锁,这个唤醒利用每次都是针对队列头部的一个线程。如许一来,线程颠末放入期待队列 -> 归还锁 -> 唤醒队头线程 -> 队头线程申请锁,就可以完善办理上述不公平标题。
于是说,多个线程访问临界资源必要线程互斥,多个线程访问临界资源还必须有序次性,这就叫线程同步。线程互斥和同步精密关联,互斥包管安全性,同步用于办理互斥锁带来的bug,包管体系调理更加公道和高效。
(2)pthread_cond系列函数和变量
①pthread_cond_t
这就是期待队列对应的团结体。当创建期待队列时,就必要通过pthread_cond_t cond来创建。当把线程放入期待队列的过程中,就要利用这个范例来罗致线程。
它和pthread_mutex_t的创建烧毁规则如出一辙,全局或静态对象可用PTHREAD_COND_INITIALIZER且无需烧毁,局部对象需用init和destroy手动创建烧毁
②wait、signal
先看一下下面的利用,基于原先的代码举行同步改动
详细讲讲wait的详细利用,wait肯定在lock和unlock之间,通常来说就紧跟在lock之后。当拿着锁的单个线程实行wait时,它要通报参数pthread_cond_t * 用于将线程参加期待队列中,尚有第二个参数pthread_mutex_t * 用于还回锁。举个例子,A线程lock后拿到锁,实行wait后,A进入了期待队列而且把锁还归去。B线程就能从lock处申请到锁被放了进来,而且同样实行wait后,进入期待队列并还回锁,线程C、D亦是云云。因此,lock、wait后全部线程都被壅闭在期待队列之中。
注意lock后的线程只要有锁就不会被壅闭了,会放进去一个线程。而wait函数壅闭的线程会不绝在期待队列内里壅闭,只有当对该期待队列实行signal时才会开释队头的线程。注意wait尚有很告急的作用,signal后队头的线程不会立刻被开释,还会被壅闭,这个线程会申请锁直到乐成。以是wait后放出的线程会拿再次回这把锁,带着锁往下实行。
为什么会有这种功能?由于wait是在临界区,被壅闭在wait期待队列的线程手上都没有锁,当要往下实行时必须重新拿到锁,没有锁为何可以大概向下实行临界区的代码,这不就和之前的规定(进入临界区的线程拿着唯一的一把锁,目的是要包管该代码块的原子性)相悖吗?
这才是wait的线程被唤醒后肯定要拿到锁才会向下实行的根本缘故原由,注意领会逻辑的精密,明白大于影象。
signal唯一的参数是pthread_cond_t * ,作用是唤醒相应期待队列的队头线程,只要包管指向的期待队列pthread_cond_t雷同,该函数在恣意地方实行均有效。
在上述代码中,全部实行fun函数的线程都被壅闭在wait队列中,主线程的signal唤醒了wait队列的队头线程,往下继续实行。unlock函数前是signal,它又唤醒了下一个线程,但这个线程必须在锁被还归去后才会被开释,也就是说unlock和signal的序次不会改变实行逻辑,wait开释线程前都必须要申请到锁!
尚有一个唤醒方法pthread_cond_broadcast( &cond ),它会立马唤醒全部队列内里的线程,到场到竞争中,一样平常来说用的很少,毕竟同步的目的就是包管序次性,signal显然更严谨地包管序次性。
下面是局部cond对象的代码
- #include <iostream>
- #include <unistd.h>
- #include <pthread.h>
- using namespace std;
- int m = 10;
- pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
- void *fun(void *p)
- {
- pthread_cond_t *pcond = static_cast<pthread_cond_t *>(p);
- pthread_mutex_lock(&mutex);
- pthread_cond_wait(pcond, &mutex);
- {
- sleep(1);
- printf("当前线程:0x%lx\n", pthread_self());
- m--;
- }
- pthread_cond_signal(pcond);
- pthread_mutex_unlock(&mutex);
- return nullptr;
- }
- int main()
- {
- pthread_cond_t cond;
- pthread_cond_init(&cond, nullptr);
- pthread_t pid_1, pid_2, pid_3, pid_4, pid_5;
- pthread_create(&pid_1, nullptr, fun, (void *)&cond);
- pthread_create(&pid_2, nullptr, fun, (void *)&cond);
- pthread_create(&pid_3, nullptr, fun, (void *)&cond);
- pthread_create(&pid_4, nullptr, fun, (void *)&cond);
- pthread_create(&pid_5, nullptr, fun, (void *)&cond);
- sleep(1);
- pthread_cond_signal(&cond);
- pthread_join(pid_1, nullptr);
- pthread_join(pid_2, nullptr);
- pthread_join(pid_3, nullptr);
- pthread_join(pid_4, nullptr);
- pthread_join(pid_5, nullptr);
- pthread_cond_destroy(&cond);
- printf("m = %d\n", m);
- return 0;
- }
复制代码 实行结果是

|