shovat wrote:Does it add complexity to the program that programmers prefer to avoid?
That's it. It adds a LOT of complexity. And I really mean A LOT.
Edit:
Let me try and explain how it does that. Basically, only a single process can run on a single core at a time (this is technically untrue, but I'll fix it before I'm done). Each process has its own memory and when a process is running on a processor (core) that processor (core) has access to certain information of that process, such as what the memory the process has access to is, which has to be updated whenever a process is "swapped out" for another. The operating systems swaps processes at its own leisure and this is something you - as a programmer - have absolutely zero control over.
However, to make things like working on multiple cores possible, there's a thing called a thread. Basically, it's a mini-process that has everything that a process has except for its own memory. It shares its memory with other threads. In fact, we need to update the terminology from the previous paragraph to reflect the existence of threads. Each process has a number of threads, which are defined by things like where in the code they are and their stack (the exact purpose of which is not relevant). Each process then has an assigned area of the memory, which is shared by all of its threads. Threads are actualy the things that are swapped in and out, but you still have no control over it. Threads can run simultaneously on different cores, but what makes this so hard is the shared memory.
Normally (which is without multiple threads in a process) you don't have to worry at all about being swapped in and out. You only have your internal state and things like user input are built on systems that don't care about this. When writing a multi-threaded application, you have to take care of the memory very carefully. Basically, any other thread can get an operation at almost any moment during your code. Let me give a very basic example of what could you wrong.
Let's say that I have a process with two threads. There is a number of tasks for them to complete, which are all completely separate from one another. I have two threads doing these tasks. I want to keep track of how many tasks have been completed. I use the following code (this is really simple so even if you can't program, you'll be able to follow it
code wrote:while (anyTasksLeft()) // as long as there are any tasks left
{
... do a task ...
tasksDone++; // increase the number of tasks we consider done by 1
}
Actually, the only part of I'm interested in is the "tasksDone++" part. While not read as such normally when programming, for this explanation I'll have to boil it down to its essence, which is: 1. Read the value from the memory we named tasksDone 2. Add one to that value 3. Store that value in the memory we named tasksDone. Or:
1. Read tasksDone
2. Add one
3. Write tasksDone
Now let's see what might happen if you have more than one thread (and remember: you have no control over when exactly a process is going to be swapped in or out). We have processes P and Q, and this is one scenario that might happen:
P1. Read tasksDone {2}
Q1. Read tasksDone {2}
Q2. Add one {3}
Q3. Write tasksDone {3}
P2. Add one {3}
P3. Write tasksDone {3}
We started with 2 tasks done, then two tasks were finished and tasksDone is now only 3! Even more serious problems might occur if we look up a couple of lines. You might consider the situation in which both threads see that there's one task left, but by the time the slower thread actually wants to a task, the other process has already done it.
There are tricks around this problem, that smartly let different thread handle the same memory in a smart way. For example, here one might imagine each thread keeping a separate counter of how many tasks that thread has completed, and the (third) thread that outputs how many tasks have been completed, reads all those counters and adds their values up. It gets you a "sort of accurate" result at all times. However, these tricks are mostly of academic interest and the solution of the real world is to use locks. Basically, the first thread to request a lock gets it, and the next thread to request the same lock has to wait until the lock is released before it can get the lock and continue.
Because of this waiting that is involved with locks, you want to minimize the usage of locks. As such, the general idea is to split the memory into separate parts, one for each thread and a shared part. The idea is that each thread may freely access its own memory but has to lock whatever it wants to access from the shared part and it may never touch the memory of another thread. However, these rules aren't enforced at all (and in fact, there might be a good reason why you're not sticking to them). As such, the programmer has to make these divisions himself and properly request locks when accessing the shared memory. In both of those tasks it is extremely easy make a mistake and moreover, there's nothing to tell you when you made a mistake, as it will often just ran as if everything had gone right *most of the time*.
That
most of the time stems from the fact that you have no control over the swapping in and out. (You only have to worry about swapping as long as there is only a single core, when there are more than one, it's also just about other core happening to do something at an unpredictable moment, but the issue remains the same.) Whether something goes wrong depends the relative timing of the threads, which again depends on the circumstances at a very small level you have no influence over. You generally can't even monitor these circumstances or even say in which order the threads got which operations executed. This also makes it extremely hard to fix bugs caused by multithreading, as you can just not reproduce them.
I hope that was maybe remotely understandable and yes, it's still really simplified.