bi-directional half-duplex communication between two AT89C2051's micros without serial port
I want to enable as much multitasking as possible
  


That just means that you will be using interrupts in some fashion.

The alternative, salting your code with short snippets of identical polling code placed strategically to simulate periodic events is as ugly as it gets. So you don't ever do it. I've seen it done before. And it's terrible and will make your code for all practical purposes unmaintainable.


  
  until all bytes are received which a counter tracks
  half-duplex bi-directional communication
  


These suggest to me that you actually may want a master/slave relationship, but where either can become the master for the period of the communication. It can be done by having only one master, ever. But the way you write suggests you would either like a protocol without a specific master (such as the asynchronous protocol used in RS-232, for example) or else a means by which either can become a master for the purposes of a burst of communication.


  
  18.432Mhz crystal
  20Mhz crystal
  


With two different processors, even if they are running at the so-called same clock rate, will start out with some phase difference which also drifts around relative to each other. Nothing is perfect in life. But with two different clock rates, one thing I also look at is the GCD. In this case, that's 32000. This means one option might be to use a divisor of 576 on your \$18.432\:\textrm{Mhz}\$ micro and a divisor of 625 on your \$20.000\:\textrm{Mhz}\$. That would get the bit timing, if you used a protocol where that mattered, into the similar ballparks. You'd still have phase error and drifting. But if you chose to use an asynchronous RS-232 like protocol, you could probably manage that after a fashion at a bit rate of \$32\:\textrm{kHz}\$.

But I also think Dave is right, too. Using something like \$\textrm{I}^2\textrm{C}\$ has its advantages over dinking around with too much over-sampling (you may need 2X over-sampling or more, regardless, since this is software; but this varies a bit with the hardware interrupt modes you can use, too) and getting your bit timings zeroed in.

The tradeoffs are now something like this:


Use multi-master \$\textrm{I}^2\textrm{C}\$. This takes a little extra effort in the code for master and slave to achieve the multi-master parts. But there is a standard for this too and plenty of example code to examine, I'm sure. The limitation here is that your communications are half-duplex using two lines. But you've already accepted this. And arbitration issues might have you scrambling for those GCD divisors I mentioned above.
Use a copy of the asynchronous format (start bit, N data bits, and let's say two stop bits) used by RS-232 and commonly found in UART hardware. The advantage here is that you get full-duplex communications. But you've indicated you don't need that. You'd probably want to use those GCD divisors here, too, to get the bit timing close enough to each other.
Use \$\textrm{I}^2\textrm{C}\$ in simple master-slave mode. Slaves can not push any data to the master. Instead, the master would have to ask the slave if there was something to send back. But you could also go this route. It would spare you arbitration issues, too.


In all cases you will have interrupts involved and either, (A) use state machines; or, (B) use an operating system with threads (cooperative ones, if not pre-emptive.)

It doesn't take much, not even on the 8051, to pony up a simple cooperative set of threads. (Yes, I've done it on a SiLabs C8051F061 and using SDCC and not Keil.) It really cleans up code and makes it very maintainable, because it avoids the chain of state transitions.

But a clearly documented set of state transitions isn't bad, either. Most with any experience using state machines can follow the clear documentation and see which code performs which step. So I think either approach would be fine.


  
  they are externally pulled high to VCC (of 5 volts) through 10K resistors.
  


So this is good as \$\textrm{I}^2\textrm{C}\$ wants that and asynchronous protocols can work with that.



Why don't you do some googling on \$\textrm{I}^2\textrm{C}\$ state machines, read through them until you feel you understand them, and also read through the documentatation available from this \$\textrm{I}^2\textrm{C}\$ documentation that is available to you. Similarly, consider an asynchronous protocol, as well.

One thing in your favor is that you do have the ability to get similar bit times on each device. This doesn't fix phase differences or drift. But a little bit of over-sampling can address that. Or you can just leave them free-run and work through the issues of that process.

Chances are, there are many different examples of code readily available on the web to cover you here, too. I'd recommend doing some searching there, as well. You are the one who needs this. So you should be doing the up-front work here. Not asking us to do that for you.

That said, if you get yourself educated about these methods and can narrow down your questions to something specific, I'm sure you'll get some wise advice. But right now, you are just throwing code against a wall to see what sticks. And I think we'd like you to do your own work getting up to speed on existing methods which are tried and true, before you start asking anyone to come up with something new for you or something based only on your own (apparently quite limited) thinking about this. If you are experienced, you can indeed draft up your own protocol and make it work -- first time, too, out of the box. But you aren't experienced. So you should first work from the benefit of others.